From 5c151683fff4df6b87623d5db00276bc31d189bb Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 17 Oct 2022 13:52:10 +0200 Subject: [PATCH 01/74] [Synthetics] Fix monitor details project monitor view (#143258) --- .../hooks/use_monitor_latest_ping.tsx | 15 ++++++++----- .../hooks/use_monitor_query_id.ts | 22 +++++++++++++++++++ .../availability_sparklines.tsx | 4 ++-- .../monitor_summary/duration_panel.tsx | 4 ++-- .../monitor_summary/duration_trend.tsx | 8 +++---- .../monitor_summary/step_duration_panel.tsx | 6 ++--- 6 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_query_id.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx index 2017109855bfb..6b9ab1442b9eb 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx @@ -7,6 +7,7 @@ import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { ConfigKey } from '../../../../../../common/runtime_types'; import { getMonitorRecentPingsAction, selectLatestPing, selectPingsLoading } from '../../../state'; import { useSelectedLocation } from './use_selected_location'; import { useSelectedMonitor } from './use_selected_monitor'; @@ -28,10 +29,14 @@ export const useMonitorLatestPing = (params?: UseMonitorLatestPingParams) => { const latestPing = useSelector(selectLatestPing); const pingsLoading = useSelector(selectPingsLoading); - const isUpToDate = - latestPing && - latestPing.monitor.id === monitorId && - latestPing.observer?.geo?.name === locationLabel; + const latestPingId = latestPing?.monitor.id; + + const isIdSame = + latestPingId === monitorId || latestPingId === monitor?.[ConfigKey.CUSTOM_HEARTBEAT_ID]; + + const isLocationSame = latestPing?.observer?.geo?.name === locationLabel; + + const isUpToDate = isIdSame && isLocationSame; useEffect(() => { if (monitorId && locationLabel && !isUpToDate) { @@ -47,7 +52,7 @@ export const useMonitorLatestPing = (params?: UseMonitorLatestPingParams) => { return { loading: pingsLoading, latestPing: null }; } - if (latestPing.monitor.id !== monitorId || latestPing.observer?.geo?.name !== locationLabel) { + if (!isIdSame || !isLocationSame) { return { loading: pingsLoading, latestPing: null }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_query_id.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_query_id.ts new file mode 100644 index 0000000000000..4b1e88461fa16 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_query_id.ts @@ -0,0 +1,22 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import { ConfigKey } from '../../../../../../common/runtime_types'; +import { useSelectedMonitor } from './use_selected_monitor'; + +export const useMonitorQueryId = () => { + const { monitorId } = useParams<{ monitorId: string }>(); + + const { monitor } = useSelectedMonitor(); + + if (monitor && monitor.origin === 'project') { + return monitor[ConfigKey.CUSTOM_HEARTBEAT_ID]!; + } + + return monitorId; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx index bc05d8fcc7a51..308ba581f6b0b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ReportTypes, useTheme } from '@kbn/observability-plugin/public'; -import { useParams } from 'react-router-dom'; import { ClientPluginsStart } from '../../../../../plugin'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; export const AvailabilitySparklines = () => { const { @@ -17,7 +17,7 @@ export const AvailabilitySparklines = () => { observability: { ExploratoryViewEmbeddable }, }, } = useKibana(); - const { monitorId } = useParams<{ monitorId: string }>(); + const monitorId = useMonitorQueryId(); const theme = useTheme(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx index 6d3ff68b2785c..4c6ede0ade308 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ReportTypes } from '@kbn/observability-plugin/public'; -import { useParams } from 'react-router-dom'; import { ClientPluginsStart } from '../../../../../plugin'; import { KpiWrapper } from './kpi_wrapper'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; export const DurationPanel = () => { const { @@ -19,7 +19,7 @@ export const DurationPanel = () => { observability: { ExploratoryViewEmbeddable }, }, } = useKibana(); - const { monitorId } = useParams<{ monitorId: string }>(); + const monitorId = useMonitorQueryId(); return ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx index 8336e33a7e973..3642aa520c211 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx @@ -7,15 +7,15 @@ import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useParams } from 'react-router-dom'; import { ClientPluginsStart } from '../../../../../plugin'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; export const MonitorDurationTrend = () => { const { observability } = useKibana().services; const { ExploratoryViewEmbeddable } = observability; - const { monitorId } = useParams<{ monitorId: string }>(); + const monitorId = useMonitorQueryId(); const metricsToShow = ['min', 'max', 'median', '25th', '75th']; @@ -31,9 +31,7 @@ export const MonitorDurationTrend = () => { }, name: metric + ' Series', selectedMetricField: 'monitor.duration.us', - reportDefinitions: { - 'monitor.id': [monitorId], - }, + reportDefinitions: { 'monitor.id': [monitorId] }, seriesType: 'line', operationType: metric, }))} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx index 120449b88bd04..1fe6ef23a44a0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ReportTypes } from '@kbn/observability-plugin/public'; -import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; import { useSelectedMonitor } from '../hooks/use_selected_monitor'; import { ClientPluginsStart } from '../../../../../plugin'; export const StepDurationPanel = () => { @@ -19,10 +19,10 @@ export const StepDurationPanel = () => { const { ExploratoryViewEmbeddable } = observability; - const { monitorId } = useParams<{ monitorId: string }>(); - const { monitor } = useSelectedMonitor(); + const monitorId = useMonitorQueryId(); + const isBrowser = monitor?.type === 'browser'; return ( From 42344172e3e1ad8527d83d4f02476cb4b6b9fd31 Mon Sep 17 00:00:00 2001 From: Arpit Bhardwaj Date: Mon, 17 Oct 2022 17:36:45 +0530 Subject: [PATCH 02/74] Removed shared_imports.ts from cases (#143427) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/mock/test_providers.tsx | 2 +- .../cases/public/common/shared_imports.ts | 34 ------------------- .../public/components/add_comment/index.tsx | 7 +++- .../public/components/add_comment/schema.tsx | 5 +-- .../case_view/components/edit_tags.tsx | 8 ++--- .../connector_selector/form.test.tsx | 4 +-- .../components/connector_selector/form.tsx | 4 +-- .../connectors/swimlane/validator.ts | 2 +- .../components/create/assignees.test.tsx | 4 +-- .../public/components/create/assignees.tsx | 7 ++-- .../components/create/connector.test.tsx | 4 +-- .../public/components/create/connector.tsx | 8 +++-- .../components/create/description.test.tsx | 4 +-- .../public/components/create/description.tsx | 6 +++- .../public/components/create/form.test.tsx | 4 +-- .../cases/public/components/create/form.tsx | 2 +- .../public/components/create/form_context.tsx | 2 +- .../cases/public/components/create/index.tsx | 3 +- .../components/create/owner_selector.test.tsx | 4 +-- .../components/create/owner_selector.tsx | 8 +++-- .../cases/public/components/create/schema.tsx | 5 +-- .../components/create/severity.test.tsx | 4 +-- .../public/components/create/severity.tsx | 6 +++- .../components/create/submit_button.test.tsx | 2 +- .../components/create/submit_button.tsx | 2 +- .../create/sync_alerts_toggle.test.tsx | 4 +-- .../components/create/sync_alerts_toggle.tsx | 3 +- .../public/components/create/tags.test.tsx | 4 +-- .../cases/public/components/create/tags.tsx | 3 +- .../public/components/create/title.test.tsx | 4 +-- .../cases/public/components/create/title.tsx | 4 +-- .../components/edit_connector/index.tsx | 4 +-- .../components/edit_connector/schema.tsx | 4 +-- .../components/insert_timeline/index.test.tsx | 4 +-- .../components/insert_timeline/index.tsx | 2 +- .../components/markdown_editor/eui_form.tsx | 4 +-- .../components/user_actions/markdown_form.tsx | 2 +- .../public/components/user_actions/schema.ts | 5 +-- .../plugins/cases/public/components/utils.ts | 5 ++- 39 files changed, 95 insertions(+), 98 deletions(-) delete mode 100644 x-pack/plugins/cases/public/common/shared_imports.ts diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 99ed0c274199c..b8c1b33507c3e 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -17,11 +17,11 @@ import type { RenderOptions, RenderResult } from '@testing-library/react'; import { render as reactRender } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { ILicense } from '@kbn/licensing-plugin/public'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import type { CasesFeatures, CasesPermissions } from '../../../common/ui/types'; import { CasesProvider } from '../../components/cases_context'; import { createStartServicesMock } from '../lib/kibana/kibana_react.mock'; -import type { FieldHook } from '../shared_imports'; import type { StartServices } from '../../types'; import type { ReleasePhase } from '../../components/types'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; diff --git a/x-pack/plugins/cases/public/common/shared_imports.ts b/x-pack/plugins/cases/public/common/shared_imports.ts deleted file mode 100644 index 84c34a6186fa8..0000000000000 --- a/x-pack/plugins/cases/public/common/shared_imports.ts +++ /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. - */ - -export type { - FieldHook, - FieldValidateResponse, - FormData, - FormHook, - FormSchema, - ValidationError, - ValidationFunc, - FieldConfig, - ValidationConfig, -} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -export { - getUseField, - getFieldValidityAndErrorMessage, - FIELD_TYPES, - Form, - FormDataProvider, - UseField, - UseMultiFields, - useForm, - useFormContext, - useFormData, - VALIDATION_TYPES, -} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -export { Field, SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components'; -export { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -export type { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 84715324bee92..dc5c1c48cfd9b 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -17,12 +17,17 @@ import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elasti import styled from 'styled-components'; import { isEmpty } from 'lodash'; +import { + Form, + useForm, + UseField, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CommentType } from '../../../common/api'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; -import { Form, useForm, UseField, useFormData } from '../../common/shared_imports'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/add_comment/schema.tsx b/x-pack/plugins/cases/public/components/add_comment/schema.tsx index 7f8da37aeedc6..5df5769ef62ab 100644 --- a/x-pack/plugins/cases/public/components/add_comment/schema.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/schema.tsx @@ -5,9 +5,10 @@ * 2.0. */ +import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import type { CommentRequestUserType } from '../../../common/api'; -import type { FormSchema } from '../../common/shared_imports'; -import { FIELD_TYPES, fieldValidators } from '../../common/shared_imports'; import * as i18n from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx index 3a3a01cc49e94..2f8567d14f08d 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx @@ -18,15 +18,15 @@ import { } from '@elastic/eui'; import styled, { css } from 'styled-components'; import { isEqual } from 'lodash/fp'; -import * as i18n from '../../tags/translations'; -import type { FormSchema } from '../../../common/shared_imports'; +import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Form, FormDataProvider, useForm, getUseField, - Field, -} from '../../../common/shared_imports'; +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import * as i18n from '../../tags/translations'; import { useGetTags } from '../../../containers/use_get_tags'; import { Tags } from '../../tags/tags'; import { useCasesContext } from '../../cases_context/use_cases_context'; diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx index 139d8c48692a0..8729c13be1487 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { mount } from 'enzyme'; -import type { FormHook } from '../../common/shared_imports'; -import { UseField, Form, useForm } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { UseField, Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { ConnectorSelector } from './form'; import { connectorsMock } from '../../containers/mock'; import { getFormMock } from '../__mock__/form'; diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index 6f9f416e69037..b588182050d6a 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -10,8 +10,8 @@ import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import type { FieldHook } from '../../common/shared_imports'; -import { getFieldValidityAndErrorMessage } from '../../common/shared_imports'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; import type { ActionConnector } from '../../../common/api'; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 8480350841b0e..eddac63223de8 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { ValidationConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { SwimlaneConnectorType } from '../../../../common/api'; -import type { ValidationConfig } from '../../../common/shared_imports'; import type { CaseActionConnector } from '../../types'; const casesRequiredFields = [ diff --git a/x-pack/plugins/cases/public/components/create/assignees.test.tsx b/x-pack/plugins/cases/public/components/create/assignees.test.tsx index 97ee74ad76831..9a32ab5f1ffd9 100644 --- a/x-pack/plugins/cases/public/components/create/assignees.test.tsx +++ b/x-pack/plugins/cases/public/components/create/assignees.test.tsx @@ -10,8 +10,8 @@ import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { userProfiles } from '../../containers/user_profiles/api.mock'; import { Assignees } from './assignees'; import type { FormProps } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/assignees.tsx b/x-pack/plugins/cases/public/components/create/assignees.tsx index 1b5577bd87469..6066957722358 100644 --- a/x-pack/plugins/cases/public/components/create/assignees.tsx +++ b/x-pack/plugins/cases/public/components/create/assignees.tsx @@ -17,10 +17,13 @@ import { } from '@elastic/eui'; import type { UserProfileWithAvatar, UserProfile } from '@kbn/user-profile-components'; import { UserAvatar, getUserDisplayName } from '@kbn/user-profile-components'; +import type { FieldConfig, FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + UseField, + getFieldValidityAndErrorMessage, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { MAX_ASSIGNEES_PER_CASE } from '../../../common/constants'; import type { CaseAssignees } from '../../../common/api'; -import type { FieldConfig, FieldHook } from '../../common/shared_imports'; -import { UseField, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx index 1fc99cf30ab75..037293802201f 100644 --- a/x-pack/plugins/cases/public/components/create/connector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -11,8 +11,8 @@ import { act, waitFor } from '@testing-library/react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { connectorsMock } from '../../containers/mock'; import { Connector } from './connector'; import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 102b8ec73d1b0..0431d31ced1e0 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -8,9 +8,13 @@ import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import type { FieldHook, FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + UseField, + useFormData, + useFormContext, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { ActionConnector } from '../../../common/api'; -import type { FieldHook, FieldConfig } from '../../common/shared_imports'; -import { UseField, useFormData, useFormContext } from '../../common/shared_imports'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; import type { FormProps } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx index d95fade009f3d..3e9adb8961cc2 100644 --- a/x-pack/plugins/cases/public/components/create/description.test.tsx +++ b/x-pack/plugins/cases/public/components/create/description.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import userEvent, { specialChars } from '@testing-library/user-event'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Description } from './description'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index 6c9792684a126..437ff5c3751c1 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -6,8 +6,12 @@ */ import React, { memo, useEffect, useRef } from 'react'; +import { + UseField, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { MarkdownEditorForm } from '../markdown_editor'; -import { UseField, useFormContext, useFormData } from '../../common/shared_imports'; import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; import { ID as LensPluginId } from '../markdown_editor/plugins/lens/constants'; diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 3316134f7762e..ddc65f443bdb3 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -11,8 +11,8 @@ import { act, render } from '@testing-library/react'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { NONE_CONNECTOR_ID } from '../../../common/api'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { connectorsMock } from '../../containers/mock'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 32fff2048685d..e1a0c4f3b1cea 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import styled, { css } from 'styled-components'; -import { useFormContext } from '../../common/shared_imports'; +import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Title } from './title'; import { Description, fieldName as descriptionFieldName } from './description'; diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 1e6cfaafe4eeb..fdce32c38c880 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -6,9 +6,9 @@ */ import React, { useCallback, useMemo } from 'react'; +import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { FormProps } from './schema'; import { schema } from './schema'; -import { Form, useForm } from '../../common/shared_imports'; import { getNoneConnector, normalizeActionConnector } from '../configure_cases/utils'; import { usePostCase } from '../../containers/use_post_case'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index a1ffd7b3ebd6b..386b64f04bd1c 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -7,7 +7,8 @@ import React from 'react'; -import { Field, getUseField } from '../../common/shared_imports'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import * as i18n from './translations'; import type { CreateCaseFormProps } from './form'; import { CreateCaseForm } from './form'; diff --git a/x-pack/plugins/cases/public/components/create/owner_selector.test.tsx b/x-pack/plugins/cases/public/components/create/owner_selector.test.tsx index 25f0f9ba0faaa..cd9515edbd28c 100644 --- a/x-pack/plugins/cases/public/components/create/owner_selector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/owner_selector.test.tsx @@ -11,8 +11,8 @@ import { act, waitFor } from '@testing-library/react'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { OBSERVABILITY_OWNER } from '../../../common/constants'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CreateCaseOwnerSelector } from './owner_selector'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/owner_selector.tsx b/x-pack/plugins/cases/public/components/create/owner_selector.tsx index e572e7c9f9ca5..440fcffcbb5d8 100644 --- a/x-pack/plugins/cases/public/components/create/owner_selector.tsx +++ b/x-pack/plugins/cases/public/components/create/owner_selector.tsx @@ -16,13 +16,15 @@ import { EuiKeyPadMenuItem, useGeneratedHtmlId, } from '@elastic/eui'; - +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + getFieldValidityAndErrorMessage, + UseField, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { OWNER_INFO } from '../../../common/constants'; -import type { FieldHook } from '../../common/shared_imports'; -import { getFieldValidityAndErrorMessage, UseField } from '../../common/shared_imports'; import * as i18n from './translations'; interface OwnerSelectorProps { diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index 30dc762143ae3..f80c4e52945bb 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -5,11 +5,12 @@ * 2.0. */ +import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FIELD_TYPES, VALIDATION_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import type { CasePostRequest, ConnectorTypeFields } from '../../../common/api'; import { isInvalidTag } from '../../../common/utils/validators'; import { MAX_TITLE_LENGTH } from '../../../common/constants'; -import type { FormSchema } from '../../common/shared_imports'; -import { FIELD_TYPES, fieldValidators, VALIDATION_TYPES } from '../../common/shared_imports'; import * as i18n from './translations'; import { OptionalFieldLabel } from './optional_field_label'; diff --git a/x-pack/plugins/cases/public/components/create/severity.test.tsx b/x-pack/plugins/cases/public/components/create/severity.test.tsx index 5d80028817a83..b8a5b446b89f8 100644 --- a/x-pack/plugins/cases/public/components/create/severity.test.tsx +++ b/x-pack/plugins/cases/public/components/create/severity.test.tsx @@ -9,8 +9,8 @@ import { CaseSeverity } from '../../../common/api'; import React from 'react'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; -import type { FormHook } from '../../common/shared_imports'; -import { Form, useForm } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Severity } from './severity'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/severity.tsx b/x-pack/plugins/cases/public/components/create/severity.tsx index 730eab5d77ac6..3e090272162e8 100644 --- a/x-pack/plugins/cases/public/components/create/severity.tsx +++ b/x-pack/plugins/cases/public/components/create/severity.tsx @@ -7,8 +7,12 @@ import { EuiFormRow } from '@elastic/eui'; import React, { memo } from 'react'; +import { + UseField, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CaseSeverity } from '../../../common/api'; -import { UseField, useFormContext, useFormData } from '../../common/shared_imports'; import { SeveritySelector } from '../severity/selector'; import { SEVERITY_TITLE } from '../severity/translations'; diff --git a/x-pack/plugins/cases/public/components/create/submit_button.test.tsx b/x-pack/plugins/cases/public/components/create/submit_button.test.tsx index b703eb703d720..95dc8d783ac3e 100644 --- a/x-pack/plugins/cases/public/components/create/submit_button.test.tsx +++ b/x-pack/plugins/cases/public/components/create/submit_button.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; -import { useForm, Form } from '../../common/shared_imports'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { SubmitCaseButton } from './submit_button'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/submit_button.tsx b/x-pack/plugins/cases/public/components/create/submit_button.tsx index 9f984b236ca69..2c3ecd563df73 100644 --- a/x-pack/plugins/cases/public/components/create/submit_button.tsx +++ b/x-pack/plugins/cases/public/components/create/submit_button.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { EuiButton } from '@elastic/eui'; -import { useFormContext } from '../../common/shared_imports'; +import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import * as i18n from './translations'; const SubmitCaseButtonComponent: React.FC = () => { diff --git a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx index 28abe40607c72..aa5d80e82a34f 100644 --- a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx +++ b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { SyncAlertsToggle } from './sync_alerts_toggle'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx index bed8e6d18f5e3..1a189de3e17ec 100644 --- a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx +++ b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx @@ -6,7 +6,8 @@ */ import React, { memo } from 'react'; -import { Field, getUseField, useFormData } from '../../common/shared_imports'; +import { getUseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import * as i18n from './translations'; const CommonUseField = getUseField({ component: Field }); diff --git a/x-pack/plugins/cases/public/components/create/tags.test.tsx b/x-pack/plugins/cases/public/components/create/tags.test.tsx index 9e84955c3a07e..653f32d09f600 100644 --- a/x-pack/plugins/cases/public/components/create/tags.test.tsx +++ b/x-pack/plugins/cases/public/components/create/tags.test.tsx @@ -11,8 +11,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Tags } from './tags'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/tags.tsx b/x-pack/plugins/cases/public/components/create/tags.tsx index 11dac39bba75b..6bae6015e769b 100644 --- a/x-pack/plugins/cases/public/components/create/tags.tsx +++ b/x-pack/plugins/cases/public/components/create/tags.tsx @@ -7,7 +7,8 @@ import React, { memo, useMemo } from 'react'; -import { Field, getUseField } from '../../common/shared_imports'; +import { getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { useGetTags } from '../../containers/use_get_tags'; const CommonUseField = getUseField({ component: Field }); diff --git a/x-pack/plugins/cases/public/components/create/title.test.tsx b/x-pack/plugins/cases/public/components/create/title.test.tsx index 37952ba530842..793a868f8a1cb 100644 --- a/x-pack/plugins/cases/public/components/create/title.test.tsx +++ b/x-pack/plugins/cases/public/components/create/title.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { act } from '@testing-library/react'; -import type { FormHook } from '../../common/shared_imports'; -import { useForm, Form } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Title } from './title'; import type { FormProps } from './schema'; import { schema } from './schema'; diff --git a/x-pack/plugins/cases/public/components/create/title.tsx b/x-pack/plugins/cases/public/components/create/title.tsx index ae8f517173132..35de4c7a41ccb 100644 --- a/x-pack/plugins/cases/public/components/create/title.tsx +++ b/x-pack/plugins/cases/public/components/create/title.tsx @@ -6,8 +6,8 @@ */ import React, { memo } from 'react'; -import { Field, getUseField } from '../../common/shared_imports'; - +import { getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; const CommonUseField = getUseField({ component: Field }); interface Props { diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 7be868fa5d625..7fa73395ba8a1 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -19,8 +19,8 @@ import { import styled from 'styled-components'; import { isEmpty, noop } from 'lodash/fp'; -import type { FieldConfig } from '../../common/shared_imports'; -import { Form, UseField, useForm } from '../../common/shared_imports'; +import type { FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Form, UseField, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { Case } from '../../../common/ui/types'; import type { ActionConnector, ConnectorTypeFields } from '../../../common/api'; import { NONE_CONNECTOR_ID } from '../../../common/api'; diff --git a/x-pack/plugins/cases/public/components/edit_connector/schema.tsx b/x-pack/plugins/cases/public/components/edit_connector/schema.tsx index 0ff191fff5bda..27eecc485ea42 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/schema.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/schema.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { FormSchema } from '../../common/shared_imports'; -import { FIELD_TYPES } from '../../common/shared_imports'; +import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; export interface FormProps { connectorId: string; diff --git a/x-pack/plugins/cases/public/components/insert_timeline/index.test.tsx b/x-pack/plugins/cases/public/components/insert_timeline/index.test.tsx index 931cf3255c5a5..d616ae71bd826 100644 --- a/x-pack/plugins/cases/public/components/insert_timeline/index.test.tsx +++ b/x-pack/plugins/cases/public/components/insert_timeline/index.test.tsx @@ -10,8 +10,8 @@ import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../common/mock'; -import type { FormHook } from '../../common/shared_imports'; -import { Form, useForm } from '../../common/shared_imports'; +import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Form, useForm } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { timelineIntegrationMock } from '../__mock__/timeline'; import { getFormMock } from '../__mock__/form'; diff --git a/x-pack/plugins/cases/public/components/insert_timeline/index.tsx b/x-pack/plugins/cases/public/components/insert_timeline/index.tsx index 62b8ad7155581..0a1e6c02b22e6 100644 --- a/x-pack/plugins/cases/public/components/insert_timeline/index.tsx +++ b/x-pack/plugins/cases/public/components/insert_timeline/index.tsx @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import { useFormContext } from '../../common/shared_imports'; +import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; type InsertFields = 'comment' | 'description'; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 2884816a091a3..ff073f7eef08a 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -9,8 +9,8 @@ import React, { forwardRef, useMemo } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import type { FieldHook } from '../../common/shared_imports'; -import { getFieldValidityAndErrorMessage } from '../../common/shared_imports'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { MarkdownEditorRef } from './editor'; import { MarkdownEditor } from './editor'; import { CommentEditorContext } from './context'; diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 784e163fa963d..b2b7443e001e8 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -9,8 +9,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/e import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; import styled from 'styled-components'; +import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import * as i18n from '../case_view/translations'; -import { Form, useForm, UseField } from '../../common/shared_imports'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; diff --git a/x-pack/plugins/cases/public/components/user_actions/schema.ts b/x-pack/plugins/cases/public/components/user_actions/schema.ts index ecc1f981829b0..8c47b700adeb5 100644 --- a/x-pack/plugins/cases/public/components/user_actions/schema.ts +++ b/x-pack/plugins/cases/public/components/user_actions/schema.ts @@ -5,8 +5,9 @@ * 2.0. */ -import type { FormSchema } from '../../common/shared_imports'; -import { FIELD_TYPES, fieldValidators } from '../../common/shared_imports'; +import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import * as i18n from '../../common/translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 45b2247b7bcac..1a5f8134563ff 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -6,8 +6,11 @@ */ import type { IconType } from '@elastic/eui'; +import type { + FieldConfig, + ValidationConfig, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { ConnectorTypes } from '../../common/api'; -import type { FieldConfig, ValidationConfig } from '../common/shared_imports'; import type { CasesPluginStart } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; import type { CaseActionConnector } from './types'; From 963ec08042b70bfe92b2c7ae7d619a598c687ae4 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 17 Oct 2022 16:41:25 +0300 Subject: [PATCH 03/74] [TSVB] Wait before setting another terms field (#143373) --- test/functional/apps/visualize/group5/_tsvb_time_series.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/group5/_tsvb_time_series.ts b/test/functional/apps/visualize/group5/_tsvb_time_series.ts index f9e016ff1f635..fb59d947b6790 100644 --- a/test/functional/apps/visualize/group5/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/group5/_tsvb_time_series.ts @@ -11,11 +11,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualize, visualBuilder, timeToVisualize, dashboard, common } = getPageObjects([ + const { visualize, visualBuilder, timeToVisualize, dashboard, header, common } = getPageObjects([ 'visualBuilder', 'visualize', 'timeToVisualize', 'dashboard', + 'header', 'common', ]); const security = getService('security'); @@ -221,6 +222,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should create a filter for series with multiple split by terms fields one of which has formatting', async () => { const expectedFilterPills = ['0, win 7']; await visualBuilder.setMetricsGroupByTerms('bytes'); + await header.waitUntilLoadingHasFinished(); await visualBuilder.setAnotherGroupByTermsField('machine.os.raw'); await visualBuilder.clickSeriesOption(); await visualBuilder.setChartType('Bar'); From 4566b88726d80e6853c663d6d7656010aee27a76 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 17 Oct 2022 16:41:43 +0300 Subject: [PATCH 04/74] [Lens] fixes the value count label (#143342) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../datasources/form_based/operations/definitions/count.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx index c05b8d415de7e..0d292b7a3a26e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx @@ -125,7 +125,6 @@ export const countOperation: OperationDefinition Date: Mon, 17 Oct 2022 16:25:23 +0200 Subject: [PATCH 05/74] [Security Solution][Response Console] Temporary solution for enabling Response Console without 'get_file' support on the endpoint (#143382) * [Security Solution][Response Console] Temporary solution for enabling Response Console without 'get_file' support on the endpoint * bypass 'get_file' instead of removing it Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../endpoint/use_does_endpoint_support_responder.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_does_endpoint_support_responder.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_does_endpoint_support_responder.ts index f98020fe62d7f..27158cbfad831 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_does_endpoint_support_responder.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_does_endpoint_support_responder.ts @@ -12,9 +12,12 @@ export const useDoesEndpointSupportResponder = ( endpointMetadata: MaybeImmutable | undefined ): boolean => { if (endpointMetadata) { - return ENDPOINT_CAPABILITIES.every((capability) => - endpointMetadata?.Endpoint.capabilities?.includes(capability) - ); + return ENDPOINT_CAPABILITIES.every((capability) => { + // TODO: remove this temporary bypass when in-context Response Console capabilities are enabled + const temporaryBypass = capability === 'get_file'; + + return endpointMetadata?.Endpoint.capabilities?.includes(capability) || temporaryBypass; + }); } return false; }; From 247adaf411e535f2c6750d315b4c1845c48b4d0e Mon Sep 17 00:00:00 2001 From: Adam Demjen Date: Mon, 17 Oct 2022 10:35:15 -0400 Subject: [PATCH 06/74] [8.6] Filter trained ML models in current Kibana space (#143227) * Redact model ID in trained models response if model is not in current space --- .../common/types/pipelines.ts | 1 + x-pack/plugins/enterprise_search/kibana.json | 2 +- .../inference_pipeline_card.test.tsx | 1 + .../pipelines/ml_model_health.test.tsx | 1 + .../__mocks__/routerDependencies.mock.ts | 5 + ...h_ml_inference_pipeline_processors.test.ts | 140 +++++++++++++++++- .../fetch_ml_inference_pipeline_processors.ts | 34 ++++- .../enterprise_search/server/plugin.ts | 9 +- .../routes/enterprise_search/indices.test.ts | 31 +++- .../routes/enterprise_search/indices.ts | 10 +- 10 files changed, 215 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/types/pipelines.ts b/x-pack/plugins/enterprise_search/common/types/pipelines.ts index d6286718b454f..b103b4a5265b3 100644 --- a/x-pack/plugins/enterprise_search/common/types/pipelines.ts +++ b/x-pack/plugins/enterprise_search/common/types/pipelines.ts @@ -8,6 +8,7 @@ import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; export interface InferencePipeline { + modelId: string | undefined; modelState: TrainedModelState; modelStateReason?: string; pipelineName: string; diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index 7cd1e2e71ee7d..4c99ca1df9e0f 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "requiredPlugins": ["features", "spaces", "security", "licensing", "data", "charts", "infra", "cloud", "esUiShared"], "configPath": ["enterpriseSearch"], - "optionalPlugins": ["usageCollection", "home", "customIntegrations"], + "optionalPlugins": ["usageCollection", "home", "customIntegrations", "ml"], "server": true, "ui": true, "requiredBundles": ["kibanaReact", "cloudChat"], diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx index 79f344e5119e4..0b927e4d01f4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx @@ -19,6 +19,7 @@ import { InferencePipelineCard } from './inference_pipeline_card'; import { TrainedModelHealth } from './ml_model_health'; export const DEFAULT_VALUES: InferencePipeline = { + modelId: 'sample-bert-ner-model', modelState: TrainedModelState.Started, pipelineName: 'Sample Processor', types: ['pytorch', 'ner'], diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx index 0eb88abb317e5..f6062fda4add9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx @@ -24,6 +24,7 @@ describe('TrainedModelHealth', () => { }); const commonModelData: InferencePipeline = { + modelId: 'sample-bert-ner-model', modelState: TrainedModelState.NotDeployed, pipelineName: 'Sample Processor', types: ['pytorch'], diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts index af11762f117d1..1f4c66e83234a 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts @@ -18,6 +18,10 @@ export const mockRequestHandler = { }, }; +export const mockMl = { + trainedModelsProvider: jest.fn(), +}; + export const mockConfig = { host: 'http://localhost:3002', accessCheckTimeout: 5000, @@ -34,4 +38,5 @@ export const mockDependencies = { config: mockConfig, log: mockLogger, enterpriseSearchRequestHandler: mockRequestHandler as any, + ml: mockMl as any, }; diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts index bb3324cf641d6..bc77b2dff7827 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts @@ -6,6 +6,7 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; +import { MlTrainedModels } from '@kbn/ml-plugin/server'; import { InferencePipeline, TrainedModelState } from '../../../common/types/pipelines'; @@ -152,7 +153,7 @@ const mockGetPipeline3 = { }; const mockGetTrainedModelsData = { - count: 1, + count: 5, trained_model_configs: [ { inference_config: { ner: {} }, @@ -172,6 +173,12 @@ const mockGetTrainedModelsData = { model_type: 'pytorch', tags: [], }, + { + inference_config: { ner: {} }, + model_id: 'trained-model-id-3-in-other-space', // Not in current Kibana space, will be filtered + model_type: 'pytorch', + tags: [], + }, { inference_config: { fill_mask: {} }, model_id: 'trained-model-id-4', @@ -206,6 +213,15 @@ const mockGetTrainedModelStats = { }, model_id: 'trained-model-id-3', }, + { + deployment_stats: { + allocation_status: { + allocation_count: 1, + }, + state: 'started', + }, + model_id: 'trained-model-id-3-in-other-space', + }, { deployment_stats: { allocation_status: { @@ -218,18 +234,29 @@ const mockGetTrainedModelStats = { ], }; +const mockTrainedModelsInCurrentSpace = { + ...mockGetTrainedModelsData, + trained_model_configs: [ + ...mockGetTrainedModelsData.trained_model_configs.slice(0, 3), // Remove 4th element + mockGetTrainedModelsData.trained_model_configs[4], + ], +}; + const trainedModelDataObject: Record = { 'trained-model-id-1': { + modelId: 'trained-model-id-1', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', types: ['lang_ident', 'ner'], }, 'trained-model-id-2': { + modelId: 'trained-model-id-2', modelState: TrainedModelState.Started, pipelineName: 'ml-inference-pipeline-2', types: ['pytorch', 'ner'], }, 'ml-inference-pipeline-3': { + modelId: 'trained-model-id-1', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-3', types: ['lang_ident', 'ner'], @@ -292,12 +319,14 @@ describe('fetchPipelineProcessorInferenceData lib function', () => { const expected: InferencePipelineData[] = [ { + modelId: 'trained-model-id-1', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', trainedModelName: 'trained-model-id-1', types: [], }, { + modelId: 'trained-model-id-2', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-2', trainedModelName: 'trained-model-id-2', @@ -324,27 +353,35 @@ describe('getMlModelConfigsForModelIds lib function', () => { getTrainedModelsStats: jest.fn(), }, }; + const mockTrainedModelsProvider = { + getTrainedModels: jest.fn(), + }; beforeEach(() => { jest.clearAllMocks(); }); - it('should fetch the models that we ask for', async () => { - mockClient.ml.getTrainedModels.mockImplementation(() => - Promise.resolve(mockGetTrainedModelsData) - ); - mockClient.ml.getTrainedModelsStats.mockImplementation(() => - Promise.resolve(mockGetTrainedModelStats) - ); + mockClient.ml.getTrainedModels.mockImplementation(() => + Promise.resolve(mockGetTrainedModelsData) + ); + mockClient.ml.getTrainedModelsStats.mockImplementation(() => + Promise.resolve(mockGetTrainedModelStats) + ); + mockTrainedModelsProvider.getTrainedModels.mockImplementation(() => + Promise.resolve(mockTrainedModelsInCurrentSpace) + ); + it('should fetch the models that we ask for', async () => { const input: Record = { 'trained-model-id-1': { + modelId: 'trained-model-id-1', modelState: TrainedModelState.Started, pipelineName: '', trainedModelName: 'trained-model-id-1', types: ['pytorch', 'ner'], }, 'trained-model-id-2': { + modelId: 'trained-model-id-2', modelState: TrainedModelState.Started, pipelineName: '', trainedModelName: 'trained-model-id-2', @@ -357,6 +394,7 @@ describe('getMlModelConfigsForModelIds lib function', () => { }; const response = await getMlModelConfigsForModelIds( mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, ['trained-model-id-2'] ); expect(mockClient.ml.getTrainedModels).toHaveBeenCalledWith({ @@ -365,6 +403,51 @@ describe('getMlModelConfigsForModelIds lib function', () => { expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ model_id: 'trained-model-id-2', }); + expect(mockTrainedModelsProvider.getTrainedModels).toHaveBeenCalled(); + expect(response).toEqual(expected); + }); + + it('should redact model IDs not in the current space', async () => { + const input: Record = { + 'trained-model-id-1': { + modelId: 'trained-model-id-1', + modelState: TrainedModelState.Started, + pipelineName: '', + trainedModelName: 'trained-model-id-1', + types: ['pytorch', 'ner'], + }, + 'trained-model-id-2': { + modelId: 'trained-model-id-2', + modelState: TrainedModelState.Started, + pipelineName: '', + trainedModelName: 'trained-model-id-2', + types: ['pytorch', 'ner'], + }, + 'trained-model-id-3-in-other-space': { + modelId: undefined, // Redacted + modelState: TrainedModelState.Started, + pipelineName: '', + trainedModelName: 'trained-model-id-3-in-other-space', + types: ['pytorch', 'ner'], + }, + }; + + const expected = { + 'trained-model-id-2': input['trained-model-id-2'], + 'trained-model-id-3-in-other-space': input['trained-model-id-3-in-other-space'], + }; + const response = await getMlModelConfigsForModelIds( + mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, + ['trained-model-id-2', 'trained-model-id-3-in-other-space'] + ); + expect(mockClient.ml.getTrainedModels).toHaveBeenCalledWith({ + model_id: 'trained-model-id-2,trained-model-id-3-in-other-space', + }); + expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ + model_id: 'trained-model-id-2,trained-model-id-3-in-other-space', + }); + expect(mockTrainedModelsProvider.getTrainedModels).toHaveBeenCalled(); expect(response).toEqual(expected); }); }); @@ -376,6 +459,9 @@ describe('fetchAndAddTrainedModelData lib function', () => { getTrainedModelsStats: jest.fn(), }, }; + const mockTrainedModelsProvider = { + getTrainedModels: jest.fn(), + }; beforeEach(() => { jest.clearAllMocks(); @@ -388,27 +474,34 @@ describe('fetchAndAddTrainedModelData lib function', () => { mockClient.ml.getTrainedModelsStats.mockImplementation(() => Promise.resolve(mockGetTrainedModelStats) ); + mockTrainedModelsProvider.getTrainedModels.mockImplementation(() => + Promise.resolve(mockTrainedModelsInCurrentSpace) + ); const pipelines: InferencePipelineData[] = [ { + modelId: 'trained-model-id-1', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', trainedModelName: 'trained-model-id-1', types: [], }, { + modelId: 'trained-model-id-2', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-2', trainedModelName: 'trained-model-id-2', types: [], }, { + modelId: 'trained-model-id-3', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-3', trainedModelName: 'trained-model-id-3', types: [], }, { + modelId: 'trained-model-id-4', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-4', trainedModelName: 'trained-model-id-4', @@ -418,18 +511,21 @@ describe('fetchAndAddTrainedModelData lib function', () => { const expected: InferencePipelineData[] = [ { + modelId: 'trained-model-id-1', modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', trainedModelName: 'trained-model-id-1', types: ['lang_ident', 'ner'], }, { + modelId: 'trained-model-id-2', modelState: TrainedModelState.Started, pipelineName: 'ml-inference-pipeline-2', trainedModelName: 'trained-model-id-2', types: ['pytorch', 'ner'], }, { + modelId: 'trained-model-id-3', modelState: TrainedModelState.Failed, modelStateReason: 'something is wrong, boom', pipelineName: 'ml-inference-pipeline-3', @@ -437,6 +533,7 @@ describe('fetchAndAddTrainedModelData lib function', () => { types: ['pytorch', 'text_classification'], }, { + modelId: 'trained-model-id-4', modelState: TrainedModelState.Starting, pipelineName: 'ml-inference-pipeline-4', trainedModelName: 'trained-model-id-4', @@ -446,6 +543,7 @@ describe('fetchAndAddTrainedModelData lib function', () => { const response = await fetchAndAddTrainedModelData( mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, pipelines ); @@ -455,6 +553,7 @@ describe('fetchAndAddTrainedModelData lib function', () => { expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ model_id: 'trained-model-id-1,trained-model-id-2,trained-model-id-3,trained-model-id-4', }); + expect(mockTrainedModelsProvider.getTrainedModels).toHaveBeenCalled(); expect(response).toEqual(expected); }); }); @@ -469,17 +568,33 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { getTrainedModelsStats: jest.fn(), }, }; + const mockTrainedModelsProvider = { + getTrainedModels: jest.fn(), + }; beforeEach(() => { jest.clearAllMocks(); }); + describe('when Machine Learning is disabled in the current space', () => { + it('should throw an eror', () => { + expect(() => + fetchMlInferencePipelineProcessors( + mockClient as unknown as ElasticsearchClient, + undefined, + 'some-index' + ) + ).rejects.toThrowError('Machine Learning is not enabled'); + }); + }); + describe('when using an index that does not have an ml-inference pipeline', () => { it('should return an empty array', async () => { mockClient.ingest.getPipeline.mockImplementation(() => Promise.reject({})); const response = await fetchMlInferencePipelineProcessors( mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, 'index-with-no-ml-inference-pipeline' ); @@ -489,6 +604,7 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { expect(mockClient.ingest.getPipeline).toHaveBeenCalledTimes(1); expect(mockClient.ml.getTrainedModels).toHaveBeenCalledTimes(0); expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledTimes(0); + expect(mockTrainedModelsProvider.getTrainedModels).toHaveBeenCalledTimes(0); expect(response).toEqual([]); }); @@ -505,6 +621,7 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { const response = await fetchMlInferencePipelineProcessors( mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, 'my-index' ); @@ -533,11 +650,15 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { mockClient.ml.getTrainedModelsStats.mockImplementation(() => Promise.resolve(mockGetTrainedModelStats) ); + mockTrainedModelsProvider.getTrainedModels.mockImplementation(() => + Promise.resolve(mockTrainedModelsInCurrentSpace) + ); const expected = [trainedModelDataObject['trained-model-id-1']] as InferencePipeline[]; const response = await fetchMlInferencePipelineProcessors( mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, 'my-index' ); @@ -553,6 +674,7 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ model_id: 'trained-model-id-1', }); + expect(mockTrainedModelsProvider.getTrainedModels).toHaveBeenCalled(); expect(response).toEqual(expected); }); @@ -580,6 +702,7 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { const response = await fetchMlInferencePipelineProcessors( mockClient as unknown as ElasticsearchClient, + mockTrainedModelsProvider as unknown as MlTrainedModels, 'my-index' ); @@ -595,6 +718,7 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ model_id: 'trained-model-id-1', }); + expect(mockTrainedModelsProvider.getTrainedModels).toHaveBeenCalled(); expect(response).toEqual(expected); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts index 72867ad717065..e5843be2a6d7d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts @@ -6,6 +6,7 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; +import { MlTrainedModels } from '@kbn/ml-plugin/server'; import { getMlModelTypesForModelConfig } from '../../../common/ml_inference_pipeline'; import { InferencePipeline, TrainedModelState } from '../../../common/types/pipelines'; @@ -57,6 +58,7 @@ export const fetchPipelineProcessorInferenceData = async ( const trainedModelName = inferenceProcessor?.inference?.model_id; if (trainedModelName) pipelineProcessorData.push({ + modelId: trainedModelName, modelState: TrainedModelState.NotDeployed, pipelineName: pipelineProcessorName, trainedModelName, @@ -71,20 +73,27 @@ export const fetchPipelineProcessorInferenceData = async ( export const getMlModelConfigsForModelIds = async ( client: ElasticsearchClient, + trainedModelsProvider: MlTrainedModels, trainedModelNames: string[] ): Promise> => { - const [trainedModels, trainedModelsStats] = await Promise.all([ + const [trainedModels, trainedModelsStats, trainedModelsInCurrentSpace] = await Promise.all([ client.ml.getTrainedModels({ model_id: trainedModelNames.join() }), client.ml.getTrainedModelsStats({ model_id: trainedModelNames.join() }), + trainedModelsProvider.getTrainedModels({}), // Get all models from current space; note we can't + // use exact model name matching, that returns an + // error for models that cannot be found ]); + const modelNamesInCurrentSpace = trainedModelsInCurrentSpace.trained_model_configs.map( + (modelConfig) => modelConfig.model_id + ); const modelConfigs: Record = {}; - trainedModels.trained_model_configs.forEach((trainedModelData) => { const trainedModelName = trainedModelData.model_id; if (trainedModelNames.includes(trainedModelName)) { modelConfigs[trainedModelName] = { + modelId: modelNamesInCurrentSpace.includes(trainedModelName) ? trainedModelName : undefined, modelState: TrainedModelState.NotDeployed, pipelineName: '', trainedModelName, @@ -125,21 +134,27 @@ export const getMlModelConfigsForModelIds = async ( export const fetchAndAddTrainedModelData = async ( client: ElasticsearchClient, + trainedModelsProvider: MlTrainedModels, pipelineProcessorData: InferencePipelineData[] ): Promise => { const trainedModelNames = Array.from( new Set(pipelineProcessorData.map((pipeline) => pipeline.trainedModelName)) ); - const modelConfigs = await getMlModelConfigsForModelIds(client, trainedModelNames); + const modelConfigs = await getMlModelConfigsForModelIds( + client, + trainedModelsProvider, + trainedModelNames + ); return pipelineProcessorData.map((data) => { const model = modelConfigs[data.trainedModelName]; if (!model) { return data; } - const { types, modelState, modelStateReason } = model; + const { modelId, types, modelState, modelStateReason } = model; return { ...data, + modelId, types, modelState, modelStateReason, @@ -149,8 +164,13 @@ export const fetchAndAddTrainedModelData = async ( export const fetchMlInferencePipelineProcessors = async ( client: ElasticsearchClient, + trainedModelsProvider: MlTrainedModels | undefined, indexName: string ): Promise => { + if (!trainedModelsProvider) { + return Promise.reject(new Error('Machine Learning is not enabled')); + } + const mlInferencePipelineProcessorNames = await fetchMlInferencePipelineProcessorNames( client, indexName @@ -171,7 +191,11 @@ export const fetchMlInferencePipelineProcessors = async ( // inference processors, return early to avoid fetching all of the possible trained model data. if (pipelineProcessorInferenceData.length === 0) return [] as InferencePipeline[]; - const pipelines = await fetchAndAddTrainedModelData(client, pipelineProcessorInferenceData); + const pipelines = await fetchAndAddTrainedModelData( + client, + trainedModelsProvider, + pipelineProcessorInferenceData + ); // Due to restrictions with Kibana spaces we do not want to return the trained model name // to the UI. So we remove it from the data structure here. diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 7113f09b7ffe6..90620af30f6b8 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -18,6 +18,7 @@ import { import { CustomIntegrationsPluginSetup } from '@kbn/custom-integrations-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { InfraPluginSetup } from '@kbn/infra-plugin/server'; +import type { MlPluginSetup } from '@kbn/ml-plugin/server'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; @@ -70,6 +71,7 @@ interface PluginsSetup { features: FeaturesPluginSetup; infra: InfraPluginSetup; customIntegrations?: CustomIntegrationsPluginSetup; + ml?: MlPluginSetup; } interface PluginsStart { @@ -83,6 +85,7 @@ export interface RouteDependencies { log: Logger; enterpriseSearchRequestHandler: IEnterpriseSearchRequestHandler; getSavedObjectsService?(): SavedObjectsServiceStart; + ml?: MlPluginSetup; } export class EnterpriseSearchPlugin implements Plugin { @@ -96,7 +99,7 @@ export class EnterpriseSearchPlugin implements Plugin { public setup( { capabilities, http, savedObjects, getStartServices, uiSettings }: CoreSetup, - { usageCollection, security, features, infra, customIntegrations }: PluginsSetup + { usageCollection, security, features, infra, customIntegrations, ml }: PluginsSetup ) { const config = this.config; const log = this.logger; @@ -142,7 +145,7 @@ export class EnterpriseSearchPlugin implements Plugin { capabilities.registerSwitcher(async (request: KibanaRequest) => { const [, { spaces }] = await getStartServices(); - const dependencies = { config, security, spaces, request, log }; + const dependencies = { config, security, spaces, request, log, ml }; const { hasAppSearchAccess, hasWorkplaceSearchAccess } = await checkAccess(dependencies); const showEnterpriseSearch = hasAppSearchAccess || hasWorkplaceSearchAccess; @@ -172,7 +175,7 @@ export class EnterpriseSearchPlugin implements Plugin { */ const router = http.createRouter(); const enterpriseSearchRequestHandler = new EnterpriseSearchRequestHandler({ config, log }); - const dependencies = { router, config, log, enterpriseSearchRequestHandler }; + const dependencies = { router, config, log, enterpriseSearchRequestHandler, ml }; registerConfigDataRoute(dependencies); registerAppSearchRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts index a55b67b798305..6732867af59b4 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts @@ -8,6 +8,9 @@ import { MockRouter, mockDependencies } from '../../__mocks__'; import { RequestHandlerContext } from '@kbn/core/server'; +import { MlTrainedModels } from '@kbn/ml-plugin/server'; + +import { SharedServices } from '@kbn/ml-plugin/server/shared_services'; import { ErrorCode } from '../../../common/types/error_codes'; @@ -51,9 +54,9 @@ describe('Enterprise Search Managed Indices', () => { search: jest.fn(), }, }; - const mockCore = { elasticsearch: { client: mockClient }, + savedObjects: { client: {} }, }; describe('GET /internal/enterprise_search/indices/{indexName}/ml_inference/errors', () => { @@ -115,6 +118,9 @@ describe('Enterprise Search Managed Indices', () => { }); describe('GET /internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors', () => { + let mockMl: SharedServices; + let mockTrainedModelsProvider: MlTrainedModels; + beforeEach(() => { const context = { core: Promise.resolve(mockCore), @@ -126,9 +132,19 @@ describe('Enterprise Search Managed Indices', () => { path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors', }); + mockTrainedModelsProvider = { + getTrainedModels: jest.fn(), + getTrainedModelsStats: jest.fn(), + } as MlTrainedModels; + + mockMl = { + trainedModelsProvider: () => Promise.resolve(mockTrainedModelsProvider), + } as unknown as jest.Mocked; + registerIndexRoutes({ ...mockDependencies, router: mockRouter.router, + ml: mockMl, }); }); @@ -157,6 +173,7 @@ describe('Enterprise Search Managed Indices', () => { expect(fetchMlInferencePipelineProcessors).toHaveBeenCalledWith( mockClient.asCurrentUser, + mockTrainedModelsProvider, 'search-index-name' ); @@ -165,6 +182,18 @@ describe('Enterprise Search Managed Indices', () => { headers: { 'content-type': 'application/json' }, }); }); + + it('returns a generic error if an error is thrown from the called service', async () => { + (fetchMlInferencePipelineProcessors as jest.Mock).mockImplementationOnce(() => { + return Promise.reject(new Error('something went wrong')); + }); + + await mockRouter.callRoute({ + params: { indexName: 'search-index-name' }, + }); + + expect(mockRouter.response.customError).toHaveBeenCalledTimes(1); + }); }); describe('POST /internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index 4ae42edf16ae7..4e0b0706d09de 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -50,6 +50,7 @@ export function registerIndexRoutes({ router, enterpriseSearchRequestHandler, log, + ml, }: RouteDependencies) { router.get( { path: '/internal/enterprise_search/search_indices', validate: false }, @@ -323,10 +324,17 @@ export function registerIndexRoutes({ }, elasticsearchErrorHandler(log, async (context, request, response) => { const indexName = decodeURIComponent(request.params.indexName); - const { client } = (await context.core).elasticsearch; + const { + elasticsearch: { client }, + savedObjects: { client: savedObjectsClient }, + } = await context.core; + const trainedModelsProvider = ml + ? await ml.trainedModelsProvider(request, savedObjectsClient) + : undefined; const mlInferencePipelineProcessorConfigs = await fetchMlInferencePipelineProcessors( client.asCurrentUser, + trainedModelsProvider, indexName ); From f2e630c308c3b390395c7ef97980823dd01417aa Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Mon, 17 Oct 2022 17:22:44 +0200 Subject: [PATCH 07/74] Unskip telemetry and add enrichment tests (#143424) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../usage_collector/detection_rule_status.ts | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts index 3f9394f2e2b5a..dea6703d2fef4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext) => { // Note: We don't actually find signals well with ML tests at the moment so there are not tests for ML rule type for telemetry // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/132856 - describe.skip('Detection rule status telemetry', async () => { + describe('Detection rule status telemetry', async () => { before(async () => { // Just in case other tests do not clean up the event logs, let us clear them now and here only once. await deleteAllEventLogExecutionEvents(es, log); @@ -209,7 +209,7 @@ export default ({ getService }: FtrProviderContext) => { expect(stats?.detection_rules.detection_rule_status.custom_rules.query.succeeded).to.eql(1); }); - it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + it('should have non zero values for "succeeded", "index_duration", "search_duration" and "enrichment_duration"', () => { expect( stats?.detection_rules.detection_rule_status.custom_rules.query.index_duration.max ).to.be.above(1); @@ -228,6 +228,15 @@ export default ({ getService }: FtrProviderContext) => { expect( stats?.detection_rules.detection_rule_status.custom_rules.query.search_duration.min ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.enrichment_duration.max + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.enrichment_duration.avg + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.query.enrichment_duration.min + ).to.be.above(0); }); it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { @@ -387,7 +396,7 @@ export default ({ getService }: FtrProviderContext) => { expect(stats?.detection_rules.detection_rule_status.custom_rules.eql.succeeded).to.eql(1); }); - it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + it('should have non zero values for "succeeded", "index_duration", "search_duration" and "enrichment_duration"', () => { expect( stats?.detection_rules.detection_rule_status.custom_rules.eql.index_duration.max ).to.be.above(1); @@ -406,6 +415,15 @@ export default ({ getService }: FtrProviderContext) => { expect( stats?.detection_rules.detection_rule_status.custom_rules.eql.search_duration.min ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.enrichment_duration.max + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.enrichment_duration.avg + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.eql.enrichment_duration.min + ).to.be.above(0); }); it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { @@ -575,7 +593,7 @@ export default ({ getService }: FtrProviderContext) => { ).to.eql(1); }); - it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + it('should have non zero values for "succeeded", "index_duration", "search_duration" and "enrichment_duration"', () => { expect( stats?.detection_rules.detection_rule_status.custom_rules.threshold.index_duration.max ).to.be.above(1); @@ -594,6 +612,18 @@ export default ({ getService }: FtrProviderContext) => { expect( stats?.detection_rules.detection_rule_status.custom_rules.threshold.search_duration.min ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.enrichment_duration + .max + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.enrichment_duration + .avg + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threshold.enrichment_duration + .min + ).to.be.above(0); }); it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { @@ -772,7 +802,7 @@ export default ({ getService }: FtrProviderContext) => { ).to.eql(1); }); - it('should have non zero values for "succeeded", "index_duration", and "search_duration"', () => { + it('should have non zero values for "succeeded", "index_duration", "search_duration" and "enrichment_duration"', () => { expect( stats?.detection_rules.detection_rule_status.custom_rules.threat_match.index_duration.max ).to.be.above(1); @@ -791,6 +821,18 @@ export default ({ getService }: FtrProviderContext) => { expect( stats?.detection_rules.detection_rule_status.custom_rules.threat_match.search_duration.min ).to.be.above(1); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.enrichment_duration + .max + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.enrichment_duration + .avg + ).to.be.above(0); + expect( + stats?.detection_rules.detection_rule_status.custom_rules.threat_match.enrichment_duration + .min + ).to.be.above(0); }); it('should have a total value for "detection_rule_status.custom_rules" rule ', () => { From 36f330ec9c87493d517012b49ec3c60a0dcd6898 Mon Sep 17 00:00:00 2001 From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com> Date: Mon, 17 Oct 2022 10:31:26 -0500 Subject: [PATCH 08/74] [ML] Add functional tests for Index data visualizer's random sampler controls (#142278) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../document_count_content.tsx | 12 ++- .../constants/random_sampler.ts | 9 +- .../apps/ml/data_visualizer/index.ts | 1 + .../index_data_visualizer_random_sampler.ts | 80 ++++++++++++++++++ .../ml/data_visualizer_index_based.ts | 84 +++++++++++++++++++ 5 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx index 911eb851924e3..a2eb458bbdf46 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx @@ -110,7 +110,7 @@ export const DocumentCountContent: FC = ({ const ProbabilityUsed = randomSamplerPreference !== RANDOM_SAMPLER_OPTION.OFF && isDefined(samplingProbability) ? ( - <> +
= ({ defaultMessage="Probability used: {samplingProbability}%" values={{ samplingProbability: samplingProbability * 100 }} /> - +
) : null; return ( @@ -127,7 +127,8 @@ export const DocumentCountContent: FC = ({ = ({ size="xs" iconType="gear" onClick={onShowSamplingOptions} - data-test-subj="discoverSamplingOptionsToggle" + data-test-subj="dvRandomSamplerOptionsButton" aria-label={i18n.translate('xpack.dataVisualizer.samplingOptionsButton', { defaultMessage: 'Sampling options', })} @@ -157,6 +158,7 @@ export const DocumentCountContent: FC = ({ = ({ )} > @@ -212,6 +215,7 @@ export const DocumentCountContent: FC = ({ } }} step={RANDOM_SAMPLER_STEP} + data-test-subj="dvRandomSamplerProbabilityRange" /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts index c2188bab87fe5..310ccde5f9e29 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/constants/random_sampler.ts @@ -24,20 +24,27 @@ export const RANDOM_SAMPLER_OPTION = { export type RandomSamplerOption = typeof RANDOM_SAMPLER_OPTION[keyof typeof RANDOM_SAMPLER_OPTION]; -export const RANDOM_SAMPLER_SELECT_OPTIONS: Array<{ value: RandomSamplerOption; text: string }> = [ +export const RANDOM_SAMPLER_SELECT_OPTIONS: Array<{ + value: RandomSamplerOption; + text: string; + 'data-test-subj': string; +}> = [ { + 'data-test-subj': 'dvRandomSamplerOptionOnAutomatic', value: RANDOM_SAMPLER_OPTION.ON_AUTOMATIC, text: i18n.translate('xpack.dataVisualizer.randomSamplerPreference.onAutomaticLabel', { defaultMessage: 'On - automatic', }), }, { + 'data-test-subj': 'dvRandomSamplerOptionOnManual', value: RANDOM_SAMPLER_OPTION.ON_MANUAL, text: i18n.translate('xpack.dataVisualizer.randomSamplerPreference.onManualLabel', { defaultMessage: 'On - manual', }), }, { + 'data-test-subj': 'dvRandomSamplerOptionOff', value: RANDOM_SAMPLER_OPTION.OFF, text: i18n.translate('xpack.dataVisualizer.randomSamplerPreference.offLabel', { defaultMessage: 'Off', diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts index ab14b7f3c86c0..13ed76a002ca6 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts @@ -33,6 +33,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./index_data_visualizer')); + loadTestFile(require.resolve('./index_data_visualizer_random_sampler')); loadTestFile(require.resolve('./index_data_visualizer_filters')); loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover')); loadTestFile(require.resolve('./index_data_visualizer_grid_in_dashboard')); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts new file mode 100644 index 0000000000000..7df4e9c18eee7 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_random_sampler.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { farequoteDataViewTestData, farequoteLuceneSearchTestData } from './index_test_data'; + +export default function ({ getPageObject, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const browser = getService('browser'); + async function goToSourceForIndexBasedDataVisualizer(sourceIndexOrSavedSearch: string) { + await ml.testExecution.logTestStep(`navigates to Data Visualizer page`); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep(`loads the saved search selection page`); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep(`loads the index data visualizer page`); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(sourceIndexOrSavedSearch); + } + describe('index based random sampler controls', function () { + this.tags(['ml']); + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs'); + + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_module_sample_logs', '@timestamp'); + await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + }); + + after(async () => { + await ml.testResources.deleteSavedSearches(); + await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + await ml.testResources.deleteIndexPatternByTitle('ft_module_sample_logs'); + await browser.removeLocalStorageItem('dataVisualizer.randomSamplerPreference'); + }); + + describe('with small data sets', function () { + it(`has random sampler 'on - automatic' by default`, async () => { + await goToSourceForIndexBasedDataVisualizer( + farequoteDataViewTestData.sourceIndexOrSavedSearch + ); + + await ml.dataVisualizerIndexBased.assertRandomSamplingOption( + 'dvRandomSamplerOptionOnAutomatic', + 100 + ); + }); + + it(`retains random sampler 'off' setting`, async () => { + await ml.dataVisualizerIndexBased.setRandomSamplingOption('dvRandomSamplerOptionOff'); + + await goToSourceForIndexBasedDataVisualizer( + farequoteLuceneSearchTestData.sourceIndexOrSavedSearch + ); + await ml.dataVisualizerIndexBased.assertRandomSamplingOption('dvRandomSamplerOptionOff'); + }); + + it(`retains random sampler 'on - manual' setting`, async () => { + await ml.dataVisualizerIndexBased.setRandomSamplingOption('dvRandomSamplerOptionOnManual'); + + await goToSourceForIndexBasedDataVisualizer('ft_module_sample_logs'); + await ml.dataVisualizerIndexBased.assertRandomSamplingOption( + 'dvRandomSamplerOptionOnManual', + 50 + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 0e1860de4dab9..600790294a539 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -17,6 +17,7 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ const PageObjects = getPageObjects(['discover']); const queryBar = getService('queryBar'); const filterBar = getService('filterBar'); + const browser = getService('browser'); return { async assertTimeRangeSelectorSectionExists() { @@ -231,5 +232,88 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ } ); }, + + async assertRandomSamplingOptionsButtonExists() { + await testSubjects.existOrFail('dvRandomSamplerOptionsButton'); + }, + + async assertRandomSamplingOption( + expectedOption: + | 'dvRandomSamplerOptionOnAutomatic' + | 'dvRandomSamplerOptionOnManual' + | 'dvRandomSamplerOptionOff', + expectedProbability?: number + ) { + await retry.tryForTime(20000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.clickWhenNotDisabled('dvRandomSamplerOptionsButton'); + await testSubjects.existOrFail('dvRandomSamplerOptionsPopover'); + + if (expectedOption === 'dvRandomSamplerOptionOff') { + await testSubjects.existOrFail('dvRandomSamplerOptionOff', { timeout: 1000 }); + await testSubjects.missingOrFail('dvRandomSamplerProbabilityRange', { timeout: 1000 }); + await testSubjects.missingOrFail('dvRandomSamplerAutomaticProbabilityMsg', { + timeout: 1000, + }); + } + + if (expectedOption === 'dvRandomSamplerOptionOnManual') { + await testSubjects.existOrFail('dvRandomSamplerOptionOnManual', { timeout: 1000 }); + await testSubjects.existOrFail('dvRandomSamplerProbabilityRange', { timeout: 1000 }); + if (expectedProbability !== undefined) { + const probability = await testSubjects.getAttribute( + 'dvRandomSamplerProbabilityRange', + 'value' + ); + expect(probability).to.eql( + `${expectedProbability}`, + `Expected probability to be ${expectedProbability}, got ${probability}` + ); + } + } + + if (expectedOption === 'dvRandomSamplerOptionOnAutomatic') { + await testSubjects.existOrFail('dvRandomSamplerOptionOnAutomatic', { timeout: 1000 }); + await testSubjects.existOrFail('dvRandomSamplerAutomaticProbabilityMsg', { + timeout: 1000, + }); + + if (expectedProbability !== undefined) { + const probabilityText = await testSubjects.getVisibleText( + 'dvRandomSamplerAutomaticProbabilityMsg' + ); + expect(probabilityText).to.contain( + `${expectedProbability}`, + `Expected probability text to contain ${expectedProbability}, got ${probabilityText}` + ); + } + } + }); + }, + + async setRandomSamplingOption( + option: + | 'dvRandomSamplerOptionOnAutomatic' + | 'dvRandomSamplerOptionOnManual' + | 'dvRandomSamplerOptionOff' + ) { + await retry.tryForTime(20000, async () => { + // escape popover + await browser.pressKeys(browser.keys.ESCAPE); + await this.assertRandomSamplingOptionsButtonExists(); + await testSubjects.clickWhenNotDisabled('dvRandomSamplerOptionsButton'); + await testSubjects.existOrFail('dvRandomSamplerOptionsPopover', { timeout: 1000 }); + + await testSubjects.clickWhenNotDisabled('dvRandomSamplerOptionsSelect'); + + await testSubjects.existOrFail('dvRandomSamplerOptionOff', { timeout: 1000 }); + await testSubjects.existOrFail('dvRandomSamplerOptionOnManual', { timeout: 1000 }); + await testSubjects.existOrFail('dvRandomSamplerOptionOnAutomatic', { timeout: 1000 }); + + await testSubjects.click(option); + + await this.assertRandomSamplingOption(option); + }); + }, }; } From 46ccdc9ee012cadfd30127719d1bf11dfd375637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 17 Oct 2022 17:41:42 +0200 Subject: [PATCH 09/74] [LaunchDarkly] Add Deployment Metadata (#143002) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test_suites/core_plugins/rendering.ts | 4 + x-pack/plugins/cloud/README.md | 12 +- ...ud_deployment_id_analytics_context.test.ts | 10 +- ...r_cloud_deployment_id_analytics_context.ts | 33 ++- x-pack/plugins/cloud/public/mocks.tsx | 2 + x-pack/plugins/cloud/public/plugin.tsx | 57 ++++- .../collectors/cloud_usage_collector.test.ts | 48 ++-- .../collectors/cloud_usage_collector.ts | 14 +- x-pack/plugins/cloud/server/config.ts | 4 + x-pack/plugins/cloud/server/mocks.ts | 2 + x-pack/plugins/cloud/server/plugin.ts | 38 +++- .../common/metadata_service/index.ts | 8 + .../metadata_service/metadata_service.test.ts | 97 ++++++++ .../metadata_service/metadata_service.ts | 113 ++++++++++ .../cloud_experiments/kibana.json | 2 +- .../public/launch_darkly_client/index.ts | 12 + .../launch_darkly_client.test.mock.ts} | 11 +- .../launch_darkly_client.test.ts | 168 ++++++++++++++ .../launch_darkly_client.ts | 79 +++++++ .../cloud_experiments/public/plugin.test.ts | 171 ++++++++++---- .../cloud_experiments/public/plugin.ts | 98 +++++--- .../cloud_experiments/server/config.test.ts | 30 ++- .../cloud_experiments/server/config.ts | 3 + .../server/launch_darkly_client/index.ts | 8 + .../launch_darkly_client.test.mock.ts} | 1 - .../launch_darkly_client.test.ts | 211 ++++++++++++++++++ .../launch_darkly_client.ts | 104 +++++++++ .../server/launch_darkly_client/mocks.ts | 26 +++ .../cloud_experiments/server/plugin.test.ts | 201 +++++++++++------ .../cloud_experiments/server/plugin.ts | 90 +++++--- .../usage/register_usage_collector.test.ts | 41 +--- .../server/usage/register_usage_collector.ts | 19 +- .../cloud_experiments/tsconfig.json | 1 + .../schema/xpack_plugins.json | 9 + 34 files changed, 1458 insertions(+), 269 deletions(-) create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/index.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.test.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/index.ts rename x-pack/plugins/cloud_integrations/cloud_experiments/public/{plugin.test.mock.ts => launch_darkly_client/launch_darkly_client.test.mock.ts} (80%) create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/index.ts rename x-pack/plugins/cloud_integrations/cloud_experiments/server/{plugin.test.mock.ts => launch_darkly_client/launch_darkly_client.test.mock.ts} (97%) create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts create mode 100644 x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/mocks.ts diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 0970854a5aeea..6509d9149b99d 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -170,12 +170,16 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cloud.base_url (string)', 'xpack.cloud.cname (string)', 'xpack.cloud.deployment_url (string)', + 'xpack.cloud.is_elastic_staff_owned (boolean)', + 'xpack.cloud.trial_end_date (string)', 'xpack.cloud_integrations.chat.chatURL (string)', // No PII. This is an escape patch to override LaunchDarkly's flag resolution mechanism for testing or quick fix. 'xpack.cloud_integrations.experiments.flag_overrides (record)', // Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared. // Added here for documentation purposes. // 'xpack.cloud_integrations.experiments.launch_darkly.client_id (string)', + // 'xpack.cloud_integrations.experiments.launch_darkly.client_log_level (string)', + 'xpack.cloud_integrations.experiments.metadata_refresh_interval (duration)', 'xpack.cloud_integrations.full_story.org_id (any)', // No PII. Just the list of event types we want to forward to FullStory. 'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)', diff --git a/x-pack/plugins/cloud/README.md b/x-pack/plugins/cloud/README.md index 77f73a3eaea01..0b9a75de7e030 100644 --- a/x-pack/plugins/cloud/README.md +++ b/x-pack/plugins/cloud/README.md @@ -52,4 +52,14 @@ This is the path to the Cloud Account and Billing page. The value is already pre This value is the same as `baseUrl` on ESS but can be customized on ECE. -**Example:** `cloud.elastic.co` (on ESS) \ No newline at end of file +**Example:** `cloud.elastic.co` (on ESS) + +### `trial_end_date` + +The end date for the Elastic Cloud trial. Only available on Elastic Cloud. + +**Example:** `2020-10-14T10:40:22Z` + +### `is_elastic_staff_owned` + +`true` if the deployment is owned by an Elastician. Only available on Elastic Cloud. \ No newline at end of file diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts index a6dc1f59b00e3..4793bb1ac6af9 100644 --- a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts @@ -6,7 +6,7 @@ */ import { firstValueFrom } from 'rxjs'; -import { registerCloudDeploymentIdAnalyticsContext } from './register_cloud_deployment_id_analytics_context'; +import { registerCloudDeploymentMetadataAnalyticsContext } from './register_cloud_deployment_id_analytics_context'; describe('registerCloudDeploymentIdAnalyticsContext', () => { let analytics: { registerContextProvider: jest.Mock }; @@ -17,14 +17,16 @@ describe('registerCloudDeploymentIdAnalyticsContext', () => { }); test('it does not register the context provider if cloudId not provided', () => { - registerCloudDeploymentIdAnalyticsContext(analytics); + registerCloudDeploymentMetadataAnalyticsContext(analytics, {}); expect(analytics.registerContextProvider).not.toHaveBeenCalled(); }); test('it registers the context provider and emits the cloudId', async () => { - registerCloudDeploymentIdAnalyticsContext(analytics, 'cloud_id'); + registerCloudDeploymentMetadataAnalyticsContext(analytics, { id: 'cloud_id' }); expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1); const [{ context$ }] = analytics.registerContextProvider.mock.calls[0]; - await expect(firstValueFrom(context$)).resolves.toEqual({ cloudId: 'cloud_id' }); + await expect(firstValueFrom(context$)).resolves.toEqual({ + cloudId: 'cloud_id', + }); }); }); diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts index e8bdc6b37b50c..68130cdcda799 100644 --- a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts @@ -8,21 +8,44 @@ import type { AnalyticsClient } from '@kbn/analytics-client'; import { of } from 'rxjs'; -export function registerCloudDeploymentIdAnalyticsContext( +export interface CloudDeploymentMetadata { + id?: string; + trial_end_date?: string; + is_elastic_staff_owned?: boolean; +} + +export function registerCloudDeploymentMetadataAnalyticsContext( analytics: Pick, - cloudId?: string + cloudMetadata: CloudDeploymentMetadata ) { - if (!cloudId) { + if (!cloudMetadata.id) { return; } + const { + id: cloudId, + trial_end_date: cloudTrialEndDate, + is_elastic_staff_owned: cloudIsElasticStaffOwned, + } = cloudMetadata; + analytics.registerContextProvider({ - name: 'Cloud Deployment ID', - context$: of({ cloudId }), + name: 'Cloud Deployment Metadata', + context$: of({ cloudId, cloudTrialEndDate, cloudIsElasticStaffOwned }), schema: { cloudId: { type: 'keyword', _meta: { description: 'The Cloud Deployment ID' }, }, + cloudTrialEndDate: { + type: 'date', + _meta: { description: 'When the Elastic Cloud trial ends/ended', optional: true }, + }, + cloudIsElasticStaffOwned: { + type: 'boolean', + _meta: { + description: '`true` if the owner of the deployment is an Elastician', + optional: true, + }, + }, }, }); } diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx index 608e826657b73..fb1b66adcec98 100644 --- a/x-pack/plugins/cloud/public/mocks.tsx +++ b/x-pack/plugins/cloud/public/mocks.tsx @@ -18,6 +18,8 @@ function createSetupMock() { deploymentUrl: 'deployment-url', profileUrl: 'profile-url', organizationUrl: 'organization-url', + isElasticStaffOwned: true, + trialEndDate: new Date('2020-10-01T14:13:12Z'), registerCloudService: jest.fn(), }; } diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index f50f41f3c79cd..ded7924b50631 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; +import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants'; import { getFullCloudUrl } from './utils'; @@ -20,11 +20,8 @@ export interface CloudConfigType { profile_url?: string; deployment_url?: string; organization_url?: string; - full_story: { - enabled: boolean; - org_id?: string; - eventTypesAllowlist?: string[]; - }; + trial_end_date?: string; + is_elastic_staff_owned?: boolean; } export interface CloudStart { @@ -55,14 +52,50 @@ export interface CloudStart { } export interface CloudSetup { + /** + * Cloud ID. Undefined if not running on Cloud. + */ cloudId?: string; + /** + * This value is the same as `baseUrl` on ESS but can be customized on ECE. + */ cname?: string; + /** + * This is the URL of the Cloud interface. + */ baseUrl?: string; + /** + * The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud. + */ deploymentUrl?: string; + /** + * The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud. + */ profileUrl?: string; + /** + * The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud. + */ organizationUrl?: string; + /** + * This is the path to the Snapshots page for the deployment to which the Kibana instance belongs. The value is already prepended with `deploymentUrl`. + */ snapshotsUrl?: string; + /** + * `true` when Kibana is running on Elastic Cloud. + */ isCloudEnabled: boolean; + /** + * When the Cloud Trial ends/ended for the organization that owns this deployment. Only available when running on Elastic Cloud. + */ + trialEndDate?: Date; + /** + * `true` if the Elastic Cloud organization that owns this deployment is owned by an Elastician. Only available when running on Elastic Cloud. + */ + isElasticStaffOwned?: boolean; + /** + * Registers CloudServiceProviders so start's `CloudContextProvider` hooks them. + * @param contextProvider The React component from the Service Provider. + */ registerCloudService: (contextProvider: FC) => void; } @@ -84,15 +117,23 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup): CloudSetup { - registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id); + registerCloudDeploymentMetadataAnalyticsContext(core.analytics, this.config); - const { id, cname, base_url: baseUrl } = this.config; + const { + id, + cname, + base_url: baseUrl, + trial_end_date: trialEndDate, + is_elastic_staff_owned: isElasticStaffOwned, + } = this.config; return { cloudId: id, cname, baseUrl, ...this.getCloudUrls(), + trialEndDate: trialEndDate ? new Date(trialEndDate) : undefined, + isElasticStaffOwned, isCloudEnabled: this.isCloudEnabled, registerCloudService: (contextProvider) => { this.contextProviders.push(contextProvider); diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts index f9fdef5319d59..612b75e5d68d3 100644 --- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts +++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts @@ -5,31 +5,51 @@ * 2.0. */ +import { + createCollectorFetchContextMock, + usageCollectionPluginMock, +} from '@kbn/usage-collection-plugin/server/mocks'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { createCloudUsageCollector } from './cloud_usage_collector'; -import { createCollectorFetchContextMock } from '@kbn/usage-collection-plugin/server/mocks'; +import { CollectorFetchContext } from '@kbn/usage-collection-plugin/server'; -const mockUsageCollection = () => ({ - makeUsageCollector: jest.fn().mockImplementation((args: any) => ({ ...args })), -}); +describe('createCloudUsageCollector', () => { + let usageCollection: UsageCollectionSetup; + let collectorFetchContext: jest.Mocked; -const getMockConfigs = (isCloudEnabled: boolean) => ({ isCloudEnabled }); + beforeEach(() => { + usageCollection = usageCollectionPluginMock.createSetupContract(); + collectorFetchContext = createCollectorFetchContextMock(); + }); -describe('createCloudUsageCollector', () => { it('calls `makeUsageCollector`', () => { - const mockConfigs = getMockConfigs(false); - const usageCollection = mockUsageCollection(); - createCloudUsageCollector(usageCollection as any, mockConfigs); + createCloudUsageCollector(usageCollection, { isCloudEnabled: false }); expect(usageCollection.makeUsageCollector).toBeCalledTimes(1); }); describe('Fetched Usage data', () => { it('return isCloudEnabled boolean', async () => { - const mockConfigs = getMockConfigs(true); - const usageCollection = mockUsageCollection() as any; - const collector = createCloudUsageCollector(usageCollection, mockConfigs); - const collectorFetchContext = createCollectorFetchContextMock(); + const collector = createCloudUsageCollector(usageCollection, { isCloudEnabled: true }); + + expect(await collector.fetch(collectorFetchContext)).toStrictEqual({ + isCloudEnabled: true, + isElasticStaffOwned: undefined, + trialEndDate: undefined, + }); + }); + + it('return inTrial boolean if trialEndDateIsProvided', async () => { + const collector = createCloudUsageCollector(usageCollection, { + isCloudEnabled: true, + trialEndDate: '2020-10-01T14:30:16Z', + }); - expect((await collector.fetch(collectorFetchContext)).isCloudEnabled).toBe(true); // Adding the await because the fetch can be a Promise or a synchronous method and TS complains in the test if not awaited + expect(await collector.fetch(collectorFetchContext)).toStrictEqual({ + isCloudEnabled: true, + isElasticStaffOwned: undefined, + trialEndDate: '2020-10-01T14:30:16Z', + inTrial: false, + }); }); }); }); diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts index 28f6e8c94d0b8..147f61a57312b 100644 --- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts +++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts @@ -9,23 +9,35 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; interface Config { isCloudEnabled: boolean; + trialEndDate?: string; + isElasticStaffOwned?: boolean; } interface CloudUsage { isCloudEnabled: boolean; + trialEndDate?: string; + inTrial?: boolean; + isElasticStaffOwned?: boolean; } export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, config: Config) { - const { isCloudEnabled } = config; + const { isCloudEnabled, trialEndDate, isElasticStaffOwned } = config; + const trialEndDateMs = trialEndDate ? new Date(trialEndDate).getTime() : undefined; return usageCollection.makeUsageCollector({ type: 'cloud', isReady: () => true, schema: { isCloudEnabled: { type: 'boolean' }, + trialEndDate: { type: 'date' }, + inTrial: { type: 'boolean' }, + isElasticStaffOwned: { type: 'boolean' }, }, fetch: () => { return { isCloudEnabled, + isElasticStaffOwned, + trialEndDate, + ...(trialEndDateMs ? { inTrial: Date.now() <= trialEndDateMs } : {}), }; }, }); diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 512542c756798..028298c2da331 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -26,6 +26,8 @@ const configSchema = schema.object({ id: schema.maybe(schema.string()), organization_url: schema.maybe(schema.string()), profile_url: schema.maybe(schema.string()), + trial_end_date: schema.maybe(schema.string()), + is_elastic_staff_owned: schema.maybe(schema.boolean()), }); export type CloudConfigType = TypeOf; @@ -38,6 +40,8 @@ export const config: PluginConfigDescriptor = { id: true, organization_url: true, profile_url: true, + trial_end_date: true, + is_elastic_staff_owned: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/cloud/server/mocks.ts b/x-pack/plugins/cloud/server/mocks.ts index 557e64edf6cc1..ad64768951450 100644 --- a/x-pack/plugins/cloud/server/mocks.ts +++ b/x-pack/plugins/cloud/server/mocks.ts @@ -13,6 +13,8 @@ function createSetupMock(): jest.Mocked { instanceSizeMb: 1234, deploymentId: 'deployment-id', isCloudEnabled: true, + isElasticStaffOwned: true, + trialEndDate: new Date('2020-10-01T14:13:12Z'), apm: { url: undefined, secretToken: undefined, diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 9cf1a308800a0..4e54c2b9b7f40 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -7,7 +7,7 @@ import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; +import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import type { CloudConfigType } from './config'; import { registerCloudUsageCollector } from './collectors'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; @@ -18,11 +18,37 @@ interface PluginsSetup { usageCollection?: UsageCollectionSetup; } +/** + * Setup contract + */ export interface CloudSetup { + /** + * The deployment's Cloud ID. Only available when running on Elastic Cloud. + */ cloudId?: string; + /** + * The deployment's ID. Only available when running on Elastic Cloud. + */ deploymentId?: string; + /** + * `true` when running on Elastic Cloud. + */ isCloudEnabled: boolean; + /** + * The size of the instance in which Kibana is running. Only available when running on Elastic Cloud. + */ instanceSizeMb?: number; + /** + * When the Cloud Trial ends/ended for the organization that owns this deployment. Only available when running on Elastic Cloud. + */ + trialEndDate?: Date; + /** + * `true` if the Elastic Cloud organization that owns this deployment is owned by an Elastician. Only available when running on Elastic Cloud. + */ + isElasticStaffOwned?: boolean; + /** + * APM configuration keys. + */ apm: { url?: string; secretToken?: string; @@ -38,14 +64,20 @@ export class CloudPlugin implements Plugin { public setup(core: CoreSetup, { usageCollection }: PluginsSetup): CloudSetup { const isCloudEnabled = getIsCloudEnabled(this.config.id); - registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id); - registerCloudUsageCollector(usageCollection, { isCloudEnabled }); + registerCloudDeploymentMetadataAnalyticsContext(core.analytics, this.config); + registerCloudUsageCollector(usageCollection, { + isCloudEnabled, + trialEndDate: this.config.trial_end_date, + isElasticStaffOwned: this.config.is_elastic_staff_owned, + }); return { cloudId: this.config.id, instanceSizeMb: readInstanceSizeMb(), deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url), isCloudEnabled, + trialEndDate: this.config.trial_end_date ? new Date(this.config.trial_end_date) : undefined, + isElasticStaffOwned: this.config.is_elastic_staff_owned, apm: { url: this.config.apm?.url, secretToken: this.config.apm?.secret_token, diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/index.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/index.ts new file mode 100644 index 0000000000000..74e2655e8302f --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/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 { MetadataService } from './metadata_service'; diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.test.ts new file mode 100644 index 0000000000000..8764eb434213e --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.test.ts @@ -0,0 +1,97 @@ +/* + * 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 moment from 'moment'; +import { fakeSchedulers } from 'rxjs-marbles/jest'; +import { firstValueFrom } from 'rxjs'; +import { MetadataService } from './metadata_service'; + +jest.mock('rxjs', () => { + const RxJs = jest.requireActual('rxjs'); + + return { + ...RxJs, + debounceTime: () => RxJs.identity, // Remove the delaying effect of debounceTime + }; +}); + +describe('MetadataService', () => { + jest.useFakeTimers(); + + let metadataService: MetadataService; + + beforeEach(() => { + metadataService = new MetadataService({ metadata_refresh_interval: moment.duration(1, 's') }); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.clearAllMocks(); + }); + + describe('setup', () => { + test('emits the initial metadata', async () => { + const initialMetadata = { userId: 'fake-user-id', kibanaVersion: 'version' }; + metadataService.setup(initialMetadata); + await expect(firstValueFrom(metadataService.userMetadata$)).resolves.toStrictEqual( + initialMetadata + ); + }); + + test( + 'emits in_trial when trial_end_date is provided', + fakeSchedulers(async (advance) => { + const initialMetadata = { + userId: 'fake-user-id', + kibanaVersion: 'version', + trial_end_date: new Date(0).toISOString(), + }; + metadataService.setup(initialMetadata); + + // Still equals initialMetadata + await expect(firstValueFrom(metadataService.userMetadata$)).resolves.toStrictEqual( + initialMetadata + ); + + // After scheduler kicks in... + advance(1); // The timer kicks in first on 0 (but let's give us 1ms so the trial is expired) + await new Promise((resolve) => process.nextTick(resolve)); // The timer triggers a promise, so we need to skip to the next tick + await expect(firstValueFrom(metadataService.userMetadata$)).resolves.toStrictEqual({ + ...initialMetadata, + in_trial: false, + }); + }) + ); + }); + + describe('start', () => { + const initialMetadata = { userId: 'fake-user-id', kibanaVersion: 'version' }; + beforeEach(() => { + metadataService.setup(initialMetadata); + }); + + test( + 'emits has_data after resolving the `hasUserDataView`', + fakeSchedulers(async (advance) => { + metadataService.start({ hasDataFetcher: async () => ({ has_data: true }) }); + + // Still equals initialMetadata + await expect(firstValueFrom(metadataService.userMetadata$)).resolves.toStrictEqual( + initialMetadata + ); + + // After scheduler kicks in... + advance(1); // The timer kicks in first on 0 (but let's give us 1ms so the trial is expired) + await new Promise((resolve) => process.nextTick(resolve)); // The timer triggers a promise, so we need to skip to the next tick + await expect(firstValueFrom(metadataService.userMetadata$)).resolves.toStrictEqual({ + ...initialMetadata, + has_data: true, + }); + }) + ); + }); +}); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.ts new file mode 100644 index 0000000000000..6691211b7b01c --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/common/metadata_service/metadata_service.ts @@ -0,0 +1,113 @@ +/* + * 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 { + BehaviorSubject, + debounceTime, + distinct, + exhaustMap, + filter, + type Observable, + shareReplay, + Subject, + takeUntil, + takeWhile, + timer, +} from 'rxjs'; +import { type Duration } from 'moment'; + +export interface MetadataServiceStartContract { + hasDataFetcher: () => Promise<{ has_data: boolean }>; +} + +export interface UserMetadata extends Record { + // Static values + userId: string; + kibanaVersion: string; + trial_end_date?: string; + is_elastic_staff_owned?: boolean; + // Dynamic/calculated values + in_trial?: boolean; + has_data?: boolean; +} + +export interface MetadataServiceConfig { + metadata_refresh_interval: Duration; +} + +export class MetadataService { + private readonly _userMetadata$ = new BehaviorSubject(undefined); + private readonly stop$ = new Subject(); + + constructor(private readonly config: MetadataServiceConfig) {} + + public setup(initialUserMetadata: UserMetadata) { + this._userMetadata$.next(initialUserMetadata); + + // Calculate `in_trial` based on the `trial_end_date`. + // Elastic Cloud allows customers to end their trials earlier or even extend it in some cases, but this is a good compromise for now. + const trialEndDate = initialUserMetadata.trial_end_date; + if (trialEndDate) { + this.scheduleUntil( + () => ({ in_trial: Date.now() <= new Date(trialEndDate).getTime() }), + // Stop recalculating in_trial when the user is no-longer in trial + (metadata) => metadata.in_trial === false + ); + } + } + + public get userMetadata$(): Observable { + return this._userMetadata$.pipe( + filter(Boolean), // Ensure we don't return undefined + debounceTime(100), // Swallows multiple emissions that may occur during bootstrap + distinct((meta) => [meta.in_trial, meta.has_data].join('-')), // Checks if any of the dynamic fields have changed + shareReplay(1) + ); + } + + public start({ hasDataFetcher }: MetadataServiceStartContract) { + // If no initial metadata (setup was not called) => it should not schedule any metadata extension + if (!this._userMetadata$.value) return; + + this.scheduleUntil( + async () => hasDataFetcher(), + // Stop checking the moment the user has any data + (metadata) => metadata.has_data === true + ); + } + + public stop() { + this.stop$.next(); + this._userMetadata$.complete(); + } + + /** + * Schedules a timer that calls `fn` to update the {@link UserMetadata} until `untilFn` returns true. + * @param fn Method to calculate the dynamic metadata. + * @param untilFn Method that returns true when the scheduler should stop calling fn (potentially because the dynamic value is not expected to change anymore). + * @private + */ + private scheduleUntil( + fn: () => Partial | Promise>, + untilFn: (value: UserMetadata) => boolean + ) { + timer(0, this.config.metadata_refresh_interval.asMilliseconds()) + .pipe( + takeUntil(this.stop$), + exhaustMap(async () => { + this._userMetadata$.next({ + ...this._userMetadata$.value!, // We are running the schedules after the initial user metadata is set + ...(await fn()), + }); + }), + takeWhile(() => { + return !untilFn(this._userMetadata$.value!); + }) + ) + .subscribe(); + } +} diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/kibana.json b/x-pack/plugins/cloud_integrations/cloud_experiments/kibana.json index 6bbb41f796f94..6dc3e8fe34c87 100755 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/kibana.json +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/kibana.json @@ -10,6 +10,6 @@ "server": true, "ui": true, "configPath": ["xpack", "cloud_integrations", "experiments"], - "requiredPlugins": ["cloud"], + "requiredPlugins": ["cloud", "dataViews"], "optionalPlugins": ["usageCollection"] } diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/index.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/index.ts new file mode 100644 index 0000000000000..ac961286b7043 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/index.ts @@ -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. + */ + +export { + LaunchDarklyClient, + type LaunchDarklyUserMetadata, + type LaunchDarklyClientConfig, +} from './launch_darkly_client'; diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.mock.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.mock.ts similarity index 80% rename from x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.mock.ts rename to x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.mock.ts index d2bfb5b54213d..b6a43a7d0715b 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.mock.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.mock.ts @@ -9,16 +9,19 @@ import type { LDClient } from 'launchdarkly-js-client-sdk'; export function createLaunchDarklyClientMock(): jest.Mocked { return { + identify: jest.fn(), waitForInitialization: jest.fn(), variation: jest.fn(), track: jest.fn(), - identify: jest.fn(), flush: jest.fn(), } as unknown as jest.Mocked; // Using casting because we only use these APIs. No need to declare everything. } export const ldClientMock = createLaunchDarklyClientMock(); -jest.doMock('launchdarkly-js-client-sdk', () => ({ - initialize: () => ldClientMock, -})); +export const launchDarklyLibraryMock = { + initialize: jest.fn(), + basicLogger: jest.fn(), +}; + +jest.doMock('launchdarkly-js-client-sdk', () => launchDarklyLibraryMock); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts new file mode 100644 index 0000000000000..8f4b0d63c9947 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts @@ -0,0 +1,168 @@ +/* + * 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 { ldClientMock, launchDarklyLibraryMock } from './launch_darkly_client.test.mock'; +import { LaunchDarklyClient, type LaunchDarklyClientConfig } from './launch_darkly_client'; + +describe('LaunchDarklyClient - browser', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const config: LaunchDarklyClientConfig = { + client_id: 'fake-client-id', + client_log_level: 'debug', + }; + + describe('Public APIs', () => { + let client: LaunchDarklyClient; + const testUserMetadata = { userId: 'fake-user-id', kibanaVersion: 'version' }; + + beforeEach(() => { + client = new LaunchDarklyClient(config, 'version'); + }); + + describe('updateUserMetadata', () => { + test("calls the client's initialize method with all the possible values", async () => { + expect(client).toHaveProperty('launchDarklyClient', undefined); + + launchDarklyLibraryMock.initialize.mockReturnValue(ldClientMock); + + const topFields = { + name: 'First Last', + firstName: 'First', + lastName: 'Last', + email: 'first.last@boring.co', + avatar: 'fake-blue-avatar', + ip: 'my-weird-ip', + country: 'distributed', + }; + + const extraFields = { + other_field: 'my other custom field', + kibanaVersion: 'version', + }; + + await client.updateUserMetadata({ userId: 'fake-user-id', ...topFields, ...extraFields }); + + expect(launchDarklyLibraryMock.initialize).toHaveBeenCalledWith( + 'fake-client-id', + { + key: 'fake-user-id', + ...topFields, + custom: extraFields, + }, + { + application: { id: 'kibana-browser', version: 'version' }, + logger: undefined, + } + ); + + expect(client).toHaveProperty('launchDarklyClient', ldClientMock); + }); + + test('sets a minimum amount of info', async () => { + expect(client).toHaveProperty('launchDarklyClient', undefined); + + await client.updateUserMetadata({ userId: 'fake-user-id', kibanaVersion: 'version' }); + + expect(launchDarklyLibraryMock.initialize).toHaveBeenCalledWith( + 'fake-client-id', + { + key: 'fake-user-id', + custom: { kibanaVersion: 'version' }, + }, + { + application: { id: 'kibana-browser', version: 'version' }, + logger: undefined, + } + ); + }); + + test('calls identify if an update comes after initializing the client', async () => { + expect(client).toHaveProperty('launchDarklyClient', undefined); + + launchDarklyLibraryMock.initialize.mockReturnValue(ldClientMock); + await client.updateUserMetadata({ userId: 'fake-user-id', kibanaVersion: 'version' }); + + expect(launchDarklyLibraryMock.initialize).toHaveBeenCalledWith( + 'fake-client-id', + { + key: 'fake-user-id', + custom: { kibanaVersion: 'version' }, + }, + { + application: { id: 'kibana-browser', version: 'version' }, + logger: undefined, + } + ); + expect(ldClientMock.identify).not.toHaveBeenCalled(); + + expect(client).toHaveProperty('launchDarklyClient', ldClientMock); + + // Update user metadata a 2nd time + await client.updateUserMetadata({ userId: 'fake-user-id', kibanaVersion: 'version' }); + expect(ldClientMock.identify).toHaveBeenCalledWith({ + key: 'fake-user-id', + custom: { kibanaVersion: 'version' }, + }); + }); + }); + + describe('getVariation', () => { + test('returns the default value if the user has not been defined', async () => { + await expect(client.getVariation('my-feature-flag', 123)).resolves.toStrictEqual(123); + expect(ldClientMock.variation).toHaveBeenCalledTimes(0); + }); + + test('calls the LaunchDarkly client when the user has been defined', async () => { + ldClientMock.variation.mockResolvedValue(1234); + await client.updateUserMetadata(testUserMetadata); + await expect(client.getVariation('my-feature-flag', 123)).resolves.toStrictEqual(1234); + expect(ldClientMock.variation).toHaveBeenCalledTimes(1); + expect(ldClientMock.variation).toHaveBeenCalledWith('my-feature-flag', 123); + }); + }); + + describe('reportMetric', () => { + test('does not call track if the user has not been defined', () => { + client.reportMetric('my-feature-flag', {}, 123); + expect(ldClientMock.track).toHaveBeenCalledTimes(0); + }); + + test('calls the LaunchDarkly client when the user has been defined', async () => { + await client.updateUserMetadata(testUserMetadata); + client.reportMetric('my-feature-flag', {}, 123); + expect(ldClientMock.track).toHaveBeenCalledTimes(1); + expect(ldClientMock.track).toHaveBeenCalledWith('my-feature-flag', {}, 123); + }); + }); + + describe('stop', () => { + test('flushes the events', async () => { + await client.updateUserMetadata(testUserMetadata); + + ldClientMock.flush.mockResolvedValue(); + expect(() => client.stop()).not.toThrow(); + expect(ldClientMock.flush).toHaveBeenCalledTimes(1); + await new Promise((resolve) => process.nextTick(resolve)); // wait for the flush resolution + }); + + test('handles errors when flushing events', async () => { + await client.updateUserMetadata(testUserMetadata); + + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const err = new Error('Something went terribly wrong'); + ldClientMock.flush.mockRejectedValue(err); + expect(() => client.stop()).not.toThrow(); + expect(ldClientMock.flush).toHaveBeenCalledTimes(1); + await new Promise((resolve) => process.nextTick(resolve)); // wait for the flush resolution + expect(consoleWarnSpy).toHaveBeenCalledWith(err); + }); + }); + }); +}); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts new file mode 100644 index 0000000000000..f78286f0fa8ca --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts @@ -0,0 +1,79 @@ +/* + * 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 LDClient, type LDUser, type LDLogLevel } from 'launchdarkly-js-client-sdk'; + +export interface LaunchDarklyClientConfig { + client_id: string; + client_log_level: LDLogLevel; +} + +export interface LaunchDarklyUserMetadata + extends Record { + userId: string; + // We are not collecting any of the above, but this is to match the LDUser first-level definition + name?: string; + firstName?: string; + lastName?: string; + email?: string; + avatar?: string; + ip?: string; + country?: string; +} + +export class LaunchDarklyClient { + private launchDarklyClient?: LDClient; + + constructor( + private readonly ldConfig: LaunchDarklyClientConfig, + private readonly kibanaVersion: string + ) {} + + public async updateUserMetadata(userMetadata: LaunchDarklyUserMetadata) { + const { userId, name, firstName, lastName, email, avatar, ip, country, ...custom } = + userMetadata; + const launchDarklyUser: LDUser = { + key: userId, + name, + firstName, + lastName, + email, + avatar, + ip, + country, + // This casting is needed because LDUser does not allow `Record` + custom: custom as Record, + }; + if (this.launchDarklyClient) { + await this.launchDarklyClient.identify(launchDarklyUser); + } else { + const { initialize, basicLogger } = await import('launchdarkly-js-client-sdk'); + this.launchDarklyClient = initialize(this.ldConfig.client_id, launchDarklyUser, { + application: { id: 'kibana-browser', version: this.kibanaVersion }, + logger: basicLogger({ level: this.ldConfig.client_log_level }), + }); + } + } + + public async getVariation(configKey: string, defaultValue: Data): Promise { + if (!this.launchDarklyClient) return defaultValue; // Skip any action if no LD User is defined + await this.launchDarklyClient.waitForInitialization(); + return await this.launchDarklyClient.variation(configKey, defaultValue); + } + + public reportMetric(metricName: string, meta?: unknown, value?: number): void { + if (!this.launchDarklyClient) return; // Skip any action if no LD User is defined + this.launchDarklyClient.track(metricName, meta, value); + } + + public stop() { + this.launchDarklyClient + ?.flush() + // eslint-disable-next-line no-console + .catch((err) => console.warn(err)); + } +} diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.ts index 3c6396d686796..9c1e3d25537fd 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.test.ts @@ -5,17 +5,30 @@ * 2.0. */ +import { duration } from 'moment'; import { coreMock } from '@kbn/core/public/mocks'; import { cloudMock } from '@kbn/cloud-plugin/public/mocks'; -import { ldClientMock } from './plugin.test.mock'; -import { CloudExperimentsPlugin } from './plugin'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { CloudExperimentsPluginStart } from '../common'; import { FEATURE_FLAG_NAMES } from '../common/constants'; +import { CloudExperimentsPlugin } from './plugin'; +import { LaunchDarklyClient } from './launch_darkly_client'; +import { MetadataService } from '../common/metadata_service'; +jest.mock('./launch_darkly_client'); + +function getLaunchDarklyClientInstanceMock() { + const launchDarklyClientInstanceMock = ( + LaunchDarklyClient as jest.MockedClass + ).mock.instances[0] as jest.Mocked; + + return launchDarklyClientInstanceMock; +} describe('Cloud Experiments public plugin', () => { jest.spyOn(console, 'debug').mockImplementation(); // silence console.debug logs - beforeEach(() => { - jest.resetAllMocks(); + afterEach(() => { + jest.clearAllMocks(); }); describe('constructor', () => { @@ -29,6 +42,7 @@ describe('Cloud Experiments public plugin', () => { expect(plugin).toHaveProperty('stop'); expect(plugin).toHaveProperty('flagOverrides', undefined); expect(plugin).toHaveProperty('launchDarklyClient', undefined); + expect(plugin).toHaveProperty('metadataService', expect.any(MetadataService)); }); test('fails if launch_darkly is not provided in the config and it is a non-dev environment', () => { @@ -49,17 +63,34 @@ describe('Cloud Experiments public plugin', () => { const plugin = new CloudExperimentsPlugin(initializerContext); expect(plugin).toHaveProperty('flagOverrides', { my_flag: '1234' }); }); + + test('it initializes the LaunchDarkly client', () => { + const initializerContext = coreMock.createPluginInitializerContext({ + launch_darkly: { client_id: 'sdk-1234' }, + }); + const plugin = new CloudExperimentsPlugin(initializerContext); + expect(LaunchDarklyClient).toHaveBeenCalledTimes(1); + expect(plugin).toHaveProperty('launchDarklyClient', expect.any(LaunchDarklyClient)); + }); }); describe('setup', () => { let plugin: CloudExperimentsPlugin; + let metadataServiceSetupSpy: jest.SpyInstance; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext({ launch_darkly: { client_id: '1234' }, flag_overrides: { my_flag: '1234' }, + metadata_refresh_interval: duration(1, 'h'), }); plugin = new CloudExperimentsPlugin(initializerContext); + // eslint-disable-next-line dot-notation + metadataServiceSetupSpy = jest.spyOn(plugin['metadataService'], 'setup'); + }); + + afterEach(() => { + plugin.stop(); }); test('returns no contract', () => { @@ -74,37 +105,41 @@ describe('Cloud Experiments public plugin', () => { test('it skips creating the client if no client id provided in the config', () => { const initializerContext = coreMock.createPluginInitializerContext({ flag_overrides: { my_flag: '1234' }, + metadata_refresh_interval: duration(1, 'h'), }); const customPlugin = new CloudExperimentsPlugin(initializerContext); - expect(customPlugin).toHaveProperty('launchDarklyClient', undefined); customPlugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); expect(customPlugin).toHaveProperty('launchDarklyClient', undefined); }); - test('it skips creating the client if cloud is not enabled', () => { - expect(plugin).toHaveProperty('launchDarklyClient', undefined); + test('it skips identifying the user if cloud is not enabled', () => { plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: false }, }); - expect(plugin).toHaveProperty('launchDarklyClient', undefined); + + expect(metadataServiceSetupSpy).not.toHaveBeenCalled(); }); test('it initializes the LaunchDarkly client', async () => { - expect(plugin).toHaveProperty('launchDarklyClient', undefined); plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); - // await the lazy import - await new Promise((resolve) => process.nextTick(resolve)); - expect(plugin).toHaveProperty('launchDarklyClient', ldClientMock); + + expect(metadataServiceSetupSpy).toHaveBeenCalledWith({ + is_elastic_staff_owned: true, + kibanaVersion: 'version', + trial_end_date: '2020-10-01T14:13:12.000Z', + userId: '1c2412b751f056aef6e340efa5637d137442d489a4b1e3117071e7c87f8523f2', + }); }); }); }); describe('start', () => { let plugin: CloudExperimentsPlugin; + let launchDarklyInstanceMock: jest.Mocked; const firstKnownFlag = Object.keys(FEATURE_FLAG_NAMES)[0] as keyof typeof FEATURE_FLAG_NAMES; @@ -114,11 +149,19 @@ describe('Cloud Experiments public plugin', () => { flag_overrides: { [firstKnownFlag]: '1234' }, }); plugin = new CloudExperimentsPlugin(initializerContext); + launchDarklyInstanceMock = getLaunchDarklyClientInstanceMock(); + }); + + afterEach(() => { + plugin.stop(); }); test('returns the contract', () => { plugin.setup(coreMock.createSetup(), { cloud: cloudMock.createSetup() }); - const startContract = plugin.start(coreMock.createStart()); + const startContract = plugin.start(coreMock.createStart(), { + cloud: cloudMock.createStart(), + dataViews: dataViewPluginMocks.createStartContract(), + }); expect(startContract).toStrictEqual( expect.objectContaining({ getVariation: expect.any(Function), @@ -127,24 +170,46 @@ describe('Cloud Experiments public plugin', () => { ); }); + test('triggers a userMetadataUpdate for `has_data`', async () => { + plugin.setup(coreMock.createSetup(), { + cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, + }); + + const dataViews = dataViewPluginMocks.createStartContract(); + plugin.start(coreMock.createStart(), { cloud: cloudMock.createStart(), dataViews }); + + // After scheduler kicks in... + await new Promise((resolve) => setTimeout(resolve, 200)); + // Using a timeout of 0ms to let the `timer` kick in. + // For some reason, fakeSchedulers is not working on browser-side tests :shrug: + expect(launchDarklyInstanceMock.updateUserMetadata).toHaveBeenCalledWith( + expect.objectContaining({ + has_data: true, + }) + ); + }); + describe('getVariation', () => { - describe('with the user identified', () => { + let startContract: CloudExperimentsPluginStart; + describe('with the client created', () => { beforeEach(() => { plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); + startContract = plugin.start(coreMock.createStart(), { + cloud: cloudMock.createStart(), + dataViews: dataViewPluginMocks.createStartContract(), + }); }); test('uses the flag overrides to respond early', async () => { - const startContract = plugin.start(coreMock.createStart()); await expect(startContract.getVariation(firstKnownFlag, 123)).resolves.toStrictEqual( '1234' ); }); test('calls the client', async () => { - const startContract = plugin.start(coreMock.createStart()); - ldClientMock.variation.mockReturnValue('12345'); + launchDarklyInstanceMock.getVariation.mockResolvedValue('12345'); await expect( startContract.getVariation( // @ts-expect-error We only allow existing flags in FEATURE_FLAG_NAMES @@ -152,29 +217,37 @@ describe('Cloud Experiments public plugin', () => { 123 ) ).resolves.toStrictEqual('12345'); - expect(ldClientMock.variation).toHaveBeenCalledWith( + expect(launchDarklyInstanceMock.getVariation).toHaveBeenCalledWith( undefined, // it couldn't find it in FEATURE_FLAG_NAMES 123 ); }); }); - describe('with the user not identified', () => { + describe('with the client not created', () => { beforeEach(() => { - plugin.setup(coreMock.createSetup(), { - cloud: { ...cloudMock.createSetup(), isCloudEnabled: false }, + const initializerContext = coreMock.createPluginInitializerContext({ + flag_overrides: { [firstKnownFlag]: '1234' }, + metadata_refresh_interval: duration(1, 'h'), + }); + const customPlugin = new CloudExperimentsPlugin(initializerContext); + customPlugin.setup(coreMock.createSetup(), { + cloud: cloudMock.createSetup(), + }); + expect(customPlugin).toHaveProperty('launchDarklyClient', undefined); + startContract = customPlugin.start(coreMock.createStart(), { + cloud: cloudMock.createStart(), + dataViews: dataViewPluginMocks.createStartContract(), }); }); test('uses the flag overrides to respond early', async () => { - const startContract = plugin.start(coreMock.createStart()); await expect(startContract.getVariation(firstKnownFlag, 123)).resolves.toStrictEqual( '1234' ); }); test('returns the default value without calling the client', async () => { - const startContract = plugin.start(coreMock.createStart()); await expect( startContract.getVariation( // @ts-expect-error We only allow existing flags in FEATURE_FLAG_NAMES @@ -182,28 +255,32 @@ describe('Cloud Experiments public plugin', () => { 123 ) ).resolves.toStrictEqual(123); - expect(ldClientMock.variation).not.toHaveBeenCalled(); + expect(launchDarklyInstanceMock.getVariation).not.toHaveBeenCalled(); }); }); }); describe('reportMetric', () => { - describe('with the user identified', () => { + let startContract: CloudExperimentsPluginStart; + describe('with the client created', () => { beforeEach(() => { plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); + startContract = plugin.start(coreMock.createStart(), { + cloud: cloudMock.createStart(), + dataViews: dataViewPluginMocks.createStartContract(), + }); }); test('calls the track API', () => { - const startContract = plugin.start(coreMock.createStart()); startContract.reportMetric({ // @ts-expect-error We only allow existing flags in METRIC_NAMES name: 'my-flag', meta: {}, value: 1, }); - expect(ldClientMock.track).toHaveBeenCalledWith( + expect(launchDarklyInstanceMock.reportMetric).toHaveBeenCalledWith( undefined, // it couldn't find it in METRIC_NAMES {}, 1 @@ -211,22 +288,31 @@ describe('Cloud Experiments public plugin', () => { }); }); - describe('with the user not identified', () => { + describe('with the client not created', () => { beforeEach(() => { - plugin.setup(coreMock.createSetup(), { - cloud: { ...cloudMock.createSetup(), isCloudEnabled: false }, + const initializerContext = coreMock.createPluginInitializerContext({ + flag_overrides: { [firstKnownFlag]: '1234' }, + metadata_refresh_interval: duration(1, 'h'), + }); + const customPlugin = new CloudExperimentsPlugin(initializerContext); + customPlugin.setup(coreMock.createSetup(), { + cloud: cloudMock.createSetup(), + }); + expect(customPlugin).toHaveProperty('launchDarklyClient', undefined); + startContract = customPlugin.start(coreMock.createStart(), { + cloud: cloudMock.createStart(), + dataViews: dataViewPluginMocks.createStartContract(), }); }); test('calls the track API', () => { - const startContract = plugin.start(coreMock.createStart()); startContract.reportMetric({ // @ts-expect-error We only allow existing flags in METRIC_NAMES name: 'my-flag', meta: {}, value: 1, }); - expect(ldClientMock.track).not.toHaveBeenCalled(); + expect(launchDarklyInstanceMock.reportMetric).not.toHaveBeenCalled(); }); }); }); @@ -234,33 +320,28 @@ describe('Cloud Experiments public plugin', () => { describe('stop', () => { let plugin: CloudExperimentsPlugin; + let launchDarklyInstanceMock: jest.Mocked; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext({ launch_darkly: { client_id: '1234' }, flag_overrides: { my_flag: '1234' }, + metadata_refresh_interval: duration(1, 'h'), }); plugin = new CloudExperimentsPlugin(initializerContext); + launchDarklyInstanceMock = getLaunchDarklyClientInstanceMock(); plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); - plugin.start(coreMock.createStart()); + plugin.start(coreMock.createStart(), { + cloud: cloudMock.createStart(), + dataViews: dataViewPluginMocks.createStartContract(), + }); }); test('flushes the events on stop', () => { - ldClientMock.flush.mockResolvedValue(); - expect(() => plugin.stop()).not.toThrow(); - expect(ldClientMock.flush).toHaveBeenCalledTimes(1); - }); - - test('handles errors when flushing events', async () => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - const error = new Error('Something went terribly wrong'); - ldClientMock.flush.mockRejectedValue(error); expect(() => plugin.stop()).not.toThrow(); - expect(ldClientMock.flush).toHaveBeenCalledTimes(1); - await new Promise((resolve) => process.nextTick(resolve)); - expect(consoleWarnSpy).toHaveBeenCalledWith(error); + expect(launchDarklyInstanceMock.stop).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.ts index 45ca45ab08af1..164d6e45c5294 100755 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/plugin.ts @@ -6,29 +6,38 @@ */ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; -import type { LDClient } from 'launchdarkly-js-client-sdk'; import { get, has } from 'lodash'; +import { duration } from 'moment'; +import { concatMap } from 'rxjs'; import { Sha256 } from '@kbn/crypto-browser'; -import type { CloudSetup } from '@kbn/cloud-plugin/public'; +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { LaunchDarklyClient, type LaunchDarklyClientConfig } from './launch_darkly_client'; import type { CloudExperimentsFeatureFlagNames, CloudExperimentsMetric, CloudExperimentsPluginStart, } from '../common'; +import { MetadataService } from '../common/metadata_service'; import { FEATURE_FLAG_NAMES, METRIC_NAMES } from '../common/constants'; interface CloudExperimentsPluginSetupDeps { cloud: CloudSetup; } +interface CloudExperimentsPluginStartDeps { + cloud: CloudStart; + dataViews: DataViewsPublicPluginStart; +} + /** * Browser-side implementation of the Cloud Experiments plugin */ export class CloudExperimentsPlugin implements Plugin { - private launchDarklyClient?: LDClient; - private readonly clientId?: string; + private readonly metadataService: MetadataService; + private readonly launchDarklyClient?: LaunchDarklyClient; private readonly kibanaVersion: string; private readonly flagOverrides?: Record; private readonly isDev: boolean; @@ -38,22 +47,28 @@ export class CloudExperimentsPlugin this.isDev = initializerContext.env.mode.dev; this.kibanaVersion = initializerContext.env.packageInfo.version; const config = initializerContext.config.get<{ - launch_darkly?: { client_id: string }; + launch_darkly?: LaunchDarklyClientConfig; flag_overrides?: Record; + metadata_refresh_interval: string; }>(); + + this.metadataService = new MetadataService({ + metadata_refresh_interval: duration(config.metadata_refresh_interval), + }); + if (config.flag_overrides) { this.flagOverrides = config.flag_overrides; } const ldConfig = config.launch_darkly; - if (!ldConfig && !initializerContext.env.mode.dev) { + if (!ldConfig?.client_id && !initializerContext.env.mode.dev) { // If the plugin is enabled, and it's in prod mode, launch_darkly must exist // (config-schema should enforce it, but just in case). throw new Error( 'xpack.cloud_integrations.experiments.launch_darkly configuration should exist' ); } - if (ldConfig) { - this.clientId = ldConfig.client_id; + if (ldConfig?.client_id) { + this.launchDarklyClient = new LaunchDarklyClient(ldConfig, this.kibanaVersion); } } @@ -63,26 +78,13 @@ export class CloudExperimentsPlugin * @param deps {@link CloudExperimentsPluginSetupDeps} */ public setup(core: CoreSetup, deps: CloudExperimentsPluginSetupDeps) { - if (deps.cloud.isCloudEnabled && deps.cloud.cloudId && this.clientId) { - import('launchdarkly-js-client-sdk').then( - (LaunchDarkly) => { - this.launchDarklyClient = LaunchDarkly.initialize( - this.clientId!, - { - // We use the Hashed Cloud Deployment ID as the userId in the Cloud Experiments - key: sha256(deps.cloud.cloudId!), - custom: { - kibanaVersion: this.kibanaVersion, - }, - }, - { application: { id: 'kibana-browser', version: this.kibanaVersion } } - ); - }, - (err) => { - // eslint-disable-next-line no-console - console.debug(`Error setting up LaunchDarkly: ${err.toString()}`); - } - ); + if (deps.cloud.isCloudEnabled && deps.cloud.cloudId && this.launchDarklyClient) { + this.metadataService.setup({ + userId: sha256(deps.cloud.cloudId), + kibanaVersion: this.kibanaVersion, + trial_end_date: deps.cloud.trialEndDate?.toISOString(), + is_elastic_staff_owned: deps.cloud.isElasticStaffOwned, + }); } } @@ -90,7 +92,26 @@ export class CloudExperimentsPlugin * Returns the contract {@link CloudExperimentsPluginStart} * @param core {@link CoreStart} */ - public start(core: CoreStart): CloudExperimentsPluginStart { + public start( + core: CoreStart, + { cloud, dataViews }: CloudExperimentsPluginStartDeps + ): CloudExperimentsPluginStart { + if (cloud.isCloudEnabled) { + this.metadataService.start({ + hasDataFetcher: async () => ({ has_data: await dataViews.hasData.hasUserDataView() }), + }); + + // We only subscribe to the user metadata updates if Cloud is enabled. + // This way, since the user is not identified, it cannot retrieve Feature Flags from LaunchDarkly when not running on Cloud. + this.metadataService.userMetadata$ + .pipe( + // Using concatMap to ensure we call the promised update in an orderly manner to avoid concurrency issues + concatMap( + async (userMetadata) => await this.launchDarklyClient!.updateUserMetadata(userMetadata) + ) + ) + .subscribe(); // This subscription will stop on when the metadataService stops because it completes the Observable + } return { getVariation: this.getVariation, reportMetric: this.reportMetric, @@ -101,10 +122,8 @@ export class CloudExperimentsPlugin * Cleans up and flush the sending queues. */ public stop() { - this.launchDarklyClient - ?.flush() - // eslint-disable-next-line no-console - .catch((err) => console.warn(err)); + this.launchDarklyClient?.stop(); + this.metadataService.stop(); } private getVariation = async ( @@ -112,18 +131,23 @@ export class CloudExperimentsPlugin defaultValue: Data ): Promise => { const configKey = FEATURE_FLAG_NAMES[featureFlagName]; + // Apply overrides if they exist without asking LaunchDarkly. if (this.flagOverrides && has(this.flagOverrides, configKey)) { return get(this.flagOverrides, configKey, defaultValue) as Data; } - if (!this.launchDarklyClient) return defaultValue; // Skip any action if no LD User is defined - await this.launchDarklyClient.waitForInitialization(); - return this.launchDarklyClient.variation(configKey, defaultValue); + + // Skip any action if no LD Client is defined + if (!this.launchDarklyClient) { + return defaultValue; + } + + return await this.launchDarklyClient.getVariation(configKey, defaultValue); }; private reportMetric = ({ name, meta, value }: CloudExperimentsMetric): void => { const metricName = METRIC_NAMES[name]; - this.launchDarklyClient?.track(metricName, meta, value); + this.launchDarklyClient?.reportMetric(metricName, meta, value); if (this.isDev) { // eslint-disable-next-line no-console console.debug(`Reported experimentation metric ${metricName}`, { diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.test.ts index 2e762ece1d8fe..146de2c3ddc9a 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.test.ts @@ -5,13 +5,17 @@ * 2.0. */ +import moment from 'moment'; import { config } from './config'; describe('cloudExperiments config', () => { describe.each([true, false])('when disabled (dev: %p)', (dev) => { const ctx = { dev }; test('should default to `enabled:false` and the rest empty', () => { - expect(config.schema.validate({}, ctx)).toStrictEqual({ enabled: false }); + expect(config.schema.validate({}, ctx)).toStrictEqual({ + enabled: false, + metadata_refresh_interval: moment.duration(1, 'h'), + }); }); test('it should allow any additional config', () => { @@ -26,7 +30,11 @@ describe('cloudExperiments config', () => { 'my-plugin.my-feature-flag': 1234, }, }; - expect(config.schema.validate(cfg, ctx)).toStrictEqual(cfg); + expect(config.schema.validate(cfg, ctx)).toStrictEqual({ + ...cfg, + // Additional default fields + metadata_refresh_interval: moment.duration(1, 'h'), + }); }); test('it should allow any additional config (missing flag_overrides)', () => { @@ -38,7 +46,10 @@ describe('cloudExperiments config', () => { client_log_level: 'none', }, }; - expect(config.schema.validate(cfg, ctx)).toStrictEqual(cfg); + expect(config.schema.validate(cfg, ctx)).toStrictEqual({ + ...cfg, + metadata_refresh_interval: moment.duration(1, 'h'), + }); }); test('it should allow any additional config (missing launch_darkly)', () => { @@ -48,7 +59,10 @@ describe('cloudExperiments config', () => { 'my-plugin.my-feature-flag': 1234, }, }; - expect(config.schema.validate(cfg, ctx)).toStrictEqual(cfg); + expect(config.schema.validate(cfg, ctx)).toStrictEqual({ + ...cfg, + metadata_refresh_interval: moment.duration(1, 'h'), + }); }); }); @@ -61,11 +75,15 @@ describe('cloudExperiments config', () => { ).toStrictEqual({ enabled: true, flag_overrides: { my_flag: 1 }, + metadata_refresh_interval: moment.duration(1, 'h'), }); }); test('in dev mode, it allows `launch_darkly` and `flag_overrides` to be empty', () => { - expect(config.schema.validate({ enabled: true }, ctx)).toStrictEqual({ enabled: true }); + expect(config.schema.validate({ enabled: true }, ctx)).toStrictEqual({ + enabled: true, + metadata_refresh_interval: moment.duration(1, 'h'), + }); }); }); @@ -98,6 +116,7 @@ describe('cloudExperiments config', () => { client_id: '1234', client_log_level: 'none', }, + metadata_refresh_interval: moment.duration(1, 'h'), }); }); @@ -126,6 +145,7 @@ describe('cloudExperiments config', () => { flag_overrides: { my_flag: 123, }, + metadata_refresh_interval: moment.duration(1, 'h'), }); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.ts index 79b49bcb77509..a5b5eeb88c2dd 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/config.ts @@ -37,6 +37,7 @@ const configSchema = schema.object({ schema.maybe(launchDarklySchema) ), flag_overrides: schema.maybe(schema.recordOf(schema.string(), schema.any())), + metadata_refresh_interval: schema.duration({ defaultValue: '1h' }), }); export type CloudExperimentsConfigType = TypeOf; @@ -45,8 +46,10 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { launch_darkly: { client_id: true, + client_log_level: true, }, flag_overrides: true, + metadata_refresh_interval: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/index.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/index.ts new file mode 100644 index 0000000000000..d298aad1ad6c1 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/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 { LaunchDarklyClient, type LaunchDarklyUserMetadata } from './launch_darkly_client'; diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.mock.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.mock.ts similarity index 97% rename from x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.mock.ts rename to x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.mock.ts index b76e629458e00..755d565e6b82f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.mock.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.mock.ts @@ -13,7 +13,6 @@ export function createLaunchDarklyClientMock(): jest.Mocked { variation: jest.fn(), allFlagsState: jest.fn(), track: jest.fn(), - identify: jest.fn(), flush: jest.fn(), } as unknown as jest.Mocked; // Using casting because we only use these APIs. No need to declare everything. } diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts new file mode 100644 index 0000000000000..cc140ea44ffdb --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts @@ -0,0 +1,211 @@ +/* + * 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 { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { ldClientMock } from './launch_darkly_client.test.mock'; +import LaunchDarkly from 'launchdarkly-node-server-sdk'; +import { LaunchDarklyClient, type LaunchDarklyClientConfig } from './launch_darkly_client'; + +describe('LaunchDarklyClient - server', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + const config: LaunchDarklyClientConfig = { + sdk_key: 'fake-sdk-key', + client_id: 'fake-client-id', + client_log_level: 'debug', + kibana_version: 'version', + }; + + describe('constructor', () => { + let launchDarklyInitSpy: jest.SpyInstance; + + beforeEach(() => { + launchDarklyInitSpy = jest.spyOn(LaunchDarkly, 'init'); + }); + + afterEach(() => { + launchDarklyInitSpy.mockRestore(); + }); + + test('it initializes the LaunchDarkly client', async () => { + const logger = loggerMock.create(); + ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock); + + const client = new LaunchDarklyClient(config, logger); + expect(launchDarklyInitSpy).toHaveBeenCalledWith('fake-sdk-key', { + application: { id: 'kibana-server', version: 'version' }, + logger: undefined, // The method basicLogger is mocked without a return value + stream: false, + }); + expect(client).toHaveProperty('launchDarklyClient', ldClientMock); + await new Promise((resolve) => process.nextTick(resolve)); // wait for the waitForInitialization resolution + expect(logger.debug).toHaveBeenCalledWith('LaunchDarkly is initialized!'); + }); + + test('it initializes the LaunchDarkly client... and handles failure', async () => { + const logger = loggerMock.create(); + ldClientMock.waitForInitialization.mockRejectedValue( + new Error('Something went terribly wrong') + ); + + const client = new LaunchDarklyClient(config, logger); + expect(launchDarklyInitSpy).toHaveBeenCalledWith('fake-sdk-key', { + application: { id: 'kibana-server', version: 'version' }, + logger: undefined, // The method basicLogger is mocked without a return value + stream: false, + }); + expect(client).toHaveProperty('launchDarklyClient', ldClientMock); + await new Promise((resolve) => process.nextTick(resolve)); // wait for the waitForInitialization resolution + expect(logger.warn).toHaveBeenCalledWith( + 'Error initializing LaunchDarkly: Error: Something went terribly wrong' + ); + }); + }); + + describe('Public APIs', () => { + let client: LaunchDarklyClient; + let logger: MockedLogger; + const testUserMetadata = { userId: 'fake-user-id', kibanaVersion: 'version' }; + + beforeEach(() => { + logger = loggerMock.create(); + ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock); + client = new LaunchDarklyClient(config, logger); + }); + + describe('updateUserMetadata', () => { + test('sets the top-level properties at the root (renaming userId to key) and the rest under `custom`', () => { + expect(client).toHaveProperty('launchDarklyUser', undefined); + + const topFields = { + name: 'First Last', + firstName: 'First', + lastName: 'Last', + email: 'first.last@boring.co', + avatar: 'fake-blue-avatar', + ip: 'my-weird-ip', + country: 'distributed', + }; + + const extraFields = { + other_field: 'my other custom field', + kibanaVersion: 'version', + }; + + client.updateUserMetadata({ userId: 'fake-user-id', ...topFields, ...extraFields }); + + expect(client).toHaveProperty('launchDarklyUser', { + key: 'fake-user-id', + ...topFields, + custom: extraFields, + }); + }); + + test('sets a minimum amount of info', () => { + expect(client).toHaveProperty('launchDarklyUser', undefined); + + client.updateUserMetadata({ userId: 'fake-user-id', kibanaVersion: 'version' }); + + expect(client).toHaveProperty('launchDarklyUser', { + key: 'fake-user-id', + custom: { kibanaVersion: 'version' }, + }); + }); + }); + + describe('getVariation', () => { + test('returns the default value if the user has not been defined', async () => { + await expect(client.getVariation('my-feature-flag', 123)).resolves.toStrictEqual(123); + expect(ldClientMock.variation).toHaveBeenCalledTimes(0); + }); + + test('calls the LaunchDarkly client when the user has been defined', async () => { + ldClientMock.variation.mockResolvedValue(1234); + client.updateUserMetadata(testUserMetadata); + await expect(client.getVariation('my-feature-flag', 123)).resolves.toStrictEqual(1234); + expect(ldClientMock.variation).toHaveBeenCalledTimes(1); + expect(ldClientMock.variation).toHaveBeenCalledWith( + 'my-feature-flag', + { key: 'fake-user-id', custom: { kibanaVersion: 'version' } }, + 123 + ); + }); + }); + + describe('reportMetric', () => { + test('does not call track if the user has not been defined', () => { + client.reportMetric('my-feature-flag', {}, 123); + expect(ldClientMock.track).toHaveBeenCalledTimes(0); + }); + + test('calls the LaunchDarkly client when the user has been defined', () => { + client.updateUserMetadata(testUserMetadata); + client.reportMetric('my-feature-flag', {}, 123); + expect(ldClientMock.track).toHaveBeenCalledTimes(1); + expect(ldClientMock.track).toHaveBeenCalledWith( + 'my-feature-flag', + { key: 'fake-user-id', custom: { kibanaVersion: 'version' } }, + {}, + 123 + ); + }); + }); + + describe('getAllFlags', () => { + test('returns the non-initialized state if the user has not been defined', async () => { + await expect(client.getAllFlags()).resolves.toStrictEqual({ + initialized: false, + flagNames: [], + flags: {}, + }); + expect(ldClientMock.allFlagsState).toHaveBeenCalledTimes(0); + }); + + test('calls the LaunchDarkly client when the user has been defined', async () => { + ldClientMock.allFlagsState.mockResolvedValue({ + valid: true, + allValues: jest.fn().mockReturnValue({ my_flag: '1234' }), + getFlagValue: jest.fn(), + getFlagReason: jest.fn(), + toJSON: jest.fn(), + }); + client.updateUserMetadata(testUserMetadata); + await expect(client.getAllFlags()).resolves.toStrictEqual({ + initialized: true, + flagNames: ['my_flag'], + flags: { my_flag: '1234' }, + }); + expect(ldClientMock.allFlagsState).toHaveBeenCalledTimes(1); + expect(ldClientMock.allFlagsState).toHaveBeenCalledWith({ + key: 'fake-user-id', + custom: { kibanaVersion: 'version' }, + }); + }); + }); + + describe('stop', () => { + test('flushes the events', async () => { + ldClientMock.flush.mockResolvedValue(); + expect(() => client.stop()).not.toThrow(); + expect(ldClientMock.flush).toHaveBeenCalledTimes(1); + await new Promise((resolve) => process.nextTick(resolve)); // wait for the flush resolution + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('handles errors when flushing events', async () => { + const err = new Error('Something went terribly wrong'); + ldClientMock.flush.mockRejectedValue(err); + expect(() => client.stop()).not.toThrow(); + expect(ldClientMock.flush).toHaveBeenCalledTimes(1); + await new Promise((resolve) => process.nextTick(resolve)); // wait for the flush resolution + expect(logger.error).toHaveBeenCalledWith(err); + }); + }); + }); +}); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts new file mode 100644 index 0000000000000..10126e6d48a46 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts @@ -0,0 +1,104 @@ +/* + * 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 LaunchDarkly, { + type LDClient, + type LDFlagSet, + type LDLogLevel, + type LDUser, +} from 'launchdarkly-node-server-sdk'; +import type { Logger } from '@kbn/core/server'; + +export interface LaunchDarklyClientConfig { + sdk_key: string; + client_id: string; + client_log_level: LDLogLevel; + kibana_version: string; +} + +export interface LaunchDarklyUserMetadata + extends Record { + userId: string; + // We are not collecting any of the above, but this is to match the LDUser first-level definition + name?: string; + firstName?: string; + lastName?: string; + email?: string; + avatar?: string; + ip?: string; + country?: string; +} + +export interface LaunchDarklyGetAllFlags { + initialized: boolean; + flags: LDFlagSet; + flagNames: string[]; +} + +export class LaunchDarklyClient { + private readonly launchDarklyClient: LDClient; + private launchDarklyUser?: LDUser; + + constructor(ldConfig: LaunchDarklyClientConfig, private readonly logger: Logger) { + this.launchDarklyClient = LaunchDarkly.init(ldConfig.sdk_key, { + application: { id: `kibana-server`, version: ldConfig.kibana_version }, + logger: LaunchDarkly.basicLogger({ level: ldConfig.client_log_level }), + // For some reason, the stream API does not work in Kibana. `.waitForInitialization()` hangs forever (doesn't throw, neither logs any errors). + // Using polling for now until we resolve that issue. + // Relevant issue: https://github.com/launchdarkly/node-server-sdk/issues/132 + stream: false, + }); + this.launchDarklyClient.waitForInitialization().then( + () => this.logger.debug('LaunchDarkly is initialized!'), + (err) => this.logger.warn(`Error initializing LaunchDarkly: ${err}`) + ); + } + + public updateUserMetadata(userMetadata: LaunchDarklyUserMetadata) { + const { userId, name, firstName, lastName, email, avatar, ip, country, ...custom } = + userMetadata; + this.launchDarklyUser = { + key: userId, + name, + firstName, + lastName, + email, + avatar, + ip, + country, + // This casting is needed because LDUser does not allow `Record` + custom: custom as Record, + }; + } + + public async getVariation(configKey: string, defaultValue: Data): Promise { + if (!this.launchDarklyUser) return defaultValue; // Skip any action if no LD User is defined + await this.launchDarklyClient.waitForInitialization(); + return await this.launchDarklyClient.variation(configKey, this.launchDarklyUser, defaultValue); + } + + public reportMetric(metricName: string, meta?: unknown, value?: number): void { + if (!this.launchDarklyUser) return; // Skip any action if no LD User is defined + this.launchDarklyClient.track(metricName, this.launchDarklyUser, meta, value); + } + + public async getAllFlags(): Promise { + if (!this.launchDarklyUser) return { initialized: false, flagNames: [], flags: {} }; + // According to the docs, this method does not send analytics back to LaunchDarkly, so it does not provide false results + const flagsState = await this.launchDarklyClient.allFlagsState(this.launchDarklyUser); + const flags = flagsState.allValues(); + return { + initialized: flagsState.valid, + flags, + flagNames: Object.keys(flags), + }; + } + + public stop() { + this.launchDarklyClient?.flush().catch((err) => this.logger.error(err)); + } +} diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/mocks.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/mocks.ts new file mode 100644 index 0000000000000..3fe1838815b27 --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/mocks.ts @@ -0,0 +1,26 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { LaunchDarklyClient } from './launch_darkly_client'; + +function createLaunchDarklyClientMock(): jest.Mocked { + const launchDarklyClientMock: jest.Mocked> = { + updateUserMetadata: jest.fn(), + getVariation: jest.fn(), + getAllFlags: jest.fn(), + reportMetric: jest.fn(), + stop: jest.fn(), + }; + + return launchDarklyClientMock as jest.Mocked; +} + +export const launchDarklyClientMocks = { + launchDarklyClientMock: createLaunchDarklyClientMock(), + createLaunchDarklyClient: createLaunchDarklyClientMock, +}; diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.ts index aa78353b72329..4d33e0ce65a7d 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.test.ts @@ -5,24 +5,28 @@ * 2.0. */ +import { fakeSchedulers } from 'rxjs-marbles/jest'; import { coreMock } from '@kbn/core/server/mocks'; import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks'; -import { ldClientMock } from './plugin.test.mock'; +import { + createIndexPatternsStartMock, + dataViewsService, +} from '@kbn/data-views-plugin/server/mocks'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { config } from './config'; import { CloudExperimentsPlugin } from './plugin'; import { FEATURE_FLAG_NAMES } from '../common/constants'; +import { LaunchDarklyClient } from './launch_darkly_client'; +jest.mock('./launch_darkly_client'); describe('Cloud Experiments server plugin', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); + jest.useFakeTimers(); - const ldUser = { - key: '1c2412b751f056aef6e340efa5637d137442d489a4b1e3117071e7c87f8523f2', - custom: { - kibanaVersion: coreMock.createPluginInitializerContext().env.packageInfo.version, - }, - }; + afterEach(() => { + jest.clearAllTimers(); + jest.clearAllMocks(); + }); describe('constructor', () => { test('successfully creates a new plugin if provided an empty configuration', () => { @@ -48,9 +52,9 @@ describe('Cloud Experiments server plugin', () => { const initializerContext = coreMock.createPluginInitializerContext({ launch_darkly: { sdk_key: 'sdk-1234' }, }); - ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock); const plugin = new CloudExperimentsPlugin(initializerContext); - expect(plugin).toHaveProperty('launchDarklyClient', ldClientMock); + expect(LaunchDarklyClient).toHaveBeenCalledTimes(1); + expect(plugin).toHaveProperty('launchDarklyClient', expect.any(LaunchDarklyClient)); }); test('it initializes the flagOverrides property', () => { @@ -67,14 +71,22 @@ describe('Cloud Experiments server plugin', () => { let plugin: CloudExperimentsPlugin; beforeEach(() => { - const initializerContext = coreMock.createPluginInitializerContext({ - launch_darkly: { sdk_key: 'sdk-1234' }, - flag_overrides: { my_flag: '1234' }, - }); - ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock); + const initializerContext = coreMock.createPluginInitializerContext( + config.schema.validate( + { + launch_darkly: { sdk_key: 'sdk-1234', client_id: 'fake-client-id' }, + flag_overrides: { my_flag: '1234' }, + }, + { dev: true } + ) + ); plugin = new CloudExperimentsPlugin(initializerContext); }); + afterEach(() => { + plugin.stop(); + }); + test('returns the contract', () => { expect( plugin.setup(coreMock.createSetup(), { @@ -93,35 +105,58 @@ describe('Cloud Experiments server plugin', () => { expect(usageCollection.registerCollector).toHaveBeenCalledTimes(1); }); - describe('identifyUser', () => { - test('sets launchDarklyUser and calls identify', () => { - expect(plugin).toHaveProperty('launchDarklyUser', undefined); + test( + 'updates the user metadata on setup', + fakeSchedulers((advance) => { plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); - expect(plugin).toHaveProperty('launchDarklyUser', ldUser); - expect(ldClientMock.identify).toHaveBeenCalledWith(ldUser); - }); - }); + const launchDarklyInstanceMock = ( + LaunchDarklyClient as jest.MockedClass + ).mock.instances[0]; + advance(100); // Remove the debounceTime effect + expect(launchDarklyInstanceMock.updateUserMetadata).toHaveBeenCalledWith({ + userId: '1c2412b751f056aef6e340efa5637d137442d489a4b1e3117071e7c87f8523f2', + kibanaVersion: coreMock.createPluginInitializerContext().env.packageInfo.version, + is_elastic_staff_owned: true, + trial_end_date: expect.any(String), + }); + }) + ); }); describe('start', () => { let plugin: CloudExperimentsPlugin; + let dataViews: jest.Mocked; + let launchDarklyInstanceMock: jest.Mocked; const firstKnownFlag = Object.keys(FEATURE_FLAG_NAMES)[0] as keyof typeof FEATURE_FLAG_NAMES; beforeEach(() => { - const initializerContext = coreMock.createPluginInitializerContext({ - launch_darkly: { sdk_key: 'sdk-1234' }, - flag_overrides: { [firstKnownFlag]: '1234' }, - }); - ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock); + jest.useRealTimers(); + const initializerContext = coreMock.createPluginInitializerContext( + config.schema.validate( + { + launch_darkly: { sdk_key: 'sdk-1234', client_id: 'fake-client-id' }, + flag_overrides: { [firstKnownFlag]: '1234' }, + }, + { dev: true } + ) + ); plugin = new CloudExperimentsPlugin(initializerContext); + dataViews = createIndexPatternsStartMock(); + launchDarklyInstanceMock = (LaunchDarklyClient as jest.MockedClass) + .mock.instances[0] as jest.Mocked; + }); + + afterEach(() => { + plugin.stop(); + jest.useFakeTimers(); }); test('returns the contract', () => { plugin.setup(coreMock.createSetup(), { cloud: cloudMock.createSetup() }); - const startContract = plugin.start(coreMock.createStart()); + const startContract = plugin.start(coreMock.createStart(), { dataViews }); expect(startContract).toStrictEqual( expect.objectContaining({ getVariation: expect.any(Function), @@ -130,8 +165,25 @@ describe('Cloud Experiments server plugin', () => { ); }); + test('triggers a userMetadataUpdate for `has_data`', async () => { + plugin.setup(coreMock.createSetup(), { + cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, + }); + dataViews.dataViewsServiceFactory.mockResolvedValue(dataViewsService); + dataViewsService.hasUserDataView.mockResolvedValue(true); + plugin.start(coreMock.createStart(), { dataViews }); + + // After scheduler kicks in... + await new Promise((resolve) => setTimeout(resolve, 200)); // Waiting for scheduler and debounceTime to complete (don't know why fakeScheduler didn't work here). + expect(launchDarklyInstanceMock.updateUserMetadata).toHaveBeenCalledWith( + expect.objectContaining({ + has_data: true, + }) + ); + }); + describe('getVariation', () => { - describe('with the user identified', () => { + describe('with the client created', () => { beforeEach(() => { plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, @@ -139,15 +191,15 @@ describe('Cloud Experiments server plugin', () => { }); test('uses the flag overrides to respond early', async () => { - const startContract = plugin.start(coreMock.createStart()); + const startContract = plugin.start(coreMock.createStart(), { dataViews }); await expect(startContract.getVariation(firstKnownFlag, 123)).resolves.toStrictEqual( '1234' ); }); test('calls the client', async () => { - const startContract = plugin.start(coreMock.createStart()); - ldClientMock.variation.mockResolvedValue('12345'); + const startContract = plugin.start(coreMock.createStart(), { dataViews }); + launchDarklyInstanceMock.getVariation.mockResolvedValue('12345'); await expect( startContract.getVariation( // @ts-expect-error We only allow existing flags in FEATURE_FLAG_NAMES @@ -155,30 +207,38 @@ describe('Cloud Experiments server plugin', () => { 123 ) ).resolves.toStrictEqual('12345'); - expect(ldClientMock.variation).toHaveBeenCalledWith( + expect(launchDarklyInstanceMock.getVariation).toHaveBeenCalledWith( undefined, // it couldn't find it in FEATURE_FLAG_NAMES - ldUser, 123 ); }); }); - describe('with the user not identified', () => { + describe('with the client not created (missing LD settings)', () => { beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext( + config.schema.validate( + { + flag_overrides: { [firstKnownFlag]: '1234' }, + }, + { dev: true } + ) + ); + plugin = new CloudExperimentsPlugin(initializerContext); plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: false }, }); }); test('uses the flag overrides to respond early', async () => { - const startContract = plugin.start(coreMock.createStart()); + const startContract = plugin.start(coreMock.createStart(), { dataViews }); await expect(startContract.getVariation(firstKnownFlag, 123)).resolves.toStrictEqual( '1234' ); }); test('returns the default value without calling the client', async () => { - const startContract = plugin.start(coreMock.createStart()); + const startContract = plugin.start(coreMock.createStart(), { dataViews }); await expect( startContract.getVariation( // @ts-expect-error We only allow existing flags in FEATURE_FLAG_NAMES @@ -186,52 +246,59 @@ describe('Cloud Experiments server plugin', () => { 123 ) ).resolves.toStrictEqual(123); - expect(ldClientMock.variation).not.toHaveBeenCalled(); }); }); }); describe('reportMetric', () => { - describe('with the user identified', () => { + describe('with the client created', () => { beforeEach(() => { plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); }); - test('calls the track API', () => { - const startContract = plugin.start(coreMock.createStart()); + test('calls LaunchDarklyClient.reportMetric', () => { + const startContract = plugin.start(coreMock.createStart(), { dataViews }); startContract.reportMetric({ // @ts-expect-error We only allow existing flags in METRIC_NAMES name: 'my-flag', meta: {}, value: 1, }); - expect(ldClientMock.track).toHaveBeenCalledWith( + expect(launchDarklyInstanceMock.reportMetric).toHaveBeenCalledWith( undefined, // it couldn't find it in METRIC_NAMES - ldUser, {}, 1 ); }); }); - describe('with the user not identified', () => { + describe('with the client not created (missing LD settings)', () => { beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext( + config.schema.validate( + { + flag_overrides: { [firstKnownFlag]: '1234' }, + }, + { dev: true } + ) + ); + plugin = new CloudExperimentsPlugin(initializerContext); plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: false }, }); }); - test('calls the track API', () => { - const startContract = plugin.start(coreMock.createStart()); + test('does not call LaunchDarklyClient.reportMetric because the client is not there', () => { + const startContract = plugin.start(coreMock.createStart(), { dataViews }); startContract.reportMetric({ // @ts-expect-error We only allow existing flags in METRIC_NAMES name: 'my-flag', meta: {}, value: 1, }); - expect(ldClientMock.track).not.toHaveBeenCalled(); + expect(plugin).toHaveProperty('launchDarklyClient', undefined); }); }); }); @@ -241,28 +308,36 @@ describe('Cloud Experiments server plugin', () => { let plugin: CloudExperimentsPlugin; beforeEach(() => { - const initializerContext = coreMock.createPluginInitializerContext({ - launch_darkly: { sdk_key: 'sdk-1234' }, - flag_overrides: { my_flag: '1234' }, - }); - ldClientMock.waitForInitialization.mockResolvedValue(ldClientMock); + const initializerContext = coreMock.createPluginInitializerContext( + config.schema.validate( + { + launch_darkly: { sdk_key: 'sdk-1234', client_id: 'fake-client-id' }, + flag_overrides: { my_flag: '1234' }, + }, + { dev: true } + ) + ); plugin = new CloudExperimentsPlugin(initializerContext); + const dataViews = createIndexPatternsStartMock(); plugin.setup(coreMock.createSetup(), { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true }, }); - plugin.start(coreMock.createStart()); + plugin.start(coreMock.createStart(), { dataViews }); }); - test('flushes the events', () => { - ldClientMock.flush.mockResolvedValue(); - expect(() => plugin.stop()).not.toThrow(); - expect(ldClientMock.flush).toHaveBeenCalledTimes(1); + test('stops the LaunchDarkly client', () => { + plugin.stop(); + const launchDarklyInstanceMock = ( + LaunchDarklyClient as jest.MockedClass + ).mock.instances[0] as jest.Mocked; + expect(launchDarklyInstanceMock.stop).toHaveBeenCalledTimes(1); }); - test('handles errors when flushing events', () => { - ldClientMock.flush.mockRejectedValue(new Error('Something went terribly wrong')); - expect(() => plugin.stop()).not.toThrow(); - expect(ldClientMock.flush).toHaveBeenCalledTimes(1); + test('stops the Metadata Service', () => { + // eslint-disable-next-line dot-notation + const metadataServiceStopSpy = jest.spyOn(plugin['metadataService'], 'stop'); + plugin.stop(); + expect(metadataServiceStopSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.ts index 782159dc12ab9..093b17934b686 100755 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/plugin.ts @@ -13,11 +13,14 @@ import type { Logger, } from '@kbn/core/server'; import { get, has } from 'lodash'; -import LaunchDarkly, { type LDClient, type LDUser } from 'launchdarkly-node-server-sdk'; import { createSHA256Hash } from '@kbn/crypto'; import type { LogMeta } from '@kbn/logging'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server/types'; +import { filter, map } from 'rxjs'; +import { MetadataService } from '../common/metadata_service'; +import { LaunchDarklyClient } from './launch_darkly_client'; import { registerUsageCollector } from './usage'; import type { CloudExperimentsConfigType } from './config'; import type { @@ -32,17 +35,26 @@ interface CloudExperimentsPluginSetupDeps { usageCollection?: UsageCollectionSetup; } +interface CloudExperimentsPluginStartDeps { + dataViews: DataViewsServerPluginStart; +} + export class CloudExperimentsPlugin implements Plugin { private readonly logger: Logger; - private readonly launchDarklyClient?: LDClient; + private readonly launchDarklyClient?: LaunchDarklyClient; private readonly flagOverrides?: Record; - private launchDarklyUser: LDUser | undefined; + private readonly metadataService: MetadataService; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); const config = initializerContext.config.get(); + + this.metadataService = new MetadataService({ + metadata_refresh_interval: config.metadata_refresh_interval, + }); + if (config.flag_overrides) { this.flagOverrides = config.flag_overrides; } @@ -55,17 +67,12 @@ export class CloudExperimentsPlugin ); } if (ldConfig) { - this.launchDarklyClient = LaunchDarkly.init(ldConfig.sdk_key, { - application: { id: `kibana-server`, version: initializerContext.env.packageInfo.version }, - logger: LaunchDarkly.basicLogger({ level: ldConfig.client_log_level }), - // For some reason, the stream API does not work in Kibana. `.waitForInitialization()` hangs forever (doesn't throw, neither logs any errors). - // Using polling for now until we resolve that issue. - // Relevant issue: https://github.com/launchdarkly/node-server-sdk/issues/132 - stream: false, - }); - this.launchDarklyClient.waitForInitialization().then( - () => this.logger.debug('LaunchDarkly is initialized!'), - (err) => this.logger.warn(`Error initializing LaunchDarkly: ${err}`) + this.launchDarklyClient = new LaunchDarklyClient( + { + ...ldConfig, + kibana_version: initializerContext.env.packageInfo.version, + }, + this.logger.get('launch_darkly') ); } } @@ -74,24 +81,33 @@ export class CloudExperimentsPlugin if (deps.usageCollection) { registerUsageCollector(deps.usageCollection, () => ({ launchDarklyClient: this.launchDarklyClient, - launchDarklyUser: this.launchDarklyUser, })); } if (deps.cloud.isCloudEnabled && deps.cloud.cloudId) { - this.launchDarklyUser = { + this.metadataService.setup({ // We use the Cloud ID as the userId in the Cloud Experiments - key: createSHA256Hash(deps.cloud.cloudId), - custom: { - // This list of deployment metadata will likely grow in future versions - kibanaVersion: this.initializerContext.env.packageInfo.version, - }, - }; - this.launchDarklyClient?.identify(this.launchDarklyUser); + userId: createSHA256Hash(deps.cloud.cloudId), + kibanaVersion: this.initializerContext.env.packageInfo.version, + trial_end_date: deps.cloud.trialEndDate?.toISOString(), + is_elastic_staff_owned: deps.cloud.isElasticStaffOwned, + }); + + // We only subscribe to the user metadata updates if Cloud is enabled. + // This way, since the user is not identified, it cannot retrieve Feature Flags from LaunchDarkly when not running on Cloud. + this.metadataService.userMetadata$ + .pipe( + filter(Boolean), // Filter out undefined + map((userMetadata) => this.launchDarklyClient?.updateUserMetadata(userMetadata)) + ) + .subscribe(); // This subscription will stop on when the metadataService stops because it completes the Observable } } - public start(core: CoreStart) { + public start(core: CoreStart, deps: CloudExperimentsPluginStartDeps) { + this.metadataService.start({ + hasDataFetcher: async () => await this.addHasDataMetadata(core, deps.dataViews), + }); return { getVariation: this.getVariation, reportMetric: this.reportMetric, @@ -99,7 +115,8 @@ export class CloudExperimentsPlugin } public stop() { - this.launchDarklyClient?.flush().catch((err) => this.logger.error(err)); + this.launchDarklyClient?.stop(); + this.metadataService.stop(); } private getVariation = async ( @@ -111,15 +128,13 @@ export class CloudExperimentsPlugin if (this.flagOverrides && has(this.flagOverrides, configKey)) { return get(this.flagOverrides, configKey, defaultValue) as Data; } - if (!this.launchDarklyUser) return defaultValue; // Skip any action if no LD User is defined - await this.launchDarklyClient?.waitForInitialization(); - return await this.launchDarklyClient?.variation(configKey, this.launchDarklyUser, defaultValue); + if (!this.launchDarklyClient) return defaultValue; + return await this.launchDarklyClient.getVariation(configKey, defaultValue); }; private reportMetric = ({ name, meta, value }: CloudExperimentsMetric): void => { const metricName = METRIC_NAMES[name]; - if (!this.launchDarklyUser) return; // Skip any action if no LD User is defined - this.launchDarklyClient?.track(metricName, this.launchDarklyUser, meta, value); + this.launchDarklyClient?.reportMetric(metricName, meta, value); this.logger.debug<{ experimentationMetric: CloudExperimentsMetric } & LogMeta>( `Reported experimentation metric ${metricName}`, { @@ -127,4 +142,19 @@ export class CloudExperimentsPlugin } ); }; + + private async addHasDataMetadata( + core: CoreStart, + dataViews: DataViewsServerPluginStart + ): Promise<{ has_data: boolean }> { + const dataViewsService = await dataViews.dataViewsServiceFactory( + core.savedObjects.createInternalRepository(), + core.elasticsearch.client.asInternalUser, + void 0, // No Kibana Request to scope the check + true // Ignore capabilities checks + ); + return { + has_data: await dataViewsService.hasUserDataView(), + }; + } } diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.test.ts index 176bbad4f6702..ab18c2dbed613 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.test.ts @@ -15,7 +15,7 @@ import { type LaunchDarklyEntitiesGetter, type Usage, } from './register_usage_collector'; -import { createLaunchDarklyClientMock } from '../plugin.test.mock'; +import { launchDarklyClientMocks } from '../launch_darkly_client/mocks'; describe('cloudExperiments usage collector', () => { let collector: Collector; @@ -34,27 +34,7 @@ describe('cloudExperiments usage collector', () => { expect(collector.isReady()).toStrictEqual(true); }); - test('should return initialized false and empty values when the user and the client are not initialized', async () => { - await expect(collector.fetch(createCollectorFetchContextMock())).resolves.toStrictEqual({ - flagNames: [], - flags: {}, - initialized: false, - }); - }); - - test('should return initialized false and empty values when the user is not initialized', async () => { - getLaunchDarklyEntitiesMock.mockReturnValueOnce({ - launchDarklyClient: createLaunchDarklyClientMock(), - }); - await expect(collector.fetch(createCollectorFetchContextMock())).resolves.toStrictEqual({ - flagNames: [], - flags: {}, - initialized: false, - }); - }); - test('should return initialized false and empty values when the client is not initialized', async () => { - getLaunchDarklyEntitiesMock.mockReturnValueOnce({ launchDarklyUser: { key: 'test' } }); await expect(collector.fetch(createCollectorFetchContextMock())).resolves.toStrictEqual({ flagNames: [], flags: {}, @@ -63,21 +43,16 @@ describe('cloudExperiments usage collector', () => { }); test('should return all the flags returned by the client', async () => { - const launchDarklyClient = createLaunchDarklyClientMock(); - getLaunchDarklyEntitiesMock.mockReturnValueOnce({ - launchDarklyClient, - launchDarklyUser: { key: 'test' }, - }); + const launchDarklyClient = launchDarklyClientMocks.createLaunchDarklyClient(); + getLaunchDarklyEntitiesMock.mockReturnValueOnce({ launchDarklyClient }); - launchDarklyClient.allFlagsState.mockResolvedValueOnce({ - valid: true, - getFlagValue: jest.fn(), - getFlagReason: jest.fn(), - toJSON: jest.fn(), - allValues: jest.fn().mockReturnValueOnce({ + launchDarklyClient.getAllFlags.mockResolvedValueOnce({ + initialized: true, + flags: { 'my-plugin.my-feature-flag': true, 'my-plugin.my-other-feature-flag': 22, - }), + }, + flagNames: ['my-plugin.my-feature-flag', 'my-plugin.my-other-feature-flag'], }); await expect(collector.fetch(createCollectorFetchContextMock())).resolves.toStrictEqual({ diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.ts index c9390915fd8c2..9a800dabe9fc8 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/usage/register_usage_collector.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { LDClient, LDUser } from 'launchdarkly-node-server-sdk'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import type { LaunchDarklyClient } from '../launch_darkly_client'; export interface Usage { initialized: boolean; @@ -15,8 +15,7 @@ export interface Usage { } export type LaunchDarklyEntitiesGetter = () => { - launchDarklyUser?: LDUser; - launchDarklyClient?: LDClient; + launchDarklyClient?: LaunchDarklyClient; }; export function registerUsageCollector( @@ -53,17 +52,9 @@ export function registerUsageCollector( }, }, fetch: async () => { - const { launchDarklyUser, launchDarklyClient } = getLaunchDarklyEntities(); - if (!launchDarklyUser || !launchDarklyClient) - return { initialized: false, flagNames: [], flags: {} }; - // According to the docs, this method does not send analytics back to LaunchDarkly, so it does not provide false results - const flagsState = await launchDarklyClient.allFlagsState(launchDarklyUser); - const flags = flagsState.allValues(); - return { - initialized: flagsState.valid, - flags, - flagNames: Object.keys(flags), - }; + const { launchDarklyClient } = getLaunchDarklyEntities(); + if (!launchDarklyClient) return { initialized: false, flagNames: [], flags: {} }; + return await launchDarklyClient.getAllFlags(); }, }) ); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/tsconfig.json b/x-pack/plugins/cloud_integrations/cloud_experiments/tsconfig.json index 917c8c0c9fc53..7f0e98957c5f7 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/tsconfig.json +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/tsconfig.json @@ -15,6 +15,7 @@ ], "references": [ { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data_views/tsconfig.json" }, { "path": "../../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../cloud/tsconfig.json" }, ] diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 890e4c8a5e4f1..7b850a44528fd 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4970,6 +4970,15 @@ "properties": { "isCloudEnabled": { "type": "boolean" + }, + "trialEndDate": { + "type": "date" + }, + "inTrial": { + "type": "boolean" + }, + "isElasticStaffOwned": { + "type": "boolean" } } }, From 02d639a5c9f1d4848929df7a37e9372beba0e93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 17 Oct 2022 17:45:47 +0200 Subject: [PATCH 10/74] [Enterprise Search] Reorder pipeline cards to match the execution order (#143384) * Reorder pipeline cards to match the execution order Also adds some small refactoring and tests for relevant cards. * Add changes that are gone by conflict --- .../__mocks__/pipeline.mock.ts | 15 ++ .../custom_pipeline_item.test.tsx | 45 ++++++ .../custom_pipeline_item.tsx} | 4 +- .../default_pipeline_item.test.tsx | 87 +++++++++++ .../default_pipeline_item.tsx | 89 +++++++++++ .../ingest_pipeline_modal.tsx | 14 +- .../ingest_pipelines_card.tsx | 88 +++++++++++ .../pipelines/ingest_pipelines_card.tsx | 141 ------------------ .../ml_inference_pipeline_processors_card.tsx | 2 +- .../search_index/pipelines/pipelines.tsx | 2 +- 10 files changed, 333 insertions(+), 154 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/pipeline.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.test.tsx rename x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/{custom_pipeline_panel.tsx => ingest_pipelines/custom_pipeline_item.tsx} (95%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx rename x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/{ => ingest_pipelines}/ingest_pipeline_modal.tsx (96%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/pipeline.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/pipeline.mock.ts new file mode 100644 index 0000000000000..55b21f2e4c753 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/pipeline.mock.ts @@ -0,0 +1,15 @@ +/* + * 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 { IngestPipelineParams } from '../../../../common/types/connectors'; + +export const mockPipelineState: IngestPipelineParams = { + extract_binary_content: true, + name: 'pipeline-name', + reduce_whitespace: true, + run_ml_inference: true, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.test.tsx new file mode 100644 index 0000000000000..1aa74f8957670 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; + +import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers'; + +import { CustomPipelineItem } from './custom_pipeline_item'; + +describe('CustomPipelineItem', () => { + it('renders custom pipeline item', () => { + const indexName = 'fake-index-name'; + const pipelineSuffix = 'custom-pipeline'; + const ingestionMethod = 'crawler'; + const processorsCount = 12; + + const wrapper = shallow( + + ); + const title = wrapper.find('h4').text(); + const editLink = wrapper.find(EuiButtonEmptyTo).prop('to'); + const trackLink = wrapper.find(EuiButtonEmptyTo).prop('data-telemetry-id'); + const processorCountBadge = wrapper.find(EuiBadge).render().text(); + + expect(title).toEqual(`${indexName}@${pipelineSuffix}`); + expect(editLink.endsWith(`${indexName}@${pipelineSuffix}`)).toBe(true); + expect(trackLink).toEqual( + `entSearchContent-${ingestionMethod}-pipelines-customPipeline-editPipeline` + ); + expect(processorCountBadge).toEqual(`${processorsCount} Processors`); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/custom_pipeline_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.tsx similarity index 95% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/custom_pipeline_panel.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.tsx index 0a15e03eb2326..54471840aff4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/custom_pipeline_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/custom_pipeline_item.tsx @@ -11,9 +11,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiBadge } from '@elastic import { i18n } from '@kbn/i18n'; -import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; +import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers'; -export const CustomPipelinePanel: React.FC<{ +export const CustomPipelineItem: React.FC<{ indexName: string; ingestionMethod: string; pipelineSuffix: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.test.tsx new file mode 100644 index 0000000000000..29bcd0b03dbd3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.test.tsx @@ -0,0 +1,87 @@ +/* + * 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 { mockPipelineState } from '../../../../__mocks__/pipeline.mock'; +import { indices } from '../../../../__mocks__/search_indices.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBadge, EuiButtonEmpty } from '@elastic/eui'; + +import { CurlRequest } from '../../components/curl_request/curl_request'; + +import { DefaultPipelineItem } from './default_pipeline_item'; + +describe('DefaultPipelineItem', () => { + it('renders default pipeline item for ingestion indices', () => { + const index = indices[1]; + const mockOpenModal = jest.fn(); + const ingestionMethod = 'connector'; + const wrapper = shallow( + + ); + + const title = wrapper.find('h4').text(); + const settingsButton = wrapper.find(EuiButtonEmpty); + const curlRequest = wrapper.find(CurlRequest); + const badge = wrapper.find(EuiBadge); + + expect(title).toEqual(mockPipelineState.name); + expect(settingsButton.prop('data-telemetry-id')).toEqual( + `entSearchContent-${ingestionMethod}-pipelines-ingestPipelines-settings` + ); + settingsButton.simulate('click'); + expect(mockOpenModal).toHaveBeenCalledTimes(1); + expect(curlRequest.length).toEqual(0); + expect(badge.render().text()).toEqual('Managed'); + }); + + it('renders default pipeline item for api indices', () => { + const index = indices[0]; + const mockOpenModal = jest.fn(); + const ingestionMethod = 'api'; + const wrapper = shallow( + + ); + + const title = wrapper.find('h4').text(); + const settingsButton = wrapper.find(EuiButtonEmpty); + const curlRequest = wrapper.find(CurlRequest); + const badge = wrapper.find(EuiBadge); + + expect(title).toEqual(mockPipelineState.name); + expect(settingsButton.prop('data-telemetry-id')).toEqual( + `entSearchContent-${ingestionMethod}-pipelines-ingestPipelines-settings` + ); + settingsButton.simulate('click'); + expect(mockOpenModal).toHaveBeenCalledTimes(1); + expect(curlRequest.prop('document')).toBeDefined(); + expect(curlRequest.prop('indexName')).toEqual(index.name); + expect(curlRequest.prop('pipeline')).toEqual({ + ...mockPipelineState, + name: mockPipelineState.name, + }); + + expect(badge.render().text()).toEqual('Managed'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx new file mode 100644 index 0000000000000..c22f7910bdc4f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx @@ -0,0 +1,89 @@ +/* + * 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 { + EuiAccordion, + EuiBadge, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { IngestPipelineParams } from '../../../../../../../common/types/connectors'; +import { ElasticsearchIndexWithIngestion } from '../../../../../../../common/types/indices'; + +import { isApiIndex } from '../../../../utils/indices'; +import { CurlRequest } from '../../components/curl_request/curl_request'; + +export const DefaultPipelineItem: React.FC<{ + index: ElasticsearchIndexWithIngestion; + indexName: string; + ingestionMethod: string; + openModal: () => void; + pipelineName: string; + pipelineState: IngestPipelineParams; +}> = ({ index, indexName, ingestionMethod, openModal, pipelineName, pipelineState }) => { + return ( + + + + + +

{pipelineName}

+
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.ingestPipelinesCard.settings.label', + { defaultMessage: 'Settings' } + )} + + +
+
+ + + {isApiIndex(index) && ( + + + + + + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.ingestPipelinesCard.managedBadge.label', + { defaultMessage: 'Managed' } + )} + + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipeline_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx similarity index 96% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipeline_modal.tsx rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx index 34cb59e734d70..6be65b6387d49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipeline_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx @@ -27,13 +27,13 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DEFAULT_PIPELINE_NAME } from '../../../../../../common/constants'; +import { DEFAULT_PIPELINE_NAME } from '../../../../../../../common/constants'; -import { IngestPipelineParams } from '../../../../../../common/types/connectors'; +import { IngestPipelineParams } from '../../../../../../../common/types/connectors'; -import { CurlRequest } from '../components/curl_request/curl_request'; +import { CurlRequest } from '../../components/curl_request/curl_request'; -import { PipelineSettingsForm } from './pipeline_settings_form'; +import { PipelineSettingsForm } from '../pipeline_settings_form'; interface IngestPipelineModalProps { closeModal: () => void; @@ -46,7 +46,6 @@ interface IngestPipelineModalProps { pipeline: IngestPipelineParams; savePipeline: () => void; setPipeline: (pipeline: IngestPipelineParams) => void; - showModal: boolean; } export const IngestPipelineModal: React.FC = ({ @@ -60,14 +59,13 @@ export const IngestPipelineModal: React.FC = ({ pipeline, savePipeline, setPipeline, - showModal, }) => { const { name } = pipeline; // can't customize if you already have a custom pipeline! const canCustomize = name === DEFAULT_PIPELINE_NAME; - return showModal ? ( + return ( @@ -260,7 +258,5 @@ export const IngestPipelineModal: React.FC = ({ )} - ) : ( - <> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx new file mode 100644 index 0000000000000..7406d9e89150b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { KibanaLogic } from '../../../../../shared/kibana'; + +import { LicensingLogic } from '../../../../../shared/licensing'; +import { CreateCustomPipelineApiLogic } from '../../../../api/index/create_custom_pipeline_api_logic'; +import { FetchCustomPipelineApiLogic } from '../../../../api/index/fetch_custom_pipeline_api_logic'; +import { IndexViewLogic } from '../../index_view_logic'; + +import { PipelinesLogic } from '../pipelines_logic'; + +import { CustomPipelineItem } from './custom_pipeline_item'; +import { DefaultPipelineItem } from './default_pipeline_item'; +import { IngestPipelineModal } from './ingest_pipeline_modal'; + +export const IngestPipelinesCard: React.FC = () => { + const { indexName, ingestionMethod } = useValues(IndexViewLogic); + + const { canSetPipeline, index, pipelineName, pipelineState, showModal } = + useValues(PipelinesLogic); + const { closeModal, openModal, setPipelineState, savePipeline } = useActions(PipelinesLogic); + const { makeRequest: fetchCustomPipeline } = useActions(FetchCustomPipelineApiLogic); + const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic); + const { data: customPipelines } = useValues(FetchCustomPipelineApiLogic); + const { isCloud } = useValues(KibanaLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const isGated = !isCloud && !hasPlatinumLicense; + const customPipeline = customPipelines ? customPipelines[`${indexName}@custom`] : undefined; + + useEffect(() => { + fetchCustomPipeline({ indexName }); + }, [indexName]); + + return ( + + {showModal && ( + createCustomPipeline({ indexName })} + displayOnly={!canSetPipeline} + indexName={indexName} + ingestionMethod={ingestionMethod} + isGated={isGated} + isLoading={false} + pipeline={{ ...pipelineState, name: pipelineName }} + savePipeline={savePipeline} + setPipeline={setPipelineState} + /> + )} + + + + + + {customPipeline && ( + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx deleted file mode 100644 index 711d83f11bdea..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx +++ /dev/null @@ -1,141 +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 { useActions, useValues } from 'kea'; - -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiButtonEmpty, - EuiAccordion, - EuiBadge, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { KibanaLogic } from '../../../../shared/kibana'; - -import { LicensingLogic } from '../../../../shared/licensing'; -import { CreateCustomPipelineApiLogic } from '../../../api/index/create_custom_pipeline_api_logic'; -import { FetchCustomPipelineApiLogic } from '../../../api/index/fetch_custom_pipeline_api_logic'; -import { isApiIndex } from '../../../utils/indices'; -import { CurlRequest } from '../components/curl_request/curl_request'; -import { IndexViewLogic } from '../index_view_logic'; - -import { CustomPipelinePanel } from './custom_pipeline_panel'; -import { IngestPipelineModal } from './ingest_pipeline_modal'; -import { PipelinesLogic } from './pipelines_logic'; - -export const IngestPipelinesCard: React.FC = () => { - const { indexName, ingestionMethod } = useValues(IndexViewLogic); - - const { canSetPipeline, index, pipelineName, pipelineState, showModal } = - useValues(PipelinesLogic); - const { closeModal, openModal, setPipelineState, savePipeline } = useActions(PipelinesLogic); - const { makeRequest: fetchCustomPipeline } = useActions(FetchCustomPipelineApiLogic); - const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic); - const { data: customPipelines } = useValues(FetchCustomPipelineApiLogic); - const { isCloud } = useValues(KibanaLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); - - const isGated = !isCloud && !hasPlatinumLicense; - const customPipeline = customPipelines ? customPipelines[`${indexName}@custom`] : undefined; - - useEffect(() => { - fetchCustomPipeline({ indexName }); - }, [indexName]); - - return ( - - createCustomPipeline({ indexName })} - displayOnly={!canSetPipeline} - indexName={indexName} - ingestionMethod={ingestionMethod} - isGated={isGated} - isLoading={false} - pipeline={{ ...pipelineState, name: pipelineName }} - savePipeline={savePipeline} - setPipeline={setPipelineState} - showModal={showModal} - /> - {customPipeline && ( - - - - - - )} - - - - - - - -

{pipelineName}

-
-
- - - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.ingestPipelinesCard.settings.label', - { defaultMessage: 'Settings' } - )} - - -
-
- - - {isApiIndex(index) && ( - - - - - - )} - - - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.ingestPipelinesCard.managedBadge.label', - { defaultMessage: 'Managed' } - )} - - - - -
-
-
-
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference_pipeline_processors_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference_pipeline_processors_card.tsx index 28e37603435e1..4c87086b5a7b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference_pipeline_processors_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference_pipeline_processors_card.tsx @@ -31,7 +31,7 @@ export const MlInferencePipelineProcessorsCard: React.FC = () => { return ( {inferencePipelines.map((item: InferencePipeline, index: number) => ( - + ))} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx index 4da033792a17b..0deb89a378a04 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx @@ -27,7 +27,7 @@ import { isApiIndex } from '../../../utils/indices'; import { InferenceErrors } from './inference_errors'; import { InferenceHistory } from './inference_history'; -import { IngestPipelinesCard } from './ingest_pipelines_card'; +import { IngestPipelinesCard } from './ingest_pipelines/ingest_pipelines_card'; import { AddMLInferencePipelineButton } from './ml_inference/add_ml_inference_button'; import { AddMLInferencePipelineModal } from './ml_inference/add_ml_inference_pipeline_modal'; import { MlInferencePipelineProcessorsCard } from './ml_inference_pipeline_processors_card'; From 2d48eef8aa3ee7fda4a0cf88fd0928cc4e3b7dcc Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 17 Oct 2022 11:47:15 -0400 Subject: [PATCH 11/74] [Synthetics] add project monitor GET route (#143123) * synthetics - add project monitor GET route * update tests * adjust tests * make hash not required * add monitor id * update saved_object snapshot * Update x-pack/plugins/synthetics/server/routes/common.ts Co-authored-by: Shahzad * adjust project monitors response body * adjust types * add config_hash to ui keys to skip * update types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../migrations/check_registered_types.test.ts | 2 +- .../common/constants/monitor_defaults.ts | 1 + .../common/constants/monitor_management.ts | 1 + .../synthetics/common/constants/rest_api.ts | 3 +- .../common/formatters/common/formatters.ts | 1 + .../monitor_management/monitor_types.ts | 1 + .../monitor_types_project.ts | 20 + .../monitor_add_edit/form/formatter.test.tsx | 5 + .../fleet_package/common/normalizers.ts | 1 + .../lib/saved_objects/synthetics_monitor.ts | 3 + .../synthetics/server/routes/common.ts | 6 + .../plugins/synthetics/server/routes/index.ts | 2 + .../monitor_cruds/add_monitor_project.ts | 2 +- .../monitor_cruds/get_monitor_project.ts | 67 +++ .../synthetics_service/formatters/common.ts | 1 + .../formatters/format_configs.ts | 1 + .../normalizers/browser_monitor.test.ts | 7 + .../normalizers/common_fields.ts | 1 + .../normalizers/http_monitor.test.ts | 15 +- .../normalizers/icmp_monitor.test.ts | 18 +- .../normalizers/tcp_monitor.test.ts | 18 +- .../project_monitor_formatter.test.ts | 9 +- .../apis/uptime/rest/add_monitor_project.ts | 67 +-- .../uptime/rest/fixtures/browser_monitor.json | 3 +- .../uptime/rest/fixtures/http_monitor.json | 3 +- .../uptime/rest/fixtures/icmp_monitor.json | 3 +- .../fixtures/project_browser_monitor.json | 3 +- .../rest/fixtures/project_http_monitor.json | 6 +- .../rest/fixtures/project_icmp_monitor.json | 9 +- .../rest/fixtures/project_tcp_monitor.json | 9 +- .../uptime/rest/fixtures/tcp_monitor.json | 3 +- .../apis/uptime/rest/get_monitor_project.ts | 440 ++++++++++++++++++ .../api_integration/apis/uptime/rest/index.ts | 1 + 33 files changed, 682 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor_project.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/get_monitor_project.ts diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index f6591cf7a7759..7dce76528a5a0 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -130,7 +130,7 @@ describe('checking migration metadata changes on all registered SO types', () => "siem-ui-timeline-pinned-event": "e2697b38751506c7fce6e8b7207a830483dc4283", "space": "c4a0acce1bd4b9cce85154f2a350624a53111c59", "spaces-usage-stats": "922d3235bbf519e3fb3b260e27248b1df8249b79", - "synthetics-monitor": "cffb4dfe9e0a36755a226d5cf983c21aac2b5b1e", + "synthetics-monitor": "7bebb6511c70359386f9e20c982db86259c7915c", "synthetics-privates-locations": "dd00385f4a27ef062c3e57312eeb3799872fa4af", "tag": "39413f4578cc2128c9a0fda97d0acd1c8862c47a", "task": "ef53d0f070bd54957b8fe22fae3b1ff208913f76", diff --git a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts index ff2aefa12d94d..9f40d49d7086b 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts @@ -47,6 +47,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = { [ConfigKey.NAMESPACE]: DEFAULT_NAMESPACE_STRING, [ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.UI, [ConfigKey.JOURNEY_ID]: '', + [ConfigKey.CONFIG_HASH]: '', // Deprecated, slated to be removed in a future version [ConfigKey.ID]: '', diff --git a/x-pack/plugins/synthetics/common/constants/monitor_management.ts b/x-pack/plugins/synthetics/common/constants/monitor_management.ts index da3f138091723..bdf5dc6548f20 100644 --- a/x-pack/plugins/synthetics/common/constants/monitor_management.ts +++ b/x-pack/plugins/synthetics/common/constants/monitor_management.ts @@ -10,6 +10,7 @@ export enum ConfigKey { APM_SERVICE_NAME = 'service.name', CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id', CONFIG_ID = 'config_id', + CONFIG_HASH = 'hash', ENABLED = 'enabled', FORM_MONITOR_TYPE = 'form_monitor_type', HOSTS = 'hosts', diff --git a/x-pack/plugins/synthetics/common/constants/rest_api.ts b/x-pack/plugins/synthetics/common/constants/rest_api.ts index dad434194f21d..0f43ff654c77e 100644 --- a/x-pack/plugins/synthetics/common/constants/rest_api.ts +++ b/x-pack/plugins/synthetics/common/constants/rest_api.ts @@ -48,5 +48,6 @@ export enum API_URLS { SYNTHETICS_HAS_ZIP_URL_MONITORS = '/internal/uptime/fleet/has_zip_url_monitors', // Project monitor public endpoint - SYNTHETICS_MONITORS_PROJECT = '/api/synthetics/service/project/monitors', + SYNTHETICS_MONITORS_PROJECT_LEGACY = '/api/synthetics/service/project/monitors', + SYNTHETICS_MONITORS_PROJECT = '/api/synthetics/project/{projectName}/monitors', } diff --git a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts index 882182b2fe07f..ab7c3bc9cfa29 100644 --- a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts @@ -32,6 +32,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.PROJECT_ID]: null, [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, [ConfigKey.ORIGINAL_SPACE]: null, + [ConfigKey.CONFIG_HASH]: null, // Deprecated, slated to be removed in a later release [ConfigKey.ID]: null, diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 163041986f1a3..78bca7a681461 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -82,6 +82,7 @@ export const CommonFieldsCodec = t.intersection([ [ConfigKey.REVISION]: t.number, [ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec, [ConfigKey.CONFIG_ID]: t.string, + [ConfigKey.CONFIG_HASH]: t.string, [ConfigKey.JOURNEY_ID]: t.string, [ConfigKey.PROJECT_ID]: t.string, [ConfigKey.ORIGINAL_SPACE]: t.string, diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts index cba2f506189e6..ef5f6a23b4413 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts @@ -40,6 +40,7 @@ export const ProjectMonitorCodec = t.intersection([ hosts: t.union([t.string, t.array(t.string)]), max_redirects: t.string, wait: t.string, + hash: t.string, }), ]); @@ -49,8 +50,27 @@ export const ProjectMonitorsRequestCodec = t.interface({ monitors: t.array(ProjectMonitorCodec), }); +export const ProjectMonitorMetaDataCodec = t.interface({ + hash: t.string, + journey_id: t.string, +}); + +export const ProjectMonitorsResponseCodec = t.intersection([ + t.interface({ + total: t.number, + monitors: t.array(ProjectMonitorMetaDataCodec), + }), + t.partial({ + after_key: t.string, + }), +]); + export type ProjectMonitorThrottlingConfig = t.TypeOf; export type ProjectMonitor = t.TypeOf; export type ProjectMonitorsRequest = t.TypeOf; + +export type ProjectMonitorsResponse = t.TypeOf; + +export type ProjectMonitorMetaData = t.TypeOf; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx index 2903a0375d038..d13213766c3db 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx @@ -6,6 +6,8 @@ */ import { format } from './formatter'; +import { DataStream } from '../../../../../../common/runtime_types'; +import { DEFAULT_FIELDS } from '../../../../../../common/constants/monitor_defaults'; describe('format', () => { let formValues: Record; @@ -88,6 +90,7 @@ describe('format', () => { it.each([[true], [false]])('correctly formats form fields to monitor type', (enabled) => { formValues.enabled = enabled; expect(format(formValues)).toEqual({ + ...DEFAULT_FIELDS[DataStream.HTTP], __ui: { is_tls_enabled: false, }, @@ -223,6 +226,7 @@ describe('format', () => { }, }; expect(format(browserFormFields)).toEqual({ + ...DEFAULT_FIELDS[DataStream.BROWSER], __ui: { script_source: { file_name: fileName, @@ -304,6 +308,7 @@ describe('format', () => { }, }) ).toEqual({ + ...DEFAULT_FIELDS[DataStream.HTTP], __ui: { is_tls_enabled: isTLSEnabled, }, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts index 31ba9784e22a4..f4678b5569872 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts @@ -96,6 +96,7 @@ export const commonNormalizers: CommonNormalizerMap = { [ConfigKey.PROJECT_ID]: getCommonNormalizer(ConfigKey.PROJECT_ID), [ConfigKey.CUSTOM_HEARTBEAT_ID]: getCommonNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID), [ConfigKey.ORIGINAL_SPACE]: getCommonNormalizer(ConfigKey.ORIGINAL_SPACE), + [ConfigKey.CONFIG_HASH]: getCommonNormalizer(ConfigKey.CONFIG_HASH), // Deprecated, slated to be removed in a future release [ConfigKey.ID]: getCommonNormalizer(ConfigKey.ID), diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts index 52be898373ef2..94a8a0085e170 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts @@ -54,6 +54,9 @@ export const syntheticsMonitor: SavedObjectsType = { origin: { type: 'keyword', }, + hash: { + type: 'keyword', + }, locations: { properties: { id: { diff --git a/x-pack/plugins/synthetics/server/routes/common.ts b/x-pack/plugins/synthetics/server/routes/common.ts index bdf06c8373af3..84a1a294f27e6 100644 --- a/x-pack/plugins/synthetics/server/routes/common.ts +++ b/x-pack/plugins/synthetics/server/routes/common.ts @@ -23,6 +23,8 @@ const querySchema = schema.object({ monitorType: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), locations: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), status: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + fields: schema.maybe(schema.arrayOf(schema.string())), + searchAfter: schema.maybe(schema.arrayOf(schema.string())), }); type MonitorsQuery = TypeOf; @@ -42,6 +44,8 @@ export const getMonitors = ( monitorType, locations, filter = '', + fields, + searchAfter, } = request as MonitorsQuery; const locationFilter = parseLocationFilter(syntheticsService.locations, locations); @@ -60,6 +64,8 @@ export const getMonitors = ( searchFields: ['name', 'tags.text', 'locations.id.text', 'urls'], search: query ? `${query}*` : undefined, filter: filters + filter, + fields, + searchAfter, }); }; diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index 63a2a0b889fe5..8cff5dc88c918 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -18,6 +18,7 @@ import { getSyntheticsMonitorOverviewRoute, getSyntheticsMonitorRoute, } from './monitor_cruds/get_monitor'; +import { getSyntheticsProjectMonitorsRoute } from './monitor_cruds/get_monitor_project'; import { runOnceSyntheticsMonitorRoute } from './synthetics_service/run_once_monitor'; import { getServiceAllowedRoute } from './synthetics_service/get_service_allowed'; import { testNowMonitorRoute } from './synthetics_service/test_now_monitor'; @@ -42,6 +43,7 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ enableSyntheticsRoute, getServiceLocationsRoute, getSyntheticsMonitorRoute, + getSyntheticsProjectMonitorsRoute, getAllSyntheticsMonitorRoute, getSyntheticsMonitorOverviewRoute, installIndexTemplatesRoute, diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts index ea269d87413e7..533690de30bed 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts @@ -19,7 +19,7 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory = libs: UMServerLibs ) => ({ method: 'PUT', - path: API_URLS.SYNTHETICS_MONITORS_PROJECT, + path: API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY, validate: { body: schema.object({ project: schema.string(), diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor_project.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor_project.ts new file mode 100644 index 0000000000000..fc965b94cb096 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor_project.ts @@ -0,0 +1,67 @@ +/* + * 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'; +import { ConfigKey } from '../../../common/runtime_types'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; +import { API_URLS } from '../../../common/constants'; +import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor'; +import { getMonitors } from '../common'; + +const querySchema = schema.object({ + search_after: schema.maybe(schema.string()), + per_page: schema.maybe(schema.number()), +}); + +export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory = () => ({ + method: 'GET', + path: API_URLS.SYNTHETICS_MONITORS_PROJECT, + validate: { + params: schema.object({ + projectName: schema.string(), + }), + query: querySchema, + }, + handler: async ({ + request, + response, + server: { logger }, + savedObjectsClient, + syntheticsMonitorClient, + }): Promise => { + const { projectName } = request.params; + const { per_page: perPage = 500, search_after: searchAfter } = request.query; + const decodedProjectName = decodeURI(projectName); + const decodedSearchAfter = searchAfter ? decodeURI(searchAfter) : undefined; + + try { + const { saved_objects: monitors, total } = await getMonitors( + { + filter: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${decodedProjectName}"`, + fields: [ConfigKey.JOURNEY_ID, ConfigKey.CONFIG_HASH], + perPage, + sortField: ConfigKey.JOURNEY_ID, + sortOrder: 'asc', + searchAfter: decodedSearchAfter ? [...decodedSearchAfter.split(',')] : undefined, + }, + syntheticsMonitorClient.syntheticsService, + savedObjectsClient + ); + const projectMonitors = monitors.map((monitor) => ({ + journey_id: monitor.attributes[ConfigKey.JOURNEY_ID], + hash: monitor.attributes[ConfigKey.CONFIG_HASH] || '', + })); + + return { + total, + after_key: monitors.length ? monitors[monitors.length - 1].sort?.join(',') : null, + monitors: projectMonitors, + }; + } catch (error) { + logger.error(error); + } + }, +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts index 5e9f35f030bd8..f88f23ca38037 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts @@ -34,6 +34,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.PROJECT_ID]: null, [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, [ConfigKey.ORIGINAL_SPACE]: null, + [ConfigKey.CONFIG_HASH]: null, // Deprecated, slated to be removed in a later releae [ConfigKey.ID]: null, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts index a1c1d68c4a829..320d3c28b48d3 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts @@ -27,6 +27,7 @@ const UI_KEYS_TO_SKIP = [ ConfigKey.CUSTOM_HEARTBEAT_ID, ConfigKey.FORM_MONITOR_TYPE, ConfigKey.TEXT_ASSERTION, + ConfigKey.CONFIG_HASH, 'secrets', ]; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts index 3133e98b57935..a11a990abaa56 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts @@ -18,6 +18,7 @@ import { normalizeProjectMonitors } from '.'; describe('browser normalizers', () => { describe('normalize push monitors', () => { + const testHash = 'ljlkj'; const playwrightOptions = { headless: true, }; @@ -67,6 +68,7 @@ describe('browser normalizers', () => { locations: ['us_central'], tags: ['tag1', 'tag2'], ignoreHTTPSErrors: true, + hash: testHash, } as ProjectMonitor, // test that normalizers defaults to browser when type is omitted { id: 'test-id-2', @@ -85,6 +87,7 @@ describe('browser normalizers', () => { tags: ['tag3', 'tag4'], ignoreHTTPSErrors: false, type: DataStream.BROWSER, + hash: testHash, }, { id: 'test-id-3', @@ -104,6 +107,7 @@ describe('browser normalizers', () => { tags: ['tag3', 'tag4'], ignoreHTTPSErrors: false, type: DataStream.BROWSER, + hash: testHash, }, ]; @@ -158,6 +162,7 @@ describe('browser normalizers', () => { custom_heartbeat_id: 'test-id-1-test-project-id-test-space', timeout: null, id: '', + hash: testHash, }, unsupportedKeys: [], errors: [], @@ -215,6 +220,7 @@ describe('browser normalizers', () => { custom_heartbeat_id: 'test-id-2-test-project-id-test-space', timeout: null, id: '', + hash: testHash, }, unsupportedKeys: [], errors: [], @@ -279,6 +285,7 @@ describe('browser normalizers', () => { custom_heartbeat_id: 'test-id-3-test-project-id-test-space', timeout: null, id: '', + hash: testHash, }, unsupportedKeys: [], errors: [], diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index 56045712606a1..564de1c1c92d6 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -73,6 +73,7 @@ export const getNormalizeCommonFields = ({ [ConfigKey.TIMEOUT]: monitor.timeout ? getValueInSeconds(monitor.timeout) : defaultFields[ConfigKey.TIMEOUT], + [ConfigKey.CONFIG_HASH]: monitor.hash || defaultFields[ConfigKey.CONFIG_HASH], }; return normalizedFields; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts index 055c375bfb3b3..e80972f0a0c3b 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts @@ -5,10 +5,17 @@ * 2.0. */ -import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types'; +import { + DataStream, + Locations, + LocationStatus, + PrivateLocation, +} from '../../../../common/runtime_types'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; import { normalizeProjectMonitors } from '.'; describe('http normalizers', () => { + const testHash = 'ljlkj'; describe('normalize push monitors', () => { const projectId = 'test-project-id'; const locations: Locations = [ @@ -74,6 +81,7 @@ describe('http normalizers', () => { ssl: { supported_protocols: ['TLSv1.2', 'TLSv1.3'], }, + hash: testHash, }, { locations: ['localhost'], @@ -104,6 +112,7 @@ describe('http normalizers', () => { }, 'service.name': 'test service', 'ssl.supported_protocols': 'TLSv1.2,TLSv1.3', + hash: testHash, }, ]; @@ -133,6 +142,7 @@ describe('http normalizers', () => { }, ], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.HTTP], __ui: { is_tls_enabled: false, }, @@ -182,12 +192,14 @@ describe('http normalizers', () => { 'url.port': null, username: '', id: '', + hash: testHash, }, unsupportedKeys: ['check.response.body', 'unsupportedKey.nestedUnsupportedKey'], }, { errors: [], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.HTTP], __ui: { is_tls_enabled: false, }, @@ -237,6 +249,7 @@ describe('http normalizers', () => { 'url.port': null, username: '', id: '', + hash: testHash, }, unsupportedKeys: [], }, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts index 17bdd9e8ca24e..3c1d9209ff1bb 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts @@ -5,10 +5,17 @@ * 2.0. */ -import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types'; +import { + DataStream, + Locations, + LocationStatus, + PrivateLocation, +} from '../../../../common/runtime_types'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; import { normalizeProjectMonitors } from '.'; describe('icmp normalizers', () => { + const testHash = 'ljlkj'; describe('normalize push monitors', () => { const projectId = 'test-project-id'; const locations: Locations = [ @@ -51,6 +58,7 @@ describe('icmp normalizers', () => { timeout: '1m', wait: '30s', 'service.name': 'test service', + hash: testHash, }, { locations: ['us_central'], @@ -65,6 +73,7 @@ describe('icmp normalizers', () => { service: { name: 'test service', }, + hash: testHash, }, { locations: ['us_central'], @@ -78,6 +87,7 @@ describe('icmp normalizers', () => { unsupportedKey: { nestedUnsupportedKey: 'unnsuportedValue', }, + hash: testHash, }, ]; @@ -94,6 +104,7 @@ describe('icmp normalizers', () => { { errors: [], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.ICMP], config_id: '', custom_heartbeat_id: 'Cloudflare-DNS-test-project-id-test-space', enabled: true, @@ -128,12 +139,14 @@ describe('icmp normalizers', () => { type: 'icmp', wait: '30', id: '', + hash: testHash, }, unsupportedKeys: [], }, { errors: [], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.ICMP], config_id: '', custom_heartbeat_id: 'Cloudflare-DNS-2-test-project-id-test-space', enabled: true, @@ -168,6 +181,7 @@ describe('icmp normalizers', () => { type: 'icmp', wait: '60', id: '', + hash: testHash, }, unsupportedKeys: [], }, @@ -187,6 +201,7 @@ describe('icmp normalizers', () => { }, ], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.ICMP], config_id: '', custom_heartbeat_id: 'Cloudflare-DNS-3-test-project-id-test-space', enabled: true, @@ -221,6 +236,7 @@ describe('icmp normalizers', () => { type: 'icmp', wait: '1', id: '', + hash: testHash, }, unsupportedKeys: ['unsupportedKey.nestedUnsupportedKey'], }, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts index 9fbcb0c3b4ddf..79ea67964fd28 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts @@ -5,11 +5,18 @@ * 2.0. */ -import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types'; +import { + DataStream, + Locations, + LocationStatus, + PrivateLocation, +} from '../../../../common/runtime_types'; +import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; import { normalizeProjectMonitors } from '.'; describe('tcp normalizers', () => { describe('normalize push monitors', () => { + const testHash = 'lleklrkelkj'; const projectId = 'test-project-id'; const locations: Locations = [ { @@ -50,6 +57,7 @@ describe('tcp normalizers', () => { privateLocations: ['BEEP'], 'service.name': 'test service', 'ssl.supported_protocols': ['TLSv1.2', 'TLSv1.3'], + hash: testHash, }, { locations: ['us_central'], @@ -66,6 +74,7 @@ describe('tcp normalizers', () => { ssl: { supported_protocols: 'TLSv1.2,TLSv1.3', }, + hash: testHash, }, { locations: ['us_central'], @@ -80,6 +89,7 @@ describe('tcp normalizers', () => { unsupportedKey: { nestedUnsupportedKey: 'unnsuportedValue', }, + hash: testHash, }, ]; @@ -96,6 +106,7 @@ describe('tcp normalizers', () => { { errors: [], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.TCP], __ui: { is_tls_enabled: false, }, @@ -144,12 +155,14 @@ describe('tcp normalizers', () => { type: 'tcp', id: '', urls: '', + hash: testHash, }, unsupportedKeys: [], }, { errors: [], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.TCP], __ui: { is_tls_enabled: false, }, @@ -198,6 +211,7 @@ describe('tcp normalizers', () => { type: 'tcp', id: '', urls: '', + hash: testHash, }, unsupportedKeys: [], }, @@ -217,6 +231,7 @@ describe('tcp normalizers', () => { }, ], normalizedFields: { + ...DEFAULT_FIELDS[DataStream.TCP], __ui: { is_tls_enabled: false, }, @@ -265,6 +280,7 @@ describe('tcp normalizers', () => { type: 'tcp', id: '', urls: '', + hash: testHash, }, unsupportedKeys: ['ports', 'unsupportedKey.nestedUnsupportedKey'], }, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts index 6cefad03b83fc..5892f53344d48 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts @@ -10,7 +10,8 @@ import { INSUFFICIENT_FLEET_PERMISSIONS, ProjectMonitorFormatter, } from './project_monitor_formatter'; -import { LocationStatus } from '../../../common/runtime_types'; +import { DataStream, LocationStatus } from '../../../common/runtime_types'; +import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults'; import { times } from 'lodash'; import { SyntheticsService } from '../synthetics_service'; import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters'; @@ -45,6 +46,7 @@ const testMonitors = [ content: 'UEsDBBQACAAIAAAAIQAAAAAAAAAAAAAAAAAQAAAAYmFzaWMuam91cm5leS50c2WQQU7DQAxF9znFV8QiUUOmXcCCUMQl2NdMnWbKJDMaO6Ilyt0JASQkNv9Z1teTZWNAIqwP5kU4iZGOug863u7uDXsSddbIddCOl0kMX6iPnsVoOAYxryTO1ucwpoGvtUrm+hiSYsLProIoxwp8iWwVM9oUeuTP/9V5k7UhofCscNhj2yx4xN2CzabElOHXWRxsx/YNroU69QwniImFB8Vui5vJzYcKxYRIJ66WTNQL5hL7p1WD9aYi9zQOtgPFGPNqecJ1sCj+tAB6J6erpj4FDcW3qh6TL5u1Mq/8yjn7BFBLBwhGDIWc4QAAAEkBAABQSwECLQMUAAgACAAAACEARgyFnOEAAABJAQAAEAAAAAAAAAAAACAApIEAAAAAYmFzaWMuam91cm5leS50c1BLBQYAAAAAAQABAD4AAAAfAQAAAAA=', filter: { match: 'check if title is present 10 0' }, + hash: 'lleklrkelkj', }, { type: 'browser', @@ -68,6 +70,7 @@ const testMonitors = [ content: 'UEsDBBQACAAIAAAAIQAAAAAAAAAAAAAAAAAQAAAAYmFzaWMuam91cm5leS50c2WQQU7DQAxF9znFV8QiUUOmXcCCUMQl2NdMnWbKJDMaO6Ilyt0JASQkNv9Z1teTZWNAIqwP5kU4iZGOug863u7uDXsSddbIddCOl0kMX6iPnsVoOAYxryTO1ucwpoGvtUrm+hiSYsLProIoxwp8iWwVM9oUeuTP/9V5k7UhofCscNhj2yx4xN2CzabElOHXWRxsx/YNroU69QwniImFB8Vui5vJzYcKxYRIJ66WTNQL5hL7p1WD9aYi9zQOtgPFGPNqecJ1sCj+tAB6J6erpj4FDcW3qh6TL5u1Mq/8yjn7BFBLBwhGDIWc4QAAAEkBAABQSwECLQMUAAgACAAAACEARgyFnOEAAABJAQAAEAAAAAAAAAAAACAApIEAAAAAYmFzaWMuam91cm5leS50c1BLBQYAAAAAAQABAD4AAAAfAQAAAAA=', filter: { match: 'check if title is present 10 1' }, + hash: 'lleklrkelkj', }, ]; @@ -492,6 +495,7 @@ describe('ProjectMonitorFormatter', () => { const payloadData = [ { + ...DEFAULT_FIELDS[DataStream.BROWSER], __ui: { is_zip_url_tls_enabled: false, script_source: { @@ -550,8 +554,10 @@ const payloadData = [ 'url.port': null, urls: '', id: '', + hash: 'lleklrkelkj', }, { + ...DEFAULT_FIELDS[DataStream.BROWSER], __ui: { is_zip_url_tls_enabled: false, script_source: { @@ -609,6 +615,7 @@ const payloadData = [ 'url.port': null, urls: '', id: '', + hash: 'lleklrkelkj', }, ]; diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts index 2be3509a61cf6..02a40be5d2f40 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts @@ -28,7 +28,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const kibanaServer = getService('kibanaServer'); - const projectMonitorEndpoint = kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT; + const projectMonitorEndpoint = kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY; let projectMonitors: ProjectMonitorsRequest; let httpProjectMonitors: ProjectMonitorsRequest; @@ -194,6 +194,7 @@ export default function ({ getService }: FtrProviderContext) { 'url.port': null, urls: '', id: '', + hash: 'ekrjelkjrelkjre', }); } } finally { @@ -308,6 +309,7 @@ export default function ({ getService }: FtrProviderContext) { urls: Array.isArray(monitor.urls) ? monitor.urls?.[0] : monitor.urls, 'url.port': null, id: '', + hash: 'ekrjelkjrelkjre', }); } } finally { @@ -409,6 +411,7 @@ export default function ({ getService }: FtrProviderContext) { 'url.port': null, urls: '', id: '', + hash: 'ekrjelkjrelkjre', }); } } finally { @@ -511,6 +514,7 @@ export default function ({ getService }: FtrProviderContext) { ? monitor.wait?.slice(0, -1) : `${parseInt(monitor.wait?.slice(0, -1) || '1', 10) * 60}`, id: '', + hash: 'ekrjelkjrelkjre', }); } } finally { @@ -547,7 +551,7 @@ export default function ({ getService }: FtrProviderContext) { it('project monitors - returns a list of successfully updated monitors', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(projectMonitors); @@ -575,12 +579,12 @@ export default function ({ getService }: FtrProviderContext) { it('project monitors - does not increment monitor revision unless a change has been made', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(projectMonitors); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(projectMonitors); @@ -609,7 +613,7 @@ export default function ({ getService }: FtrProviderContext) { it('project monitors - increments monitor revision when a change has been made', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(projectMonitors); @@ -655,7 +659,7 @@ export default function ({ getService }: FtrProviderContext) { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -701,7 +705,7 @@ export default function ({ getService }: FtrProviderContext) { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -839,7 +843,8 @@ export default function ({ getService }: FtrProviderContext) { } ); - const spaceUrl = kibanaServerUrl + `/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT}`; + const spaceUrl = + kibanaServerUrl + `/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY}`; const messages = await parseStreamApiResponse( spaceUrl, @@ -928,6 +933,7 @@ export default function ({ getService }: FtrProviderContext) { upload: 3, }, type: 'browser', + hash: 'ekrjelkjrelkjre', }, reason: 'Failed to save or update monitor. Configuration is not valid', }, @@ -966,7 +972,7 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); await supertestWithoutAuth - .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY}`) .auth(username, password) .set('kbn-xsrf', 'true') .send(projectMonitors) @@ -1020,7 +1026,7 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); await supertestWithoutAuth - .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY}`) .auth(username, password) .set('kbn-xsrf', 'true') .send(projectMonitors) @@ -1055,7 +1061,7 @@ export default function ({ getService }: FtrProviderContext) { it('project monitors - is able to decrypt monitor when updated after hydration', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(projectMonitors); @@ -1109,12 +1115,12 @@ export default function ({ getService }: FtrProviderContext) { it('project monitors - is able to enable and disable monitors', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(projectMonitors); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1172,7 +1178,7 @@ export default function ({ getService }: FtrProviderContext) { }); const messages = await parseStreamApiResponse( - kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT, + kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY, JSON.stringify({ ...projectMonitors, keep_stale: false, @@ -1219,6 +1225,7 @@ export default function ({ getService }: FtrProviderContext) { latency: 20, upload: 3, }, + hash: 'ekrjelkjrelkjre', }, reason: 'Failed to create or update monitor', }, @@ -1270,7 +1277,7 @@ export default function ({ getService }: FtrProviderContext) { }); await parseStreamApiResponse( - kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT, + kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY, JSON.stringify({ ...projectMonitors, keep_stale: false, @@ -1279,7 +1286,7 @@ export default function ({ getService }: FtrProviderContext) { ); const messages = await parseStreamApiResponse( - kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT, + kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY, JSON.stringify({ ...projectMonitors, keep_stale: false, @@ -1315,7 +1322,7 @@ export default function ({ getService }: FtrProviderContext) { }); const messages2 = await parseStreamApiResponse( - kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT, + kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY, JSON.stringify({ ...projectMonitors, keep_stale: false, @@ -1418,7 +1425,7 @@ export default function ({ getService }: FtrProviderContext) { it('creates integration policies for project monitors with private locations', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1482,7 +1489,7 @@ export default function ({ getService }: FtrProviderContext) { }; try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(monitorRequest); @@ -1509,7 +1516,7 @@ export default function ({ getService }: FtrProviderContext) { expect(packagePolicy.policy_id).eql(testPolicyId); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...monitorRequest, @@ -1537,7 +1544,7 @@ export default function ({ getService }: FtrProviderContext) { it('deletes integration policies for project monitors when private location is removed from the monitor', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1582,7 +1589,7 @@ export default function ({ getService }: FtrProviderContext) { ); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1615,7 +1622,7 @@ export default function ({ getService }: FtrProviderContext) { it('deletes integration policies when project monitors are deleted', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1661,7 +1668,7 @@ export default function ({ getService }: FtrProviderContext) { ); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1715,7 +1722,7 @@ export default function ({ getService }: FtrProviderContext) { }; try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send(monitorRequest); @@ -1849,7 +1856,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...monitorRequest, @@ -1882,7 +1889,7 @@ export default function ({ getService }: FtrProviderContext) { it('handles updating package policies when project monitors are updated', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1927,7 +1934,7 @@ export default function ({ getService }: FtrProviderContext) { ); await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -1974,7 +1981,7 @@ export default function ({ getService }: FtrProviderContext) { it('handles location formatting for both private and public locations', async () => { try { await supertest - .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY) .set('kbn-xsrf', 'true') .send({ ...projectMonitors, @@ -2082,7 +2089,7 @@ type StreamApiFunction = ( * This helps the test file have DRY code when it comes to calling * the same streaming endpoint over and over by defining some selective defaults. */ -const parseStreamApiResponse: StreamApiFunction> = async ( +export const parseStreamApiResponse: StreamApiFunction> = async ( url: string, body?: BodyInit, extraHeaders?: HeadersInit, diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json index 0d1508bf780cc..1ec38eaf7633f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/browser_monitor.json @@ -48,5 +48,6 @@ "form_monitor_type": "multistep", "urls": "", "url.port": null, - "id": "" + "id": "", + "hash": "" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json index 2f68e70f0e00d..218cc5b1b9770 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json @@ -80,5 +80,6 @@ "origin": "ui", "form_monitor_type": "http", "journey_id": "", - "id": "" + "id": "", + "hash": "" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json index fb6efa3a604d2..2e0c25085ec65 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/icmp_monitor.json @@ -20,5 +20,6 @@ "namespace": "testnamespace", "origin": "ui", "form_monitor_type": "icmp", - "id": "" + "id": "", + "hash": "" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json index ba1101be34ba4..eb51f1fa4bf18 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_browser_monitor.json @@ -22,6 +22,7 @@ "content": "UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA", "filter": { "match": "check if title is present" - } + }, + "hash": "ekrjelkjrelkjre" }] } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json index 42044c8ba9cf3..5d2a3b9ff8eb5 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json @@ -35,7 +35,8 @@ }, "unsupportedKey": { "nestedUnsupportedKey": "unsupportedValue" - } + }, + "hash": "ekrjelkjrelkjre" }, { "locations": ["localhost"], @@ -69,7 +70,8 @@ "saved" ] } - } + }, + "hash": "ekrjelkjrelkjre" } ] } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json index b19e910882582..f7dcb917b621d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json @@ -14,7 +14,8 @@ "schedule": 1, "tags": [ "service:smtp", "org:google" ], "privateLocations": [ "Test private location 0" ], - "wait": "30s" + "wait": "30s", + "hash": "ekrjelkjrelkjre" }, { "locations": [ "localhost" ], @@ -25,7 +26,8 @@ "schedule": 1, "tags": "tag1,tag2", "privateLocations": [ "Test private location 0" ], - "wait": "1m" + "wait": "1m", + "hash": "ekrjelkjrelkjre" }, { "locations": [ "localhost" ], @@ -38,7 +40,8 @@ "privateLocations": [ "Test private location 0" ], "unsupportedKey": { "nestedUnsupportedKey": "unnsuportedValue" - } + }, + "hash": "ekrjelkjrelkjre" } ] } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json index 82d6c8c557e77..5d028f447ba0a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json @@ -10,7 +10,8 @@ "hosts": [ "smtp.gmail.com:587" ], "schedule": 1, "tags": [ "service:smtp", "org:google" ], - "privateLocations": [ "BEEP" ] + "privateLocations": [ "BEEP" ], + "hash": "ekrjelkjrelkjre" }, { "locations": [ "localhost" ], @@ -20,7 +21,8 @@ "hosts": "localhost:18278", "schedule": 1, "tags": "tag1,tag2", - "privateLocations": [ "BEEP" ] + "privateLocations": [ "BEEP" ], + "hash": "ekrjelkjrelkjre" }, { "locations": [ "localhost" ], @@ -34,7 +36,8 @@ "privateLocations": [ "BEEP" ], "unsupportedKey": { "nestedUnsupportedKey": "unnsuportedValue" - } + }, + "hash": "ekrjelkjrelkjre" } ] } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json index bfa0b3a1a7242..209ef89373736 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/tcp_monitor.json @@ -34,5 +34,6 @@ "namespace": "testnamespace", "origin": "ui", "form_monitor_type": "tcp", - "id": "" + "id": "", + "hash": "" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/get_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/get_monitor_project.ts new file mode 100644 index 0000000000000..bbd0715c45739 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/get_monitor_project.ts @@ -0,0 +1,440 @@ +/* + * 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 uuid from 'uuid'; +import expect from '@kbn/expect'; +import type SuperTest from 'supertest'; +import { format as formatUrl } from 'url'; +import { + ProjectMonitorsRequest, + ProjectMonitor, + ProjectMonitorMetaData, +} from '@kbn/synthetics-plugin/common/runtime_types'; +import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getFixtureJson } from './helper/get_fixture_json'; +import { PrivateLocationTestService } from './services/private_location_test_service'; +import { parseStreamApiResponse } from './add_monitor_project'; + +export default function ({ getService }: FtrProviderContext) { + describe('GetProjectMonitors', function () { + this.tags('skipCloud'); + + const supertest = getService('supertest'); + const config = getService('config'); + const kibanaServerUrl = formatUrl(config.get('servers.kibana')); + const projectMonitorEndpoint = kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT_LEGACY; + + let projectMonitors: ProjectMonitorsRequest; + let httpProjectMonitors: ProjectMonitorsRequest; + let tcpProjectMonitors: ProjectMonitorsRequest; + let icmpProjectMonitors: ProjectMonitorsRequest; + + let testPolicyId = ''; + const testPrivateLocations = new PrivateLocationTestService(getService); + + const setUniqueIds = (request: ProjectMonitorsRequest) => { + return { + ...request, + monitors: request.monitors.map((monitor) => ({ ...monitor, id: uuid.v4() })), + }; + }; + + before(async () => { + await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); + await supertest + .post('/api/fleet/epm/packages/synthetics/0.10.3') + .set('kbn-xsrf', 'true') + .send({ force: true }) + .expect(200); + + const testPolicyName = 'Fleet test server policy' + Date.now(); + const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); + testPolicyId = apiResponse.body.item.id; + await testPrivateLocations.setTestLocations([testPolicyId]); + }); + + beforeEach(() => { + projectMonitors = setUniqueIds(getFixtureJson('project_browser_monitor')); + httpProjectMonitors = setUniqueIds(getFixtureJson('project_http_monitor')); + tcpProjectMonitors = setUniqueIds(getFixtureJson('project_tcp_monitor')); + icmpProjectMonitors = setUniqueIds(getFixtureJson('project_icmp_monitor')); + }); + + it('project monitors - fetches all monitors - browser', async () => { + const monitors = []; + const project = 'test-brower-suite'; + for (let i = 0; i < 600; i++) { + monitors.push({ + ...projectMonitors.monitors[0], + id: `test browser id ${i}`, + name: `test name ${i}`, + }); + } + + try { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + monitors, + }) + ); + + const firstPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { monitors: firstPageMonitors, total, after_key: afterKey } = firstPageResponse.body; + expect(firstPageMonitors.length).to.eql(500); + expect(total).to.eql(600); + expect(afterKey).to.eql('test browser id 548'); + + const secondPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .query({ + search_after: afterKey, + }) + .send() + .expect(200); + const { monitors: secondPageMonitors } = secondPageResponse.body; + expect(secondPageMonitors.length).to.eql(100); + checkFields([...firstPageMonitors, ...secondPageMonitors], monitors); + } finally { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + keep_stale: false, + monitors: [], + }) + ); + } + }); + + it('project monitors - fetches all monitors - http', async () => { + const monitors = []; + const project = 'test-http-suite'; + for (let i = 0; i < 600; i++) { + monitors.push({ + ...httpProjectMonitors.monitors[1], + id: `test http id ${i}`, + name: `test name ${i}`, + }); + } + + try { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + monitors, + }) + ); + + const firstPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { + monitors: firstPageProjectMonitors, + after_key: afterKey, + total, + } = firstPageResponse.body; + expect(firstPageProjectMonitors.length).to.eql(500); + expect(total).to.eql(600); + expect(afterKey).to.eql('test http id 548'); + + const secondPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .query({ + search_after: afterKey, + }) + .send() + .expect(200); + const { monitors: secondPageProjectMonitors } = secondPageResponse.body; + expect(secondPageProjectMonitors.length).to.eql(100); + checkFields([...firstPageProjectMonitors, ...secondPageProjectMonitors], monitors); + } finally { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + keep_stale: false, + monitors: [], + }) + ); + } + }); + + it('project monitors - fetches all monitors - tcp', async () => { + const monitors = []; + const project = 'test-tcp-suite'; + for (let i = 0; i < 600; i++) { + monitors.push({ + ...tcpProjectMonitors.monitors[0], + id: `test tcp id ${i}`, + name: `test name ${i}`, + }); + } + + try { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + monitors, + }) + ); + + const firstPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { + monitors: firstPageProjectMonitors, + after_key: afterKey, + total, + } = firstPageResponse.body; + expect(firstPageProjectMonitors.length).to.eql(500); + expect(total).to.eql(600); + expect(afterKey).to.eql('test tcp id 548'); + + const secondPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .query({ + search_after: afterKey, + }) + .send() + .expect(200); + const { monitors: secondPageProjectMonitors } = secondPageResponse.body; + expect(secondPageProjectMonitors.length).to.eql(100); + checkFields([...firstPageProjectMonitors, ...secondPageProjectMonitors], monitors); + } finally { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + keep_stale: false, + monitors: [], + }) + ); + } + }); + + it('project monitors - fetches all monitors - icmp', async () => { + const monitors = []; + const project = 'test-icmp-suite'; + for (let i = 0; i < 600; i++) { + monitors.push({ + ...icmpProjectMonitors.monitors[0], + id: `test icmp id ${i}`, + name: `test name ${i}`, + }); + } + + try { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + monitors, + }) + ); + + const firstPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { + monitors: firstPageProjectMonitors, + after_key: afterKey, + total, + } = firstPageResponse.body; + expect(firstPageProjectMonitors.length).to.eql(500); + expect(total).to.eql(600); + expect(afterKey).to.eql('test icmp id 548'); + + const secondPageResponse = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', project)) + .set('kbn-xsrf', 'true') + .query({ + search_after: afterKey, + }) + .send() + .expect(200); + const { monitors: secondPageProjectMonitors } = secondPageResponse.body; + expect(secondPageProjectMonitors.length).to.eql(100); + + checkFields([...firstPageProjectMonitors, ...secondPageProjectMonitors], monitors); + } finally { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project, + keep_stale: false, + monitors: [], + }) + ); + } + }); + + it('project monitors - handles url ecoded project names', async () => { + const monitors = []; + const projectName = 'Test project'; + for (let i = 0; i < 600; i++) { + monitors.push({ + ...icmpProjectMonitors.monitors[0], + id: `test url id ${i}`, + name: `test name ${i}`, + }); + } + + try { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project: projectName, + keep_stale: false, + monitors, + }) + ); + + const firstPageResponse = await supertest + .get( + API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', encodeURI(projectName)) + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { + monitors: firstPageProjectMonitors, + after_key: afterKey, + total, + } = firstPageResponse.body; + expect(firstPageProjectMonitors.length).to.eql(500); + expect(total).to.eql(600); + expect(afterKey).to.eql('test url id 548'); + + const secondPageResponse = await supertest + .get( + API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', encodeURI(projectName)) + ) + .set('kbn-xsrf', 'true') + .query({ + search_after: afterKey, + }) + .send() + .expect(200); + const { monitors: secondPageProjectMonitors } = secondPageResponse.body; + expect(secondPageProjectMonitors.length).to.eql(100); + + checkFields([...firstPageProjectMonitors, ...secondPageProjectMonitors], monitors); + } finally { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + project: projectName, + keep_stale: false, + monitors: [], + }) + ); + } + }); + + it('project monitors - handles per_page parameter', async () => { + const monitors = []; + const perPage = 250; + for (let i = 0; i < 600; i++) { + monitors.push({ + ...icmpProjectMonitors.monitors[0], + id: `test-id-${i}`, + name: `test-name-${i}`, + }); + } + + try { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + monitors, + }) + ); + let count = Number.MAX_VALUE; + let afterId; + const fullResponse: ProjectMonitorMetaData[] = []; + let page = 1; + while (count >= 250) { + const response: SuperTest.Response = await supertest + .get(API_URLS.SYNTHETICS_MONITORS_PROJECT.replace('{projectName}', 'test-suite')) + .set('kbn-xsrf', 'true') + .query({ + per_page: perPage, + search_after: afterId, + }) + .send() + .expect(200); + + const { monitors: monitorsResponse, after_key: afterKey, total } = response.body; + expect(total).to.eql(600); + count = monitorsResponse.length; + fullResponse.push(...monitorsResponse); + if (page < 3) { + expect(count).to.eql(perPage); + } else { + expect(count).to.eql(100); + } + page++; + + afterId = afterKey; + } + // expect(fullResponse.length).to.eql(600); + // checkFields(fullResponse, monitors); + } finally { + await parseStreamApiResponse( + projectMonitorEndpoint, + JSON.stringify({ + ...projectMonitors, + keep_stale: false, + monitors: [], + }) + ); + } + }); + }); +} + +const checkFields = (monitorMetaData: ProjectMonitorMetaData[], monitors: ProjectMonitor[]) => { + monitors.forEach((monitor) => { + const configIsCorrect = monitorMetaData.some((ndjson: Record) => { + return ndjson.journey_id === monitor.id && ndjson.hash === monitor.hash; + }); + expect(configIsCorrect).to.eql(true); + }); +}; diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index dfd1b3baffc5e..e1fd22c2baf8a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -78,6 +78,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get_monitor_overview')); loadTestFile(require.resolve('./add_monitor')); loadTestFile(require.resolve('./add_monitor_project')); + loadTestFile(require.resolve('./get_monitor_project')); loadTestFile(require.resolve('./add_monitor_private_location')); loadTestFile(require.resolve('./edit_monitor')); loadTestFile(require.resolve('./delete_monitor')); From 6e5f13740cb5959776664585ce5fb8ab2f00f025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 17 Oct 2022 18:17:57 +0200 Subject: [PATCH 12/74] [EBT/ElasticV3Server] Simplify leaky bucket (#143323) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../shippers/elastic_v3/server/README.md | 2 +- .../server/src/server_shipper.test.ts | 32 +++++----- .../elastic_v3/server/src/server_shipper.ts | 58 +++++++++---------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/packages/analytics/shippers/elastic_v3/server/README.md b/packages/analytics/shippers/elastic_v3/server/README.md index ccdcf6a66cbe2..edb4bdffa0dba 100644 --- a/packages/analytics/shippers/elastic_v3/server/README.md +++ b/packages/analytics/shippers/elastic_v3/server/README.md @@ -22,4 +22,4 @@ analytics.registerShipper(ElasticV3ServerShipper, { channelName: 'myChannel', ve ## Transmission protocol -This shipper sends the events to the Elastic Internal Telemetry Service. It holds up to 1000 events in a shared queue. Any additional incoming events once it's full will be dropped. It sends the events from the queue in batches of 10kB every 10 seconds. If not enough events are available in the queue for longer than 10 minutes, it will send any remaining events. When shutting down, it'll send all the remaining events in the queue. +This shipper sends the events to the Elastic Internal Telemetry Service. It holds up to 1000 events in a shared queue. Any additional incoming events once it's full will be dropped. It sends the events from the queue in batches of up to 10kB every 10 seconds. When shutting down, it'll send all the remaining events in the queue. diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts index 668574dbb8f1d..03356f7428da7 100644 --- a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts @@ -117,13 +117,13 @@ describe('ElasticV3ServerShipper', () => { ); test( - 'calls to reportEvents call `fetch` after 10 minutes when optIn value is set to true', + 'calls to reportEvents call `fetch` after 10 seconds when optIn value is set to true', fakeSchedulers(async (advance) => { shipper.reportEvents(events); shipper.optIn(true); const counter = firstValueFrom(shipper.telemetryCounter$); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).toHaveBeenCalledWith( 'https://telemetry-staging.elastic.co/v3/send/test-channel', { @@ -150,12 +150,12 @@ describe('ElasticV3ServerShipper', () => { ); test( - 'calls to reportEvents do not call `fetch` after 10 minutes when optIn value is set to false', + 'calls to reportEvents do not call `fetch` after 10 seconds when optIn value is set to false', fakeSchedulers((advance) => { shipper.reportEvents(events); shipper.optIn(false); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).not.toHaveBeenCalled(); }) ); @@ -201,8 +201,8 @@ describe('ElasticV3ServerShipper', () => { shipper['firstTimeOffline'] = null; shipper.reportEvents(events); shipper.optIn(true); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).toHaveBeenCalledWith( 'https://telemetry-staging.elastic.co/v3/send/test-channel', { @@ -226,11 +226,11 @@ describe('ElasticV3ServerShipper', () => { shipper.reportEvents(new Array(1000).fill(events[0])); shipper.optIn(true); - // Due to the size of the test events, it matches 9 rounds. + // Due to the size of the test events, it matches 8 rounds. for (let i = 0; i < 9; i++) { const counter = firstValueFrom(shipper.telemetryCounter$); setLastBatchSent(Date.now() - 10 * SECONDS); - advance(10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).toHaveBeenNthCalledWith( i + 1, 'https://telemetry-staging.elastic.co/v3/send/test-channel', @@ -277,8 +277,8 @@ describe('ElasticV3ServerShipper', () => { shipper.reportEvents(events); shipper.optIn(true); const counter = firstValueFrom(shipper.telemetryCounter$); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).toHaveBeenCalledWith( 'https://telemetry-staging.elastic.co/v3/send/test-channel', { @@ -315,8 +315,8 @@ describe('ElasticV3ServerShipper', () => { shipper.reportEvents(events); shipper.optIn(true); const counter = firstValueFrom(shipper.telemetryCounter$); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).toHaveBeenCalledWith( 'https://telemetry-staging.elastic.co/v3/send/test-channel', { @@ -458,8 +458,8 @@ describe('ElasticV3ServerShipper', () => { shipper.reportEvents(events); shipper.optIn(true); const counter = firstValueFrom(shipper.telemetryCounter$); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(1 * SECONDS); // Moving 1 second should be enough to trigger the logic expect(fetchMock).toHaveBeenNthCalledWith( 1, 'https://telemetry-staging.elastic.co/v3/send/test-channel', diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts index e8cb3c7bff5db..34ebe134adcf7 100644 --- a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts @@ -12,10 +12,7 @@ import { Subject, ReplaySubject, interval, - concatMap, merge, - from, - firstValueFrom, timer, retryWhen, tap, @@ -24,6 +21,7 @@ import { map, BehaviorSubject, exhaustMap, + mergeMap, } from 'rxjs'; import type { AnalyticsClientInitContext, @@ -46,6 +44,7 @@ const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; const KIB = 1024; const MAX_NUMBER_OF_EVENTS_IN_INTERNAL_QUEUE = 1000; +const MIN_TIME_SINCE_LAST_SEND = 10 * SECOND; /** * Elastic V3 shipper to use on the server side. @@ -130,6 +129,7 @@ export class ElasticV3ServerShipper implements IShipper { * @param events batched events {@link Event} */ public reportEvents(events: Event[]) { + // If opted out OR offline for longer than 24 hours, skip processing any events. if ( this.isOptedIn$.value === false || (this.firstTimeOffline && Date.now() - this.firstTimeOffline > 24 * HOUR) @@ -216,42 +216,40 @@ export class ElasticV3ServerShipper implements IShipper { } private setInternalSubscriber() { - // Check the status of the queues every 1 second. + // Create an emitter that emits when MIN_TIME_SINCE_LAST_SEND have passed since the last time we sent the data + const minimumTimeSinceLastSent$ = interval(SECOND).pipe( + filter(() => Date.now() - this.lastBatchSent >= MIN_TIME_SINCE_LAST_SEND) + ); + merge( - interval(1000).pipe(takeUntil(this.shutdown$)), - // Using a promise because complete does not emit through the pipe. - from(firstValueFrom(this.shutdown$, { defaultValue: true })) + minimumTimeSinceLastSent$.pipe( + takeUntil(this.shutdown$), + map(() => ({ shouldFlush: false })) + ), + // Attempt to send one last time on shutdown, flushing the queue + this.shutdown$.pipe(map(() => ({ shouldFlush: true }))) ) .pipe( - // Only move ahead if it's opted-in and online. - filter(() => this.isOptedIn$.value === true && this.firstTimeOffline === null), - - // Send the events now if (validations sorted from cheapest to most CPU expensive): - // - We are shutting down. - // - There are some events in the queue, and we didn't send anything in the last 10 minutes. - // - The last time we sent was more than 10 seconds ago and: - // - We reached the minimum batch size of 10kB per request in our leaky bucket. - // - The queue is full (meaning we'll never get to 10kB because the events are very small). + // Only move ahead if it's opted-in and online, and there are some events in the queue filter( () => - (this.internalQueue.length > 0 && - (this.shutdown$.isStopped || Date.now() - this.lastBatchSent >= 10 * MINUTE)) || - (Date.now() - this.lastBatchSent >= 10 * SECOND && - (this.internalQueue.length === MAX_NUMBER_OF_EVENTS_IN_INTERNAL_QUEUE || - this.getQueueByteSize(this.internalQueue) >= 10 * KIB)) + this.isOptedIn$.value === true && + this.firstTimeOffline === null && + this.internalQueue.length > 0 ), // Send the events: // 1. Set lastBatchSent and retrieve the events to send (clearing the queue) in a synchronous operation to avoid race conditions. - map(() => { + map(({ shouldFlush }) => { this.lastBatchSent = Date.now(); - return this.getEventsToSend(); + return this.getEventsToSend(shouldFlush); }), - // 2. Skip empty buffers + // 2. Skip empty buffers (just to be sure) filter((events) => events.length > 0), // 3. Actually send the events - // Using `concatMap` here because we want to send events whenever the emitter says so. Otherwise, it'd skip sending some events. - concatMap(async (eventsToSend) => await this.sendEvents(eventsToSend)) + // Using `mergeMap` here because we want to send events whenever the emitter says so: + // We don't want to skip emissions (exhaustMap) or enqueue them (concatMap). + mergeMap((eventsToSend) => this.sendEvents(eventsToSend)) ) .subscribe(); } @@ -278,13 +276,13 @@ export class ElasticV3ServerShipper implements IShipper { } /** - * Returns a queue of events of up-to 10kB. + * Returns a queue of events of up-to 10kB. Or all events in the queue if it's a FLUSH action. * @remarks It mutates the internal queue by removing from it the events returned by this method. * @private */ - private getEventsToSend(): Event[] { - // If the internal queue is already smaller than the minimum batch size, do a direct assignment. - if (this.getQueueByteSize(this.internalQueue) < 10 * KIB) { + private getEventsToSend(shouldFlush: boolean): Event[] { + // If the internal queue is already smaller than the minimum batch size, or it's a flush action, do a direct assignment. + if (shouldFlush || this.getQueueByteSize(this.internalQueue) < 10 * KIB) { return this.internalQueue.splice(0, this.internalQueue.length); } // Otherwise, we'll feed the events to the leaky bucket queue until we reach 10kB. From 1eed5311c7f6f37ce4dbe6b2fa01fe08b149f006 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 17 Oct 2022 10:52:24 -0600 Subject: [PATCH 13/74] [Dashboard] Modify state shared in dashboard permalinks (#141985) * Remove unnecessary global state from share URL * Clean up * Add functional tests * Fix functional tests * Undo removal of time range from global state in URL * Clean up code * Clean up functional tests * Add warning when snapshot sharing with unsaved panel changes * Modify how error is passed down * Fix flakiness of new functional test suite * Update snapshots + clean up imports * Change wording of warning + colour of text * Address first round of feedback * Switch error state to button --- .../lib/convert_dashboard_state.ts | 16 +- .../application/top_nav/show_share_modal.tsx | 32 ++- .../dashboard/public/dashboard_strings.ts | 5 + .../url_panel_content.test.tsx.snap | 248 +++++++++--------- .../public/components/share_context_menu.tsx | 3 + .../public/components/url_panel_content.tsx | 83 ++++-- .../public/services/share_menu_manager.tsx | 2 + src/plugins/share/public/types.ts | 1 + .../functional/apps/dashboard/group6/share.ts | 135 +++++++++- 9 files changed, 354 insertions(+), 171 deletions(-) diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts index 14e0f4ac4c171..fd40aec20be63 100644 --- a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { cloneDeep, omit } from 'lodash'; +import { cloneDeep } from 'lodash'; import type { KibanaExecutionContext } from '@kbn/core/public'; import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; @@ -26,7 +26,7 @@ interface StateToDashboardContainerInputProps { } interface StateToRawDashboardStateProps { - state: DashboardState; + state: Partial; } /** @@ -102,13 +102,15 @@ const filtersAreEqual = (first: Filter, second: Filter) => */ export const stateToRawDashboardState = ({ state, -}: StateToRawDashboardStateProps): RawDashboardState => { +}: StateToRawDashboardStateProps): Partial => { const { initializerContext: { kibanaVersion }, } = pluginServices.getServices(); - const savedDashboardPanels = Object.values(state.panels).map((panel) => - convertPanelStateToSavedDashboardPanel(panel, kibanaVersion) - ); - return { ...omit(state, 'panels'), panels: savedDashboardPanels }; + const savedDashboardPanels = state?.panels + ? Object.values(state.panels).map((panel) => + convertPanelStateToSavedDashboardPanel(panel, kibanaVersion) + ) + : undefined; + return { ...state, panels: savedDashboardPanels }; }; diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx index 0f7d119427dd7..6cff8ff20a9dd 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx @@ -8,11 +8,14 @@ import moment from 'moment'; import React, { ReactElement, useState } from 'react'; +import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiCheckboxGroup } from '@elastic/eui'; +import { QueryState } from '@kbn/data-plugin/common'; import type { Capabilities } from '@kbn/core/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { getStateFromKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { setStateToKbnUrl, unhashUrl } from '@kbn/kibana-utils-plugin/public'; import type { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; @@ -21,8 +24,8 @@ import { dashboardUrlParams } from '../dashboard_router'; import { shareModalStrings } from '../../dashboard_strings'; import { convertPanelMapToSavedPanels } from '../../../common'; import { pluginServices } from '../../services/plugin_services'; -import { stateToRawDashboardState } from '../lib/convert_dashboard_state'; import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator'; +import { stateToRawDashboardState } from '../lib/convert_dashboard_state'; const showFilterBarId = 'showFilterBar'; @@ -55,8 +58,8 @@ export function ShowShareModal({ }, }, }, - share: { toggleShareContextMenu }, initializerContext: { kibanaVersion }, + share: { toggleShareContextMenu }, } = pluginServices.getServices(); if (!toggleShareContextMenu) return; // TODO: Make this logic cleaner once share is an optional service @@ -151,19 +154,25 @@ export function ShowShareModal({ ...unsavedStateForLocator, }; + let _g = getStateFromKbnUrl('_g', window.location.href); + if (_g?.filters && _g.filters.length === 0) { + _g = omit(_g, 'filters'); + } + const baseUrl = setStateToKbnUrl('_g', _g); + + const shareableUrl = setStateToKbnUrl( + '_a', + stateToRawDashboardState({ state: unsavedDashboardState ?? {} }), + { useHash: false, storeInHashQuery: true }, + unhashUrl(baseUrl) + ); + toggleShareContextMenu({ isDirty, anchorElement, allowEmbed: true, allowShortUrl, - shareableUrl: setStateToKbnUrl( - '_a', - stateToRawDashboardState({ - state: currentDashboardState, - }), - { useHash: false, storeInHashQuery: true }, - unhashUrl(window.location.href) - ), + shareableUrl, objectId: savedObjectId, objectType: 'dashboard', sharingData: { @@ -185,5 +194,8 @@ export function ShowShareModal({ }, ], showPublicUrlSwitch, + snapshotShareWarning: Boolean(unsavedDashboardState?.panels) + ? shareModalStrings.getSnapshotShareWarning() + : undefined, }); } diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 6474c7dc2bba6..fcb7f3cb2a7c1 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -278,6 +278,11 @@ export const shareModalStrings = { i18n.translate('dashboard.embedUrlParamExtension.include', { defaultMessage: 'Include', }), + getSnapshotShareWarning: () => + i18n.translate('dashboard.snapshotShare.longUrlWarning', { + defaultMessage: + 'One or more panels on this dashboard have changed. Before you generate a snapshot, save the dashboard.', + }), }; export const leaveConfirmStrings = { diff --git a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap index 3278c01dd86da..b0fed8a7bd3b5 100644 --- a/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap +++ b/src/plugins/share/public/components/__snapshots__/url_panel_content.test.tsx.snap @@ -42,38 +42,40 @@ exports[`share url panel content render 1`] = ` Object { "data-test-subj": "exportAsSnapshot", "id": "snapshot", - "label": - - - - + - + + + + - } - position="bottom" - /> - - , + /> + } + position="bottom" + /> + + + , }, Object { "data-test-subj": "exportAsSavedObject", @@ -225,38 +227,40 @@ exports[`share url panel content should enable saved object export option when o Object { "data-test-subj": "exportAsSnapshot", "id": "snapshot", - "label": - - - - + - + + + + - } - position="bottom" - /> - - , + /> + } + position="bottom" + /> + + + , }, Object { "data-test-subj": "exportAsSavedObject", @@ -408,38 +412,40 @@ exports[`share url panel content should hide short url section when allowShortUr Object { "data-test-subj": "exportAsSnapshot", "id": "snapshot", - "label": - - - - + - + + + + - } - position="bottom" - /> - - , + /> + } + position="bottom" + /> + + + , }, Object { "data-test-subj": "exportAsSavedObject", @@ -527,38 +533,40 @@ exports[`should show url param extensions 1`] = ` Object { "data-test-subj": "exportAsSnapshot", "id": "snapshot", - "label": - - - - + - + + + + - } - position="bottom" - /> - - , + /> + } + position="bottom" + /> + + + , }, Object { "data-test-subj": "exportAsSavedObject", diff --git a/src/plugins/share/public/components/share_context_menu.tsx b/src/plugins/share/public/components/share_context_menu.tsx index 3550711ac84c3..c964737026b3b 100644 --- a/src/plugins/share/public/components/share_context_menu.tsx +++ b/src/plugins/share/public/components/share_context_menu.tsx @@ -32,6 +32,7 @@ export interface ShareContextMenuProps { anonymousAccess?: AnonymousAccessServiceContract; showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean; urlService: BrowserUrlService; + snapshotShareWarning?: string; } export class ShareContextMenu extends Component { @@ -66,6 +67,7 @@ export class ShareContextMenu extends Component { anonymousAccess={this.props.anonymousAccess} showPublicUrlSwitch={this.props.showPublicUrlSwitch} urlService={this.props.urlService} + snapshotShareWarning={this.props.snapshotShareWarning} /> ), }; @@ -96,6 +98,7 @@ export class ShareContextMenu extends Component { anonymousAccess={this.props.anonymousAccess} showPublicUrlSwitch={this.props.showPublicUrlSwitch} urlService={this.props.urlService} + snapshotShareWarning={this.props.snapshotShareWarning} /> ), }; diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index 54d3c8aa7e7ec..32441ab2945eb 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -20,6 +20,7 @@ import { EuiRadioGroup, EuiSwitch, EuiSwitchEvent, + EuiToolTip, } from '@elastic/eui'; import { format as formatUrl, parse as parseUrl } from 'url'; @@ -45,6 +46,7 @@ export interface UrlPanelContentProps { anonymousAccess?: AnonymousAccessServiceContract; showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean; urlService: BrowserUrlService; + snapshotShareWarning?: string; } export enum ExportUrlAsType { @@ -78,7 +80,6 @@ export class UrlPanelContent extends Component { super(props); this.shortUrlCache = undefined; - this.state = { exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, useShortUrl: false, @@ -155,6 +156,33 @@ export class UrlPanelContent extends Component { ); + const showWarningButton = + this.props.snapshotShareWarning && + this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT; + + const copyButton = (copy: () => void) => ( + + {this.props.isEmbedded ? ( + + ) : ( + + )} + + ); + return ( @@ -166,27 +194,19 @@ export class UrlPanelContent extends Component { {(copy: () => void) => ( - - {this.props.isEmbedded ? ( - + <> + {showWarningButton ? ( + + {copyButton(copy)} + ) : ( - + copyButton(copy) )} - + )} @@ -246,13 +266,11 @@ export class UrlPanelContent extends Component { }, }), }); - return this.updateUrlParams(formattedUrl); }; private getSnapshotUrl = () => { const url = this.props.shareableUrl || window.location.href; - return this.updateUrlParams(url); }; @@ -404,17 +422,24 @@ export class UrlPanelContent extends Component { }; private renderExportUrlAsOptions = () => { + const snapshotLabel = ( + + ); return [ { id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, - label: this.renderWithIconTip( - , - + values={{ objectType: this.props.objectType }} + /> + )} + ), ['data-test-subj']: 'exportAsSnapshot', }, diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx index 6f27fc8030969..a393d4aba6033 100644 --- a/src/plugins/share/public/services/share_menu_manager.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -74,6 +74,7 @@ export class ShareMenuManager { showPublicUrlSwitch, urlService, anonymousAccess, + snapshotShareWarning, onClose, }: ShowShareMenuOptions & { menuItems: ShareMenuItem[]; @@ -114,6 +115,7 @@ export class ShareMenuManager { anonymousAccess={anonymousAccess} showPublicUrlSwitch={showPublicUrlSwitch} urlService={urlService} + snapshotShareWarning={snapshotShareWarning} /> diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index 355b890c9028c..b1cd995a5ff84 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -97,5 +97,6 @@ export interface ShowShareMenuOptions extends Omit { allowEmbed: boolean; allowShortUrl: boolean; embedUrlParamExtensions?: UrlParamExtension[]; + snapshotShareWarning?: string; onClose?: () => void; } diff --git a/test/functional/apps/dashboard/group6/share.ts b/test/functional/apps/dashboard/group6/share.ts index 871ab5bed1488..40cea873988e1 100644 --- a/test/functional/apps/dashboard/group6/share.ts +++ b/test/functional/apps/dashboard/group6/share.ts @@ -9,11 +9,94 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; +type TestingModes = 'snapshot' | 'savedObject'; +type AppState = string | undefined; +interface UrlState { + globalState: string; + appState: AppState; +} + +const getStateFromUrl = (url: string): UrlState => { + const globalStateStart = url.indexOf('_g'); + const appStateStart = url.indexOf('_a'); + + // global state is always part of the URL, but app state is *not* - so, need to + // modify the logic depending on whether app state exists or not + if (appStateStart === -1) { + return { + globalState: url.substring(globalStateStart + 3), + appState: undefined, + }; + } + return { + globalState: url.substring(globalStateStart + 3, appStateStart - 1), + appState: url.substring(appStateStart + 3, url.length), + }; +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const filterBar = getService('filterBar'); const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['dashboard', 'common', 'share']); + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + const PageObjects = getPageObjects(['dashboard', 'common', 'share', 'timePicker']); + + const getSharedUrl = async (mode: TestingModes): Promise => { + await retry.waitFor('share menu to open', async () => { + await PageObjects.share.clickShareTopNavButton(); + return await PageObjects.share.isShareMenuOpen(); + }); + if (mode === 'savedObject') { + await PageObjects.share.exportAsSavedObject(); + } + const sharedUrl = await PageObjects.share.getSharedUrl(); + return sharedUrl; + }; describe('share dashboard', () => { + const testFilterState = async (mode: TestingModes) => { + it('should not have "filters" state in either app or global state when no filters', async () => { + expect(await getSharedUrl(mode)).to.not.contain('filters'); + }); + + it('unpinned filter should show up only in app state when dashboard is unsaved', async () => { + await filterBar.addFilter('geo.src', 'is', 'AE'); + await PageObjects.dashboard.waitForRenderComplete(); + + const sharedUrl = await getSharedUrl(mode); + const { globalState, appState } = getStateFromUrl(sharedUrl); + expect(globalState).to.not.contain('filters'); + if (mode === 'snapshot') { + expect(appState).to.contain('filters'); + } else { + expect(sharedUrl).to.not.contain('appState'); + } + }); + + it('unpinned filters should be removed from app state when dashboard is saved', async () => { + await PageObjects.dashboard.clickQuickSave(); + await PageObjects.dashboard.waitForRenderComplete(); + + const sharedUrl = await getSharedUrl(mode); + expect(sharedUrl).to.not.contain('appState'); + }); + + it('pinned filter should show up only in global state', async () => { + await filterBar.toggleFilterPinned('geo.src'); + await PageObjects.dashboard.clickQuickSave(); + await PageObjects.dashboard.waitForRenderComplete(); + + const sharedUrl = await getSharedUrl(mode); + const { globalState, appState } = getStateFromUrl(sharedUrl); + expect(globalState).to.contain('filters'); + if (mode === 'snapshot') { + expect(appState).to.not.contain('filters'); + } + }); + }; + before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.importExport.load( @@ -25,16 +108,58 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('few panels'); + + await PageObjects.dashboard.switchToEditMode(); + const from = 'Sep 19, 2017 @ 06:31:44.000'; + const to = 'Sep 23, 2018 @ 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(from, to); + await PageObjects.dashboard.waitForRenderComplete(); }); after(async () => { await kibanaServer.savedObjects.cleanStandardList(); }); - it('has "panels" state when sharing a snapshot', async () => { - await PageObjects.share.clickShareTopNavButton(); - const sharedUrl = await PageObjects.share.getSharedUrl(); - expect(sharedUrl).to.contain('panels'); + describe('snapshot share', async () => { + describe('test local state', async () => { + it('should not have "panels" state when not in unsaved changes state', async () => { + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + expect(await getSharedUrl('snapshot')).to.not.contain('panels'); + }); + + it('should have "panels" in app state when a panel has been modified', async () => { + await dashboardPanelActions.setCustomPanelTitle('Test New Title'); + await PageObjects.dashboard.waitForRenderComplete(); + await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); + + const sharedUrl = await getSharedUrl('snapshot'); + const { appState } = getStateFromUrl(sharedUrl); + expect(appState).to.contain('panels'); + }); + + it('should once again not have "panels" state when save is clicked', async () => { + await PageObjects.dashboard.clickQuickSave(); + await PageObjects.dashboard.waitForRenderComplete(); + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + expect(await getSharedUrl('snapshot')).to.not.contain('panels'); + }); + }); + + describe('test filter state', async () => { + await testFilterState('snapshot'); + }); + + after(async () => { + await filterBar.removeAllFilters(); + await PageObjects.dashboard.clickQuickSave(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + }); + + describe('saved object share', async () => { + describe('test filter state', async () => { + await testFilterState('savedObject'); + }); }); }); } From d01cf3f78925ac5faafbd17dc0bbf44560b21c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 17 Oct 2022 19:00:02 +0200 Subject: [PATCH 14/74] [Enterprise Search] Add simulate pipeline step for Inference Pipeline Modal (#142783) * Add simulate pipeline view to the inference pipeline modal * Type fixes * Add test base * Review changes * Fix tests and types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../api/mappings/mappings_logic.ts | 8 +- .../create_ml_inference_pipeline.test.ts | 2 +- .../ml_models/create_ml_inference_pipeline.ts | 8 +- .../api/ml_models/ml_models_logic.ts | 6 +- ...mulate_ml_inference_pipeline_processors.ts | 44 +++++ .../add_ml_inference_pipeline_modal.scss | 18 ++ .../add_ml_inference_pipeline_modal.tsx | 18 +- .../ml_inference/ml_inference_logic.test.ts | 166 ++++++++++++++++++ .../ml_inference/ml_inference_logic.ts | 143 +++++++++++++-- .../pipelines/ml_inference/test_pipeline.tsx | 108 +++++++++++- .../shared/api_logic/create_api_logic.ts | 2 +- .../routes/enterprise_search/indices.test.ts | 4 +- .../routes/enterprise_search/indices.ts | 4 +- 13 files changed, 497 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/mappings/mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/mappings/mappings_logic.ts index c9530c29b50c3..e0e3db0a2d599 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/mappings/mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/mappings/mappings_logic.ts @@ -10,7 +10,13 @@ import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/ import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; -export const getMappings = async ({ indexName }: { indexName: string }) => { +export interface GetMappingsArgs { + indexName: string; +} + +export type GetMappingsResponse = IndicesGetMappingIndexMappingRecord; + +export const getMappings = async ({ indexName }: GetMappingsArgs) => { const route = `/internal/enterprise_search/mappings/${indexName}`; return await HttpLogic.values.http.get(route); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.test.ts index 9750be9166647..d3a544d4d8c46 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.test.ts @@ -34,7 +34,7 @@ describe('CreateMlInferencePipelineApiLogic', () => { expect(http.post).toHaveBeenCalledWith( '/internal/enterprise_search/indices/unit-test-index/ml_inference/pipeline_processors', { - body: '{"pipeline_name":"unit-test","model_id":"test-model","source_field":"body"}', + body: '{"model_id":"test-model","pipeline_name":"unit-test","source_field":"body"}', } ); expect(result).toEqual({ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.ts index 3935cfa30e9f8..ee5e7dd1c4295 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/create_ml_inference_pipeline.ts @@ -8,11 +8,11 @@ import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; export interface CreateMlInferencePipelineApiLogicArgs { + destinationField?: string; indexName: string; - pipelineName: string; modelId: string; + pipelineName: string; sourceField: string; - destinationField?: string; } export interface CreateMlInferencePipelineResponse { @@ -24,10 +24,10 @@ export const createMlInferencePipeline = async ( ): Promise => { const route = `/internal/enterprise_search/indices/${args.indexName}/ml_inference/pipeline_processors`; const params = { - pipeline_name: args.pipelineName, + destination_field: args.destinationField, model_id: args.modelId, + pipeline_name: args.pipelineName, source_field: args.sourceField, - destination_field: args.destinationField, }; return await HttpLogic.values.http.post(route, { body: JSON.stringify(params), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts index 0f0ead7bb0642..a0d86a821afd3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts @@ -9,7 +9,11 @@ import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_ import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; -export const getMLModels = async (size: number = 1000) => { +export type GetMlModelsArgs = number | undefined; + +export type GetMlModelsResponse = TrainedModelConfigResponse[]; + +export const getMLModels = async (size: GetMlModelsArgs = 1000) => { return await HttpLogic.values.http.get('/api/ml/trained_models', { query: { size, with_pipelines: true }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts new file mode 100644 index 0000000000000..18c90fbd7e6c1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/simulate_ml_inference_pipeline_processors.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { MlInferencePipeline } from '../../../../../common/types/pipelines'; + +import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; + +import { HttpLogic } from '../../../shared/http'; + +export interface SimulateMlInterfacePipelineArgs { + docs: string; + indexName: string; + pipeline: MlInferencePipeline; +} +export type SimulateMlInterfacePipelineResponse = IngestSimulateResponse; + +export const simulateMlInferencePipeline = async ({ + docs, + indexName, + pipeline, +}: SimulateMlInterfacePipelineArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/ml_inference/pipeline_processors/simulate`; + + return await HttpLogic.values.http.post(route, { + body: JSON.stringify({ + docs, + pipeline: { + description: pipeline.description, + processors: pipeline.processors, + }, + }), + }); +}; + +export const SimulateMlInterfacePipelineApiLogic = createApiLogic( + ['simulate_ml_inference_pipeline_api_logic'], + simulateMlInferencePipeline +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss new file mode 100644 index 0000000000000..cd3c318635932 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.scss @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +.enterpriseSearchInferencePipelineModal { + width: $euiSizeXXL * 20; + + .resizableContainer { + min-height: $euiSizeXL * 10; + + .reviewCodeBlock { + height: 100%; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx index 99faa4920108c..edbf18f8b009c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_ml_inference_pipeline_modal.tsx @@ -43,6 +43,8 @@ import { NoModelsPanel } from './no_models'; import { ReviewPipeline } from './review_pipeline'; import { TestPipeline } from './test_pipeline'; +import './add_ml_inference_pipeline_modal.scss'; + interface AddMLInferencePipelineModalProps { onClose: () => void; } @@ -57,7 +59,7 @@ export const AddMLInferencePipelineModal: React.FC +

@@ -141,7 +143,10 @@ const ModalSteps: React.FC = () => { ), }, { - onClick: () => setAddInferencePipelineStep(AddInferencePipelineSteps.Test), + onClick: () => { + if (!isPipelineDataValid) return; + setAddInferencePipelineStep(AddInferencePipelineSteps.Test); + }, status: isPipelineDataValid ? 'incomplete' : 'disabled', title: i18n.translate( 'xpack.enterpriseSearch.content.indices.transforms.addInferencePipelineModal.steps.test.title', @@ -151,7 +156,10 @@ const ModalSteps: React.FC = () => { ), }, { - onClick: () => setAddInferencePipelineStep(AddInferencePipelineSteps.Review), + onClick: () => { + if (!isPipelineDataValid) return; + setAddInferencePipelineStep(AddInferencePipelineSteps.Review); + }, status: isPipelineDataValid ? 'incomplete' : 'disabled', title: i18n.translate( 'xpack.enterpriseSearch.content.indices.transforms.addInferencePipelineModal.steps.review.title', @@ -227,14 +235,16 @@ const ModalFooter: React.FC setAddInferencePipelineStep(nextStep as AddInferencePipelineSteps)} disabled={!isPipelineDataValid} + fill > {CONTINUE_BUTTON_LABEL} ) : ( {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts new file mode 100644 index 0000000000000..f0222becb7961 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts @@ -0,0 +1,166 @@ +/* + * 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 { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { HttpError, Status } from '../../../../../../../common/types/api'; + +import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic'; +import { CreateMlInferencePipelineApiLogic } from '../../../../api/ml_models/create_ml_inference_pipeline'; +import { MLModelsApiLogic } from '../../../../api/ml_models/ml_models_logic'; +import { SimulateMlInterfacePipelineApiLogic } from '../../../../api/pipelines/simulate_ml_inference_pipeline_processors'; + +import { + MLInferenceLogic, + EMPTY_PIPELINE_CONFIGURATION, + AddInferencePipelineSteps, +} from './ml_inference_logic'; + +const DEFAULT_VALUES = { + addInferencePipelineModal: { + configuration: { + ...EMPTY_PIPELINE_CONFIGURATION, + }, + indexName: '', + simulateBody: ` +[ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + }, + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "baz" + } + } +]`, + step: AddInferencePipelineSteps.Configuration, + }, + createErrors: [], + formErrors: { + modelID: 'Field is required.', + pipelineName: 'Field is required.', + sourceField: 'Field is required.', + }, + index: undefined, + isLoading: true, + isPipelineDataValid: false, + mappingData: undefined, + mappingStatus: 0, + mlInferencePipeline: undefined, + mlModelsData: undefined, + mlModelsStatus: 0, + simulatePipelineData: undefined, + simulatePipelineErrors: [], + simulatePipelineResult: undefined, + simulatePipelineStatus: 0, + sourceFields: undefined, + supportedMLModels: undefined, +}; + +describe('MlInferenceLogic', () => { + const { mount } = new LogicMounter(MLInferenceLogic); + const { mount: mountMappingApiLogic } = new LogicMounter(MappingsApiLogic); + const { mount: mountMLModelsApiLogic } = new LogicMounter(MLModelsApiLogic); + const { mount: mountSimulateMlInterfacePipelineApiLogic } = new LogicMounter( + SimulateMlInterfacePipelineApiLogic + ); + const { mount: mountCreateMlInferencePipelineApiLogic } = new LogicMounter( + CreateMlInferencePipelineApiLogic + ); + + beforeEach(() => { + jest.clearAllMocks(); + mountMappingApiLogic(); + mountMLModelsApiLogic(); + mountSimulateMlInterfacePipelineApiLogic(); + mountCreateMlInferencePipelineApiLogic(); + mount(); + }); + + it('has expected default values', () => { + expect(MLInferenceLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setSimulatePipelineErrors', () => { + it('sets simulatePipelineErrors to passed payload', () => { + expect(MLInferenceLogic.values).toEqual(DEFAULT_VALUES); + + MLInferenceLogic.actions.setSimulatePipelineErrors([ + 'I would be an error coming from Backend', + 'I would be another one', + ]); + + expect(MLInferenceLogic.values).toEqual({ + ...DEFAULT_VALUES, + simulatePipelineErrors: [ + 'I would be an error coming from Backend', + 'I would be another one', + ], + }); + }); + }); + }); + + describe('selectors', () => { + describe('simulatePipelineResult', () => { + it('returns undefined if simulatePipelineStatus is not success', () => { + SimulateMlInterfacePipelineApiLogic.actions.apiError({} as HttpError); + expect(MLInferenceLogic.values).toEqual({ + ...DEFAULT_VALUES, + simulatePipelineErrors: ['An unexpected error occurred'], + simulatePipelineResult: undefined, + simulatePipelineStatus: Status.ERROR, + }); + }); + it('returns simulation result when API is successful', () => { + const simulateResponse = { + docs: [ + { + doc: { + _id: 'id', + _index: 'index', + _ingest: { timestamp: '2022-10-06T10:28:54.3326245Z' }, + _source: { + _ingest: { + inference_errors: [ + { + message: + "Processor 'inference' in pipeline 'test' failed with message 'Input field [text_field] does not exist in the source document'", + pipeline: 'guy', + timestamp: '2022-10-06T10:28:54.332624500Z', + }, + ], + processors: [ + { + model_version: '8.6.0', + pipeline: 'guy', + processed_timestamp: '2022-10-06T10:28:54.332624500Z', + types: ['pytorch', 'ner'], + }, + ], + }, + _version: '-3', + foo: 'bar', + }, + }, + }, + ], + }; + SimulateMlInterfacePipelineApiLogic.actions.apiSuccess(simulateResponse); + + expect(MLInferenceLogic.values.simulatePipelineResult).toEqual(simulateResponse); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts index 082644b12c6ea..6488ca0723142 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts @@ -8,6 +8,7 @@ import { kea, MakeLogicType } from 'kea'; import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; +import { IngestSimulateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models'; @@ -15,22 +16,41 @@ import { formatPipelineName, generateMlInferencePipelineBody, } from '../../../../../../../common/ml_inference_pipeline'; -import { HttpError, Status } from '../../../../../../../common/types/api'; +import { Status } from '../../../../../../../common/types/api'; import { MlInferencePipeline } from '../../../../../../../common/types/pipelines'; +import { Actions } from '../../../../../shared/api_logic/create_api_logic'; import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors'; import { FetchIndexApiLogic, FetchIndexApiResponse, } from '../../../../api/index/fetch_index_api_logic'; -import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic'; -import { CreateMlInferencePipelineApiLogic } from '../../../../api/ml_models/create_ml_inference_pipeline'; -import { MLModelsApiLogic } from '../../../../api/ml_models/ml_models_logic'; +import { + GetMappingsArgs, + GetMappingsResponse, + MappingsApiLogic, +} from '../../../../api/mappings/mappings_logic'; +import { + CreateMlInferencePipelineApiLogic, + CreateMlInferencePipelineApiLogicArgs, + CreateMlInferencePipelineResponse, +} from '../../../../api/ml_models/create_ml_inference_pipeline'; +import { + GetMlModelsArgs, + GetMlModelsResponse, + MLModelsApiLogic, +} from '../../../../api/ml_models/ml_models_logic'; +import { + SimulateMlInterfacePipelineApiLogic, + SimulateMlInterfacePipelineArgs, + SimulateMlInterfacePipelineResponse, +} from '../../../../api/pipelines/simulate_ml_inference_pipeline_processors'; import { isConnectorIndex } from '../../../../utils/indices'; import { isSupportedMLModel, sortSourceFields } from '../../../shared/ml_inference/utils'; import { AddInferencePipelineFormErrors, InferencePipelineConfiguration } from './types'; + import { validateInferencePipelineConfiguration } from './utils'; export const EMPTY_PIPELINE_CONFIGURATION: InferencePipelineConfiguration = { @@ -50,14 +70,27 @@ const API_REQUEST_COMPLETE_STATUSES = [Status.SUCCESS, Status.ERROR]; const DEFAULT_CONNECTOR_FIELDS = ['body', 'title', 'id', 'type', 'url']; interface MLInferenceProcessorsActions { - createApiError: (error: HttpError) => HttpError; - createApiSuccess: typeof CreateMlInferencePipelineApiLogic.actions.apiSuccess; + createApiError: Actions< + CreateMlInferencePipelineApiLogicArgs, + CreateMlInferencePipelineResponse + >['apiError']; + createApiSuccess: Actions< + CreateMlInferencePipelineApiLogicArgs, + CreateMlInferencePipelineResponse + >['apiSuccess']; createPipeline: () => void; - makeCreatePipelineRequest: typeof CreateMlInferencePipelineApiLogic.actions.makeRequest; - makeMLModelsRequest: typeof MLModelsApiLogic.actions.makeRequest; - makeMappingRequest: typeof MappingsApiLogic.actions.makeRequest; - mappingsApiError(error: HttpError): HttpError; - mlModelsApiError(error: HttpError): HttpError; + makeCreatePipelineRequest: Actions< + CreateMlInferencePipelineApiLogicArgs, + CreateMlInferencePipelineResponse + >['makeRequest']; + makeMLModelsRequest: Actions['makeRequest']; + makeMappingRequest: Actions['makeRequest']; + makeSimulatePipelineRequest: Actions< + SimulateMlInterfacePipelineArgs, + SimulateMlInterfacePipelineResponse + >['makeRequest']; + mappingsApiError: Actions['apiError']; + mlModelsApiError: Actions['apiError']; setAddInferencePipelineStep: (step: AddInferencePipelineSteps) => { step: AddInferencePipelineSteps; }; @@ -66,11 +99,25 @@ interface MLInferenceProcessorsActions { setInferencePipelineConfiguration: (configuration: InferencePipelineConfiguration) => { configuration: InferencePipelineConfiguration; }; + setPipelineSimulateBody: (simulateBody: string) => { + simulateBody: string; + }; + setSimulatePipelineErrors(errors: string[]): { errors: string[] }; + simulatePipeline: () => void; + simulatePipelineApiError: Actions< + SimulateMlInterfacePipelineArgs, + SimulateMlInterfacePipelineResponse + >['apiError']; + simulatePipelineApiSuccess: Actions< + SimulateMlInterfacePipelineArgs, + SimulateMlInterfacePipelineResponse + >['apiSuccess']; } export interface AddInferencePipelineModal { configuration: InferencePipelineConfiguration; indexName: string; + simulateBody: string; step: AddInferencePipelineSteps; } @@ -78,14 +125,18 @@ interface MLInferenceProcessorsValues { addInferencePipelineModal: AddInferencePipelineModal; createErrors: string[]; formErrors: AddInferencePipelineFormErrors; + index: FetchIndexApiResponse; isLoading: boolean; isPipelineDataValid: boolean; - index: FetchIndexApiResponse; mappingData: typeof MappingsApiLogic.values.data; mappingStatus: Status; mlInferencePipeline?: MlInferencePipeline; mlModelsData: typeof MLModelsApiLogic.values.data; - mlModelsStatus: typeof MLModelsApiLogic.values.apiStatus; + mlModelsStatus: Status; + simulatePipelineData: typeof SimulateMlInterfacePipelineApiLogic.values.data; + simulatePipelineErrors: string[]; + simulatePipelineResult: IngestSimulateResponse; + simulatePipelineStatus: Status; sourceFields: string[] | undefined; supportedMLModels: typeof MLModelsApiLogic.values.data; } @@ -103,6 +154,11 @@ export const MLInferenceLogic = kea< setInferencePipelineConfiguration: (configuration: InferencePipelineConfiguration) => ({ configuration, }), + setPipelineSimulateBody: (simulateBody: string) => ({ + simulateBody, + }), + setSimulatePipelineErrors: (errors: string[]) => ({ errors }), + simulatePipeline: true, }, connect: { actions: [ @@ -110,6 +166,12 @@ export const MLInferenceLogic = kea< ['makeRequest as makeMappingRequest', 'apiError as mappingsApiError'], MLModelsApiLogic, ['makeRequest as makeMLModelsRequest', 'apiError as mlModelsApiError'], + SimulateMlInterfacePipelineApiLogic, + [ + 'makeRequest as makeSimulatePipelineRequest', + 'apiSuccess as simulatePipelineApiSuccess', + 'apiError as simulatePipelineApiError', + ], CreateMlInferencePipelineApiLogic, [ 'apiError as createApiError', @@ -124,6 +186,8 @@ export const MLInferenceLogic = kea< ['data as mappingData', 'status as mappingStatus'], MLModelsApiLogic, ['data as mlModelsData', 'status as mlModelsStatus'], + SimulateMlInterfacePipelineApiLogic, + ['data as simulatePipelineData', 'status as simulatePipelineStatus'], ], }, events: {}, @@ -134,14 +198,14 @@ export const MLInferenceLogic = kea< } = values; actions.makeCreatePipelineRequest({ - indexName, - pipelineName: configuration.pipelineName, - modelId: configuration.modelID, - sourceField: configuration.sourceField, destinationField: configuration.destinationField.trim().length > 0 ? configuration.destinationField : undefined, + indexName, + modelId: configuration.modelID, + pipelineName: configuration.pipelineName, + sourceField: configuration.sourceField, }); }, makeCreatePipelineRequest: () => actions.setCreateErrors([]), @@ -149,6 +213,16 @@ export const MLInferenceLogic = kea< actions.makeMLModelsRequest(undefined); actions.makeMappingRequest({ indexName }); }, + simulatePipeline: () => { + if (values.mlInferencePipeline) { + actions.setSimulatePipelineErrors([]); + actions.makeSimulatePipelineRequest({ + docs: values.addInferencePipelineModal.simulateBody, + indexName: values.addInferencePipelineModal.indexName, + pipeline: values.mlInferencePipeline, + }); + } + }, }), path: ['enterprise_search', 'content', 'pipelines_add_ml_inference_pipeline'], reducers: { @@ -158,6 +232,23 @@ export const MLInferenceLogic = kea< ...EMPTY_PIPELINE_CONFIGURATION, }, indexName: '', + simulateBody: ` +[ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + }, + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "baz" + } + } +]`, step: AddInferencePipelineSteps.Configuration, }, { @@ -167,6 +258,10 @@ export const MLInferenceLogic = kea< ...modal, configuration, }), + setPipelineSimulateBody: (modal, { simulateBody }) => ({ + ...modal, + simulateBody, + }), }, ], createErrors: [ @@ -176,6 +271,13 @@ export const MLInferenceLogic = kea< setCreateErrors: (_, { errors }) => errors, }, ], + simulatePipelineErrors: [ + [], + { + setSimulatePipelineErrors: (_, { errors }) => errors, + simulatePipelineApiError: (_, error) => getErrorsFromHttpResponse(error), + }, + ], }, selectors: ({ selectors }) => ({ formErrors: [ @@ -217,6 +319,13 @@ export const MLInferenceLogic = kea< }); }, ], + simulatePipelineResult: [ + () => [selectors.simulatePipelineStatus, selectors.simulatePipelineData], + (status: Status, simulateResult: IngestSimulateResponse | undefined) => { + if (status !== Status.SUCCESS) return undefined; + return simulateResult; + }, + ], sourceFields: [ () => [selectors.mappingStatus, selectors.mappingData, selectors.index], ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx index 523973ad2d3d1..bd5b561426cfa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx @@ -7,6 +7,112 @@ import React from 'react'; +import { useValues, useActions } from 'kea'; + +import { + EuiCodeBlock, + EuiResizableContainer, + EuiButton, + EuiText, + EuiFlexGroup, + EuiFlexItem, + useIsWithinMaxBreakpoint, + EuiSpacer, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; + +import { MLInferenceLogic } from './ml_inference_logic'; + +import './add_ml_inference_pipeline_modal.scss'; + export const TestPipeline: React.FC = () => { - return
Test Pipeline
; + const { + addInferencePipelineModal: { simulateBody }, + simulatePipelineResult, + simulatePipelineErrors, + } = useValues(MLInferenceLogic); + const { simulatePipeline, setPipelineSimulateBody } = useActions(MLInferenceLogic); + + const isSmallerViewport = useIsWithinMaxBreakpoint('s'); + + return ( + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.title', + { defaultMessage: 'Review pipeline results (optional)' } + )} +

+
+
+ + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + { + setPipelineSimulateBody(value); + }} + /> + + + + + + + {simulatePipelineErrors.length > 0 + ? JSON.stringify(simulatePipelineErrors, null, 2) + : JSON.stringify(simulatePipelineResult || '', null, 2)} + + + + )} + + + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.description', + { + defaultMessage: + 'You can simulate your pipeline results by passing an array of documents.', + } + )} +

+
+
+ +
+ + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.runButton', + { defaultMessage: 'Simulate Pipeline' } + )} + +
+
+
+
+
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts index 6cb35fed997dd..b8f6aac6f8624 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts @@ -44,7 +44,7 @@ export const createApiLogic = ( } }, }), - path: ['enterprise_search', ...path], + path: ['enterprise_search', 'api', ...path], reducers: () => ({ apiStatus: [ { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts index 6732867af59b4..d0a652b12d9c2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts @@ -363,7 +363,7 @@ describe('Enterprise Search Managed Indices', () => { }); }); - describe('POST /internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/_simulate', () => { + describe('POST /internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/simulate', () => { const pipelineBody = { description: 'Some pipeline', processors: [ @@ -395,7 +395,7 @@ describe('Enterprise Search Managed Indices', () => { mockRouter = new MockRouter({ context, method: 'post', - path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/_simulate', + path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/simulate', }); registerIndexRoutes({ diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index 4e0b0706d09de..e1d2f0238740c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -484,14 +484,14 @@ export function registerIndexRoutes({ router.post( { - path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/_simulate', + path: '/internal/enterprise_search/indices/{indexName}/ml_inference/pipeline_processors/simulate', validate: { body: schema.object({ + docs: schema.arrayOf(schema.any()), pipeline: schema.object({ description: schema.maybe(schema.string()), processors: schema.arrayOf(schema.any()), }), - docs: schema.arrayOf(schema.any()), }), params: schema.object({ indexName: schema.string(), From 41d88e66770bb28baad45096837df0572d4e330d Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 17 Oct 2022 12:42:17 -0500 Subject: [PATCH 15/74] [DOCS] Vis Editors 8.5 (#142520) * [DOCS] Vis Editors 8.5 * Review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...vancedTutorial_numberOfCustomers_8.5.0.png | Bin 0 -> 144772 bytes ...ercentageNumberOfOrdersByCategory_7.16.png | Bin 76275 -> 0 bytes .../images/lens_layerActions_8.5.0.png | Bin 0 -> 1329 bytes docs/user/dashboard/lens-advanced.asciidoc | 34 +++++++++------- docs/user/dashboard/lens.asciidoc | 37 ++++++++---------- docs/user/dashboard/tsvb.asciidoc | 4 +- 6 files changed, 38 insertions(+), 37 deletions(-) create mode 100644 docs/user/dashboard/images/lens_advancedTutorial_numberOfCustomers_8.5.0.png delete mode 100644 docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png create mode 100644 docs/user/dashboard/images/lens_layerActions_8.5.0.png diff --git a/docs/user/dashboard/images/lens_advancedTutorial_numberOfCustomers_8.5.0.png b/docs/user/dashboard/images/lens_advancedTutorial_numberOfCustomers_8.5.0.png new file mode 100644 index 0000000000000000000000000000000000000000..ec3085da6365356ed20cbe5790cbd807bcaaf681 GIT binary patch literal 144772 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�^{ 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 VK#`=$F9Q6>+{E%4>WXvpe*r*GUDN;o literal 0 HcmV?d00001 diff --git a/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png b/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png deleted file mode 100644 index 6addc8bc276e964e56235ad73d09db12ef3006b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76275 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 XrCS?Pze7?_F&;f_!-pmJZA1SLMJR83 diff --git a/docs/user/dashboard/images/lens_layerActions_8.5.0.png b/docs/user/dashboard/images/lens_layerActions_8.5.0.png new file mode 100644 index 0000000000000000000000000000000000000000..ca8a92a36cdedb4f92f980fb193b895989f4dcc1 GIT binary patch literal 1329 zcmeAS@N?(olHy`uVBq!ia0vp^(m>45!3HEnCvUmIz`z)n>FgZf>Flf!P?VpRnUl)E zpfRy@!d`EuM3H0l%Mv>#IJ>$uJ3BYK#w=c>8luFcc+qan+bUDuKRaT23#OMXJh*b{ zv8Ze=m0MEXZHsh&v9nB{bbDu1rmOy(?>|3&m~;4{!+iIvU)M07cW z^*c5gA5gLj)qd6Jp`s(W@5}RSyM)bO@=nbPJ$d)u6*sBF{}$NpDcf55&;0-6os(8B zQv0Q6y8NXZXU^tB6PqXH6}ncOn6R`ZTWq;?O=J-C+IAh!_uWvZ5bCcG7m47d1=YDV1SP*yoU1ySlem#Z()q%RgE~XRcci@%>-z@pa)At*vS|R>$fWvP^F`_;ur}T3=#7UTNXl z)os6OW1}wz-ubF6Tc+>$e{-gp+QG=yu&XBb{d_i;Dt(^6AnqT-l_M$DLNhgw0|R_o zYJ_K+uP=iZkj=rs$|%IZ3}i6^Ap@fn11p%#z~IFw4QB^2YCzR6F)*}eGO$3^L;-0K z@Bm_sf~dh$P)tMmIX|3)zS-?;cR}O9D`%9v!^GJ4o)sgP0cIL$S-O5{w^9Q zz**oCS9LmNm2q7CdXh;=p~!>j@_D^hbJT{3flK@D`8ouR1>*cJ>4ggYS`LyJ?3obz*m z`bz8&Q3chEEQziWY-0q%Mk8}%8<8ZCG=e-}B`WRwR^)~vT;6(~< zNL+$Nfstv)Wup&I7PT^4f^ns=6 l4O0! Normalize by unit*. +.. Click *Advanced*. .. From the *Normalize by unit* dropdown, select *per hour*, then click *Close*. + -*Normalize unit* converts *Average sales per 12 hours* into *Average sales per 12 hours (per hour)* by dividing the number of hours. +*Normalize by unit* converts `Count of Records` into `Count of records per hour` by dividing by 24. + +.. In the *Name* field, enter `Number of orders`. + +.. Click *Close*. . To hide the *Horizontal axis* label, open the *Bottom Axis* menu, then select *None* from the *Axis title* dropdown. @@ -73,13 +75,13 @@ To identify the 75th percentile of orders, add a reference line: . Click *Static value*. -.. Click *Quick functions*, then click *Percentile*. +.. Click *Quick function*, then click *Percentile*. .. From the *Field* dropdown, select *total_quantity*. -.. In the *Percentile* field, enter `75`. +.. In the *Reference line value* field, enter `75`. -. Configure the display options. +. Configure the *Appearance* options. .. In the *Name* field, enter `75th`. @@ -168,9 +170,11 @@ Add a layer to display the customer traffic: .. In the *Name* field, enter `Number of customers`. -.. In the *Series color* field, enter *#D36086*. +.. In the *Series color* field, enter `#D36086`. .. Click *Right* for the *Axis side*, then click *Close*. ++ +image::images/lens_advancedTutorial_numberOfCustomers_8.5.0.png[Number of customers area chart in Lens] . From the *Available fields* list, drag *order_date* to the *Horizontal Axis* field in the second layer. @@ -202,7 +206,7 @@ To view change over time as a percentage, create an *Area percentage* chart that For each order category, create a filter: -. In the layer pane, click *Add or drag-and-drop a field* for *Break down by*. +. In the layer pane, click *Add or drag-and-drop a field* for *Breakdown*. . Click the *Filters* function. @@ -255,7 +259,7 @@ Configure the cumulative sum of store orders: Filter the results to display the data for only Saturday and Sunday: -. In the layer pane, click *Add or drag-and-drop a field* for *Break down by*. +. In the layer pane, click *Add or drag-and-drop a field* for *Breakdown*. . Click the *Filters* function. @@ -294,7 +298,7 @@ To create a week-over-week comparison, shift *Count of Records [1]* by one week: . In the layer pane, click *Count of Records [1]*. -. Click *Add advanced options > Time shift*, select *1 week ago*, then click *Close*. +. Click *Advanced*, select *1 week ago* from the *Time shift* dropdown, then click *Close*. + To use custom time shifts, enter the time value and increment, then press Enter. For example, enter *1w* to use the *1 week ago* time shift. + @@ -322,9 +326,11 @@ To compare time range changes as a percent, create a bar chart that compares the . Click *Formula*, then enter `count() / count(shift='1w') - 1`. -. Open the *Value format* dropdown, select *Percent*, then enter `0` in the *Decimals* field. +. In the *Name* field, enter `Percent of change`. -. In the *Name* field, enter `Percent of change`, then click *Close*. +. From the *Value format* dropdown, select *Percent*, then enter `0` in the *Decimals* field. + +. Click *Close*. + [role="screenshot"] image::images/lens_percent_chage.png[Bar chart with percent change in sales between the current time and the previous week] @@ -359,7 +365,7 @@ Create a date histogram table and group the customer count metric by category, s To split the metric, add columns for each continent using the *Columns* field: -. From the *Available fields* list, drag *geoip.continent_name* to the *Columns* field in the layer pane. +. From the *Available fields* list, drag *geoip.continent_name* to the *Split metrics by* field in the layer pane. + [role="screenshot"] image::images/lens_table_over_time.png[Date histogram table with groups for the customer count metric] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 80e1665753c15..6c695cd3a74a9 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -15,21 +15,6 @@ With *Lens*, you can: * Use time shifts to compare the data in two time intervals, such as month over month. * Add annotations and reference lines. - -++++ - - -
-++++ - [float] [[create-the-visualization-panel]] ==== Create visualizations @@ -50,7 +35,7 @@ Choose the data you want to visualize. . If you want to learn more about the data a field contains, click the field. -. To visualize more than one {data-source}, click *Add layer > Visualization*, then select the {data-source}. +. To visualize more than one {data-source}, click *Add layer*, select the layer type, then select the {data-source}. Edit and delete. @@ -58,6 +43,10 @@ Edit and delete. . To delete a field, close the configuration options, then click *X* next to the field. +. To clone a layer, click image:dashboard/images/lens_layerActions_8.5.0.png[Actions menu to duplicate Lens visualization layers] in the layer pane, then select *Duplicate layer*. + +. To clear the layer configuration, click image:dashboard/images/lens_layerActions_8.5.0.png[Actions menu to clear Lens visualization layers] in the layer pane, then select *Clear layer*. + TIP: You can manually apply the changes you make, which is helpful when creating large and complex visualizations. To manually apply your changes, click *Settings* in the toolbar, then deselect *Auto-apply visualization changes*. [float] @@ -95,12 +84,16 @@ All columns that belong to the same layer pane group are sorted in the table. * *Name* — Specifies the field display name. +* *Collapse by* — Aggregates all metric values with the same value into a single number. + * *Value format* — Specifies how the field value displays in the table. * *Text alignment* — Aligns the values in the cell to the *Left*, *Center*, or *Right*. * *Hide column* — Hides the column for the field. +* *Directly filter on click* — Turns column values into clickable links that allow you to filter or drill down into the data. + * *Summary row* — Adds a row that displays the summary value. When specified, allows you to enter a *Summary label*. * *Color by value* — Applies color to the cell or text values. To change the color, click *Edit*. @@ -169,13 +162,13 @@ TIP: For detailed information on formulas, click image:dashboard/images/formula_ [[compare-data-with-time-offsets]] ==== Compare differences over time -Compare your real-time data set to the results that are offset by a time increment. For example, you can compare the real-time percentage of a user CPU time spent to the results offset by one hour. +Compare your real-time data to the results that are offset by a time increment. For example, you can compare the real-time percentage of a user CPU time spent to the results offset by one hour. . In the layer pane, click the field you want to offset. -. From the *Add advanced options* dropdown, select *Time shift*. +. Click *Advanced*. -. Select the time offset increment. +. In the *Time shift* field, enter the time offset increment. For a time shift example, refer to <>. @@ -185,13 +178,15 @@ For a time shift example, refer to <>. preview::[] -Annotations allow you to call out specific points in your visualizations that are important, such as a major change in the data. You can add text and icons to annotations and customize the appearance, such as the line format and color. +Annotations allow you to call out specific points in your visualizations that are important, such as a major change in the data. You can add annotations for any {data-source}, add text and icons, specify the line format and color, and more. [role="screenshot"] image::images/lens_annotations_8.2.0.png[Lens annotations] . In the layer pane, click *Add layer > Annotations*. +. Select the {data-source}. + . To open the annotation options, click *Event*. . Specify the *Annotation date*. @@ -202,7 +197,7 @@ image::images/lens_annotations_8.2.0.png[Lens annotations] . Change the *Appearance* options for how you want the annotation to display. -. Click *Close*. +. To close, click *X*. [float] [[add-reference-lines]] diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 07bb653af9750..d9cd137978e9c 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -134,9 +134,9 @@ The *Markdown* visualization supports Markdown with Handlebar (mustache) syntax [float] [[edit-visualizations-in-lens]] -==== Edit visualizations in Lens +==== Open and edit TSVB visualizations in Lens -Open and edit your Time Series *TSVB* visualizations in *Lens*, which is the drag-and-drop visualization editor that provides you with additional visualization types, reference lines, and more. +Open and edit Time Series and Top N *TSVB* visualizations in *Lens*. When you open *TSVB* visualizations in *Lens*, all configuration options and annotations appear in the *Lens* visualization editor. To get started, click *Edit visualization in Lens* in the toolbar. From 227288e7264134cbb04fcdcb54f9e93ee8056708 Mon Sep 17 00:00:00 2001 From: Kurt Date: Mon, 17 Oct 2022 14:00:46 -0400 Subject: [PATCH 16/74] Adding testdataloader method to remove all SO from the kibana index (#143400) * Adding testdataloader method to remove all SO from the kibana index * Changing call order per PR review feedback --- x-pack/test/common/lib/test_data_loader.ts | 24 +++++++++++++++++++ .../common/suites/bulk_create.ts | 2 +- .../common/suites/bulk_get.ts | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/x-pack/test/common/lib/test_data_loader.ts b/x-pack/test/common/lib/test_data_loader.ts index 61c8ff4c1bf52..64a69a5ac8170 100644 --- a/x-pack/test/common/lib/test_data_loader.ts +++ b/x-pack/test/common/lib/test_data_loader.ts @@ -68,6 +68,7 @@ export function getTestDataLoader({ getService }) { const kbnServer = getService('kibanaServer'); const supertest = getService('supertest'); const log = getService('log'); + const es = getService('es'); return { createFtrSpaces: async () => { @@ -124,5 +125,28 @@ export function getTestDataLoader({ getService }) { ]) ); }, + + deleteAllSavedObjectsFromKibanaIndex: async () => { + await es.deleteByQuery({ + index: '.kibana', + wait_for_completion: true, + body: { + conflicts: 'proceed', + query: { + bool: { + must_not: [ + { + term: { + type: { + value: 'space', + }, + }, + }, + ], + }, + }, + }, + }); + }, }; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index bb0bd27ce85d9..9d88842f2b0fd 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -219,8 +219,8 @@ export function bulkCreateTestSuiteFactory(context: FtrProviderContext) { }); after(async () => { + await testDataLoader.deleteAllSavedObjectsFromKibanaIndex(); await testDataLoader.deleteFtrSpaces(); - await testDataLoader.deleteFtrSavedObjectsData(); }); const attrs = { attributes: { [NEW_ATTRIBUTE_KEY]: NEW_ATTRIBUTE_VAL } }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index c9cb3b9739eee..15a51c3db3364 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -117,8 +117,8 @@ export function bulkGetTestSuiteFactory(context: FtrProviderContext) { }); after(async () => { - await testDataLoader.deleteFtrSpaces(); await testDataLoader.deleteFtrSavedObjectsData(); + await testDataLoader.deleteFtrSpaces(); }); for (const test of tests) { From b00f3b0ab956a1223b140cb62a815a3f51389642 Mon Sep 17 00:00:00 2001 From: doakalexi <109488926+doakalexi@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:33:06 -0700 Subject: [PATCH 17/74] [ResponseOps][Alerting] create new logger with tag for rule/connector type, for logger given to executors (#142121) * Updating connector logger * Updating rule logger * Fixing type failure * Fixing failing tests * Fixing other type failure * Fixing types * Fixing more types * Making logger optional * Removing change * Fixing errors * Fixing preview routes * Fixing tests and types * Updating substrings * Use logger in runRule function * Fixing task runner tests * Updating logger in constructor * Linting fix * Fixing action logger --- .../server/lib/action_executor.test.ts | 7 +++- .../actions/server/lib/action_executor.ts | 5 ++- .../sub_action_framework/executor.test.ts | 9 +++++ x-pack/plugins/actions/server/types.ts | 2 ++ .../server/task_runner/task_runner.test.ts | 1 + .../server/task_runner/task_runner.ts | 4 ++- .../task_runner/task_runner_cancel.test.ts | 6 ++-- x-pack/plugins/alerting/server/types.ts | 2 ++ .../metric_threshold_executor.test.ts | 27 +++++++------- .../utils/create_lifecycle_executor.test.ts | 6 ++++ .../utils/create_lifecycle_rule_type.test.ts | 1 + .../utils/rule_executor.test_helpers.ts | 4 +++ ...gacy_rules_notification_alert_type.test.ts | 1 + .../routes/rules/preview_rules_route.ts | 6 ++-- .../rule_types/__mocks__/rule_type.ts | 1 + .../security_solution/server/routes/index.ts | 3 +- .../server/alert_types/es_query/executor.ts | 10 ++---- .../server/alert_types/es_query/index.ts | 7 ++-- .../alert_types/es_query/rule_type.test.ts | 3 +- .../server/alert_types/es_query/rule_type.ts | 5 ++- .../alert_types/geo_containment/alert_type.ts | 6 ++-- .../geo_containment/geo_containment.ts | 12 +++---- .../alert_types/geo_containment/index.ts | 6 ++-- .../geo_containment/tests/alert_type.test.ts | 5 +-- .../tests/geo_containment.test.ts | 11 +++--- .../alert_types/index_threshold/index.ts | 6 ++-- .../index_threshold/rule_type.test.ts | 6 +++- .../alert_types/index_threshold/rule_type.ts | 4 +-- .../cases/cases_webhook/index.ts | 16 +++------ .../connector_types/cases/jira/index.ts | 15 ++------ .../connector_types/cases/resilient/index.ts | 16 ++------- .../cases/servicenow_itsm/index.test.ts | 5 ++- .../cases/servicenow_itsm/index.ts | 19 ++++------ .../cases/servicenow_sir/index.test.ts | 5 ++- .../cases/servicenow_sir/index.ts | 19 ++++------ .../connector_types/cases/swimlane/index.ts | 17 ++------- .../server/connector_types/index.test.ts | 4 --- .../server/connector_types/index.ts | 33 ++++++++--------- .../connector_types/stack/email/index.test.ts | 7 ++-- .../connector_types/stack/email/index.ts | 17 +++------ .../stack/es_index/index.test.ts | 36 +++++++++++++++---- .../connector_types/stack/es_index/index.ts | 13 +++---- .../stack/pagerduty/index.test.ts | 24 ++++++++----- .../connector_types/stack/pagerduty/index.ts | 16 +++------ .../stack/server_log/index.test.ts | 5 ++- .../connector_types/stack/server_log/index.ts | 9 ++--- .../stack/servicenow_itom/index.ts | 18 +++------- .../connector_types/stack/slack/index.test.ts | 29 +++++++-------- .../connector_types/stack/slack/index.ts | 12 ++----- .../connector_types/stack/teams/index.test.ts | 6 ++-- .../connector_types/stack/teams/index.ts | 13 +++---- .../stack/webhook/index.test.ts | 7 ++-- .../connector_types/stack/webhook/index.ts | 15 ++++---- .../stack/xmatters/index.test.ts | 7 ++-- .../connector_types/stack/xmatters/index.ts | 15 ++++---- .../plugins/stack_connectors/server/plugin.ts | 9 ++--- 56 files changed, 256 insertions(+), 317 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 54405b03e7c02..35791ffa01f23 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -35,7 +35,9 @@ const executeParams = { }; const spacesMock = spacesServiceMock.createStartContract(); -const loggerMock = loggingSystemMock.create().get(); +const loggerMock: ReturnType = + loggingSystemMock.createLogger(); + const getActionsClientWithRequest = jest.fn(); actionExecutor.initialize({ logger: loggerMock, @@ -52,6 +54,7 @@ beforeEach(() => { jest.resetAllMocks(); spacesMock.getSpaceId.mockReturnValue('some-namespace'); getActionsClientWithRequest.mockResolvedValue(actionsClient); + loggerMock.get.mockImplementation(() => loggerMock); }); test('successfully executes', async () => { @@ -109,6 +112,7 @@ test('successfully executes', async () => { baz: true, }, params: { foo: true }, + logger: loggerMock, }); expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1'); @@ -482,6 +486,7 @@ test('should not throws an error if actionType is preconfigured', async () => { baz: true, }, params: { foo: true }, + logger: loggerMock, }); }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 00a3980833ef2..26d0a55b07dc6 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -121,7 +121,6 @@ export class ActionExecutor { }, async (span) => { const { - logger, spaces, getServices, encryptedSavedObjectsClient, @@ -144,6 +143,9 @@ export class ActionExecutor { ); const { actionTypeId, name, config, secrets } = actionInfo; + const loggerId = actionTypeId.startsWith('.') ? actionTypeId.substring(1) : actionTypeId; + let { logger } = this.actionExecutorContext!; + logger = logger.get(loggerId); if (!this.actionInfo || this.actionInfo.actionId !== actionId) { this.actionInfo = actionInfo; @@ -228,6 +230,7 @@ export class ActionExecutor { isEphemeral, taskInfo, configurationUtilities, + logger, }); } catch (err) { if (err.reason === ActionExecutionErrorReason.Validation) { diff --git a/x-pack/plugins/actions/server/sub_action_framework/executor.test.ts b/x-pack/plugins/actions/server/sub_action_framework/executor.test.ts index 5fc07c4b6f236..92467f049ae3f 100644 --- a/x-pack/plugins/actions/server/sub_action_framework/executor.test.ts +++ b/x-pack/plugins/actions/server/sub_action_framework/executor.test.ts @@ -65,6 +65,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }); expect(res).toEqual({ @@ -86,6 +87,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }); expect(res).toEqual({ @@ -107,6 +109,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }); expect(res).toEqual({ @@ -126,6 +129,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }); expect(res).toEqual({ @@ -146,6 +150,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }) ).rejects.toThrowError('You should register at least one subAction for your connector type'); }); @@ -161,6 +166,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }) ).rejects.toThrowError( 'Sub action "not-exist" is not registered. Connector id: test-action-id. Connector name: Test. Connector type: .test' @@ -178,6 +184,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }) ).rejects.toThrowError( 'Method "not-exist" does not exists in service. Sub action: "testUrl". Connector id: test-action-id. Connector name: Test. Connector type: .test' @@ -195,6 +202,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }) ).rejects.toThrowError( 'Method "notAFunction" must be a function. Connector id: test-action-id. Connector name: Test. Connector type: .test' @@ -212,6 +220,7 @@ describe('Executor', () => { secrets, services, configurationUtilities: mockedActionsConfig, + logger, }) ).rejects.toThrowError( 'Request validation failed (Error: [id]: expected value of type [string] but got [undefined])' diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 3806dae00c237..147a71da0c960 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -14,6 +14,7 @@ import { ElasticsearchClient, CustomRequestHandlerContext, SavedObjectReference, + Logger, } from '@kbn/core/server'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; @@ -60,6 +61,7 @@ export interface ActionTypeExecutorOptions { config: Config; secrets: Secrets; params: Params; + logger: Logger; isEphemeral?: boolean; taskInfo?: TaskInfo; configurationUtilities: ActionsConfigurationUtilities; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index a199fb3b55998..20310fdae01f7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -202,6 +202,7 @@ describe('Task Runner', () => { alertingEventLogger.getStartAndDuration.mockImplementation(() => ({ start: new Date() })); (AlertingEventLogger as jest.Mock).mockImplementation(() => alertingEventLogger); + logger.get.mockImplementation(() => logger); }); test('successfully executes the task', async () => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 41029af567524..0c6bbfbbd9866 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -136,7 +136,8 @@ export class TaskRunner< inMemoryMetrics: InMemoryMetrics ) { this.context = context; - this.logger = context.logger; + const loggerId = ruleType.id.startsWith('.') ? ruleType.id.substring(1) : ruleType.id; + this.logger = context.logger.get(loggerId); this.usageCounter = context.usageCounter; this.ruleType = ruleType; this.ruleConsumer = null; @@ -392,6 +393,7 @@ export class TaskRunner< throttle, notifyWhen, }, + logger: this.logger, }) ); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 7b920e3957421..92353cb043984 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -64,6 +64,7 @@ let fakeTimer: sinon.SinonFakeTimers; const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const alertingEventLogger = alertingEventLoggerMock.create(); +const logger: ReturnType = loggingSystemMock.createLogger(); describe('Task Runner Cancel', () => { let mockedTaskInstance: ConcreteTaskInstance; @@ -110,7 +111,7 @@ describe('Task Runner Cancel', () => { actionsPlugin: actionsMock.createStart(), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), encryptedSavedObjectsClient, - logger: loggingSystemMock.create().get(), + logger, executionContext: executionContextServiceMock.createInternalStartContract(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), basePathService: httpServiceMock.createBasePath(), @@ -170,6 +171,7 @@ describe('Task Runner Cancel', () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertingEventLogger.getStartAndDuration.mockImplementation(() => ({ start: new Date() })); (AlertingEventLogger as jest.Mock).mockImplementation(() => alertingEventLogger); + logger.get.mockImplementation(() => logger); }); test('updates rule saved object execution status and writes to event log entry when task is cancelled mid-execution', async () => { @@ -186,7 +188,6 @@ describe('Task Runner Cancel', () => { await taskRunner.cancel(); await promise; - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenNthCalledWith( 3, `Aborting any in-progress ES searches for rule type test with id 1` @@ -390,7 +391,6 @@ describe('Task Runner Cancel', () => { }); function testLogger() { - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(7); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 632d0489a9bb7..f1917a079a26d 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -17,6 +17,7 @@ import { IScopedClusterClient, SavedObjectAttributes, SavedObjectsClientContract, + Logger, } from '@kbn/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; @@ -103,6 +104,7 @@ export interface RuleExecutorOptions< tags: string[]; createdBy: string | null; updatedBy: string | null; + logger: Logger; } export interface RuleParamsAndRefs { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 08870ddfb6f94..b345a83ab69d1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -55,6 +55,19 @@ const initialRuleState: TestRuleState = { groups: [], }; +const fakeLogger = (msg: string, meta?: Meta) => {}; + +const logger = { + trace: fakeLogger, + debug: fakeLogger, + info: fakeLogger, + warn: fakeLogger, + error: fakeLogger, + fatal: fakeLogger, + log: () => void 0, + get: () => logger, +} as unknown as Logger; + const mockOptions = { alertId: '', executionId: '', @@ -99,6 +112,7 @@ const mockOptions = { ruleTypeId: '', ruleTypeName: '', }, + logger, }; const setEvaluationResults = (response: Array>) => { @@ -1607,19 +1621,6 @@ const createMockStaticConfiguration = (sources: any) => ({ sources, }); -const fakeLogger = (msg: string, meta?: Meta) => {}; - -const logger = { - trace: fakeLogger, - debug: fakeLogger, - info: fakeLogger, - warn: fakeLogger, - error: fakeLogger, - fatal: fakeLogger, - log: () => void 0, - get: () => logger, -} as unknown as Logger; - const mockLibs: any = { sources: new InfraSources({ config: createMockStaticConfiguration({}), diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index f0190ef7adf67..b4aa184cab592 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -49,6 +49,7 @@ describe('createLifecycleExecutor', () => { createDefaultAlertExecutorOptions({ params: {}, state: { wrapped: initialRuleState, trackedAlerts: {} }, + logger, }) ); @@ -83,6 +84,7 @@ describe('createLifecycleExecutor', () => { createDefaultAlertExecutorOptions({ params: {}, state: { wrapped: initialRuleState, trackedAlerts: {} }, + logger, }) ); @@ -198,6 +200,7 @@ describe('createLifecycleExecutor', () => { }, }, }, + logger, }) ); @@ -313,6 +316,7 @@ describe('createLifecycleExecutor', () => { }, }, }, + logger, }) ); @@ -372,6 +376,7 @@ describe('createLifecycleExecutor', () => { params: {}, state: { wrapped: initialRuleState, trackedAlerts: {} }, shouldWriteAlerts: false, + logger, }) ); @@ -401,6 +406,7 @@ describe('createLifecycleExecutor', () => { params: {}, state: { wrapped: initialRuleState, trackedAlerts: {} }, shouldWriteAlerts: false, + logger, }) ) ).rejects.toThrowErrorMatchingInlineSnapshot(`"error initializing!"`); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index acb12645cbaed..f71c7391cec77 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -131,6 +131,7 @@ function createRule(shouldWriteAlerts: boolean = true) { updatedBy: 'updatedBy', namespace: 'namespace', executionId: 'b33f65d7-6e8b-4aae-8d20-c93613dec9f9', + logger: loggerMock.create(), })) ?? {}) as Record; previousStartedAt = startedAt; diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts index b2c25973f7cc4..4a9ab6652ec6e 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts @@ -18,6 +18,7 @@ import { } from '@kbn/alerting-plugin/server'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { Logger } from '@kbn/logging'; export const createDefaultAlertExecutorOptions = < Params extends RuleTypeParams = never, @@ -30,6 +31,7 @@ export const createDefaultAlertExecutorOptions = < ruleName = 'ALERT_RULE_NAME', params, state, + logger, createdAt = new Date(), startedAt = new Date(), updatedAt = new Date(), @@ -39,6 +41,7 @@ export const createDefaultAlertExecutorOptions = < ruleName?: string; params: Params; state: State; + logger: Logger; createdAt?: Date; startedAt?: Date; updatedAt?: Date; @@ -83,4 +86,5 @@ export const createDefaultAlertExecutorOptions = < previousStartedAt: null, namespace: undefined, executionId: 'b33f65d7-6e8b-4aae-8d20-c93613deb33f', + logger, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts index 89d760c4e6eeb..b79e8ac4cbbdb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts @@ -70,6 +70,7 @@ describe('legacyRules_notification_alert_type', () => { throttle: null, notifyWhen: null, }, + logger, }; alert = legacyRulesNotificationAlertType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 15b6ffe47d349..9a1ba3a2b144c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import uuid from 'uuid'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { StartServicesAccessor } from '@kbn/core/server'; +import type { Logger, StartServicesAccessor } from '@kbn/core/server'; import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import type { AlertInstanceContext, @@ -64,7 +64,8 @@ export const previewRulesRoute = async ( ruleOptions: CreateRuleOptions, securityRuleTypeOptions: CreateSecurityRuleTypeWrapperProps, previewRuleDataClient: IRuleDataClient, - getStartServices: StartServicesAccessor + getStartServices: StartServicesAccessor, + logger: Logger ) => { router.post( { @@ -251,6 +252,7 @@ export const previewRulesRoute = async ( state: statePreview, tags: [], updatedBy: rule.updatedBy, + logger, })) as TState; const errors = loggedStatusChanges diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 96ca9e9d5ef40..bfe25804de29a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -114,6 +114,7 @@ export const createRuleTypeMocks = ( params, alertId: v4(), state: {}, + logger: loggerMock, }), runOpts: { completeRule: getCompleteRuleMock(params as QueryRuleParams), diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 485047721ab18..f9d2177aa9e10 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -118,7 +118,8 @@ export const initRoutes = ( ruleOptions, securityRuleTypeOptions, previewRuleDataClient, - getStartServices + getStartServices, + logger ); createRuleExceptionsRoute(router); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index b52f5803405a7..f50368f29ae76 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -6,7 +6,7 @@ */ import { sha256 } from 'js-sha256'; import { i18n } from '@kbn/i18n'; -import { CoreSetup, Logger } from '@kbn/core/server'; +import { CoreSetup } from '@kbn/core/server'; import { parseDuration } from '@kbn/alerting-plugin/server'; import { addMessages, EsQueryRuleActionContext } from './action_context'; import { ComparatorFns, getHumanReadableComparator } from '../lib'; @@ -18,13 +18,9 @@ import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; import { Comparator } from '../../../common/comparator_types'; import { isEsQueryRule } from './util'; -export async function executor( - logger: Logger, - core: CoreSetup, - options: ExecutorOptions -) { +export async function executor(core: CoreSetup, options: ExecutorOptions) { const esQueryRule = isEsQueryRule(options.params.searchType); - const { alertId: ruleId, name, services, params, state, spaceId } = options; + const { alertId: ruleId, name, services, params, state, spaceId, logger } = options; const { alertFactory, scopedClusterClient, searchSourceClient } = services; const currentTimestamp = new Date().toISOString(); const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts index 54bfabdf49ad6..a92b5be3a9d63 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts @@ -5,17 +5,16 @@ * 2.0. */ -import { CoreSetup, Logger } from '@kbn/core/server'; +import { CoreSetup } from '@kbn/core/server'; import { AlertingSetup } from '../../types'; import { getRuleType } from './rule_type'; interface RegisterParams { - logger: Logger; alerting: AlertingSetup; core: CoreSetup; } export function register(params: RegisterParams) { - const { logger, alerting, core } = params; - alerting.registerType(getRuleType(logger, core)); + const { alerting, core } = params; + alerting.registerType(getRuleType(core)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts index 18397e4515e7b..b647531222407 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts @@ -27,7 +27,7 @@ import { Comparator } from '../../../common/comparator_types'; const logger = loggingSystemMock.create().get(); const coreSetup = coreMock.createSetup(); -const ruleType = getRuleType(logger, coreSetup); +const ruleType = getRuleType(coreSetup); describe('ruleType', () => { it('rule type creation structure is the expected value', async () => { @@ -678,5 +678,6 @@ async function invokeExecutor({ throttle: null, notifyWhen: null, }, + logger, }); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts index 27e79e86fb3c3..c56f691cc2580 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Logger } from '@kbn/core/server'; +import { CoreSetup } from '@kbn/core/server'; import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; import { RuleType } from '../../types'; import { ActionContext } from './action_context'; @@ -23,7 +23,6 @@ import { executor } from './executor'; import { isEsQueryRule } from './util'; export function getRuleType( - logger: Logger, core: CoreSetup ): RuleType< EsQueryRuleParams, @@ -184,7 +183,7 @@ export function getRuleType( minimumLicenseRequired: 'basic', isExportable: true, executor: async (options: ExecutorOptions) => { - return await executor(logger, core, options); + return await executor(core, options); }, producer: STACK_ALERTS_FEATURE_ID, doesSetRecoveryContext: true, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts index 95e7376a6febb..4a0c89531c880 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { Logger, SavedObjectReference } from '@kbn/core/server'; +import { SavedObjectReference } from '@kbn/core/server'; import { RuleType, RuleTypeState, @@ -214,7 +214,7 @@ export function injectEntityAndBoundaryIds( } as GeoContainmentParams; } -export function getAlertType(logger: Logger): GeoContainmentAlertType { +export function getAlertType(): GeoContainmentAlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.geoContainment.alertTypeTitle', { defaultMessage: 'Tracking containment', }); @@ -238,7 +238,7 @@ export function getAlertType(logger: Logger): GeoContainmentAlertType { }, doesSetRecoveryContext: true, defaultActionGroupId: ActionGroupId, - executor: getGeoContainmentExecutor(logger), + executor: getGeoContainmentExecutor(), producer: STACK_ALERTS_FEATURE_ID, validate: { params: ParamsSchema, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index efa8c869ed405..fd86dc3d3800d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -6,7 +6,6 @@ */ import _ from 'lodash'; -import { Logger } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder'; @@ -138,7 +137,7 @@ export function getEntitiesAndGenerateAlerts( return { activeEntities, inactiveEntities }; } -export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType['executor'] => +export const getGeoContainmentExecutor = (): GeoContainmentAlertType['executor'] => async function ({ previousStartedAt: windowStart, startedAt: windowEnd, @@ -146,6 +145,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ params, alertId, state, + logger, }): Promise { const { shapesFilters, shapesIdsNamesMap } = state.shapesFilters ? state @@ -154,7 +154,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ params.boundaryGeoField, params.geoField, services.scopedClusterClient.asCurrentUser, - log, + logger, alertId, params.boundaryNameField, params.boundaryIndexQuery @@ -163,14 +163,14 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ const executeEsQuery = await executeEsQueryFactory( params, services.scopedClusterClient.asCurrentUser, - log, + logger, shapesFilters ); // Start collecting data only on the first cycle let currentIntervalResults: estypes.SearchResponse | undefined; if (!windowStart) { - log.debug(`alert ${GEO_CONTAINMENT_ID}:${alertId} alert initialized. Collecting data`); + logger.debug(`alert ${GEO_CONTAINMENT_ID}:${alertId} alert initialized. Collecting data`); // Consider making first time window configurable? const START_TIME_WINDOW = 1; const tempPreviousEndTime = new Date(windowEnd); @@ -213,7 +213,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ recoveredAlert.setContext(context); } } catch (e) { - log.warn(`Unable to set alert context for recovered alert, error: ${e.message}`); + logger.warn(`Unable to set alert context for recovered alert, error: ${e.message}`); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts index 072a01efc9749..f5b9ebc5e85cd 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Logger } from '@kbn/core/server'; import { AlertingSetup } from '../../types'; import { GeoContainmentState, @@ -19,12 +18,11 @@ import { import { GeoContainmentExtractedParams, GeoContainmentParams } from './alert_type'; interface RegisterParams { - logger: Logger; alerting: AlertingSetup; } export function register(params: RegisterParams) { - const { logger, alerting } = params; + const { alerting } = params; alerting.registerType< GeoContainmentParams, GeoContainmentExtractedParams, @@ -33,5 +31,5 @@ export function register(params: RegisterParams) { GeoContainmentInstanceContext, typeof ActionGroupId, typeof RecoveryActionGroupId - >(getAlertType(logger)); + >(getAlertType()); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts index 1f08564a4d668..9a67a1280f730 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { loggingSystemMock } from '@kbn/core/server/mocks'; import { getAlertType, injectEntityAndBoundaryIds, @@ -14,9 +13,7 @@ import { } from '../alert_type'; describe('alertType', () => { - const logger = loggingSystemMock.create().get(); - - const alertType = getAlertType(logger); + const alertType = getAlertType(); it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.geo-containment'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 82dac9ba6ff9a..e4d4e49036f4e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -6,7 +6,7 @@ */ import _ from 'lodash'; -import { loggingSystemMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks'; import sampleAggsJsonResponse from './es_sample_response.json'; import sampleShapesJsonResponse from './es_sample_response_shapes.json'; @@ -505,7 +505,6 @@ describe('geo_containment', () => { }, ]; const testAlertActionArr: unknown[] = []; - const mockLogger = loggingSystemMock.createLogger(); const previousStartedAt = new Date('2021-04-27T16:56:11.923Z'); const startedAt = new Date('2021-04-29T16:56:11.923Z'); const geoContainmentParams: GeoContainmentParams = { @@ -560,7 +559,7 @@ describe('geo_containment', () => { }); it('should query for shapes if state does not contain shapes', async () => { - const executor = await getGeoContainmentExecutor(mockLogger); + const executor = await getGeoContainmentExecutor(); // @ts-ignore const executionResult = await executor({ previousStartedAt, @@ -580,7 +579,7 @@ describe('geo_containment', () => { }); it('should not query for shapes if state contains shapes', async () => { - const executor = await getGeoContainmentExecutor(mockLogger); + const executor = await getGeoContainmentExecutor(); // @ts-ignore const executionResult = await executor({ previousStartedAt, @@ -599,7 +598,7 @@ describe('geo_containment', () => { }); it('should carry through shapes filters in state to next call unmodified', async () => { - const executor = await getGeoContainmentExecutor(mockLogger); + const executor = await getGeoContainmentExecutor(); // @ts-ignore const executionResult = await executor({ previousStartedAt, @@ -635,7 +634,7 @@ describe('geo_containment', () => { }, ], }; - const executor = await getGeoContainmentExecutor(mockLogger); + const executor = await getGeoContainmentExecutor(); // @ts-ignore const executionResult = await executor({ previousStartedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts index 449c6528798a6..dbae7907dd9f6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Logger } from '@kbn/core/server'; import { AlertingSetup, StackAlertsStartDeps } from '../../types'; import { getRuleType } from './rule_type'; @@ -15,12 +14,11 @@ export const MAX_GROUPS = 1000; export const DEFAULT_GROUPS = 100; interface RegisterParams { - logger: Logger; data: Promise; alerting: AlertingSetup; } export function register(params: RegisterParams) { - const { logger, data, alerting } = params; - alerting.registerType(getRuleType(logger, data)); + const { data, alerting } = params; + alerting.registerType(getRuleType(data)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts index adb722ef094d2..f7704e4699930 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.test.ts @@ -26,7 +26,7 @@ describe('ruleType', () => { }; const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); - const ruleType = getRuleType(logger, Promise.resolve(data)); + const ruleType = getRuleType(Promise.resolve(data)); beforeAll(() => { fakeTimer = sinon.useFakeTimers(); @@ -220,6 +220,7 @@ describe('ruleType', () => { throttle: null, notifyWhen: null, }, + logger, }); expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents'); @@ -286,6 +287,7 @@ describe('ruleType', () => { throttle: null, notifyWhen: null, }, + logger, }); expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); @@ -352,6 +354,7 @@ describe('ruleType', () => { throttle: null, notifyWhen: null, }, + logger, }); expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); @@ -417,6 +420,7 @@ describe('ruleType', () => { throttle: null, notifyWhen: null, }, + logger, }); expect(data.timeSeriesQuery).toHaveBeenCalledWith( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts index 32f30c2db437d..2fcb36b267d6d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/rule_type.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { Logger } from '@kbn/core/server'; import { CoreQueryParamsSchemaProperties, TimeSeriesQuery, @@ -23,7 +22,6 @@ export const ID = '.index-threshold'; export const ActionGroupId = 'threshold met'; export function getRuleType( - logger: Logger, data: Promise ): RuleType { const ruleTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', { @@ -136,7 +134,7 @@ export function getRuleType( async function executor( options: RuleExecutorOptions ) { - const { alertId: ruleId, name, services, params } = options; + const { alertId: ruleId, name, services, params, logger } = options; const { alertFactory, scopedClusterClient } = services; const alertLimit = alertFactory.alertLimit.getValue(); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/cases_webhook/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/cases_webhook/index.ts index 3b32e79ac725c..9bfb66f1a69aa 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/cases_webhook/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/cases_webhook/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { curry } from 'lodash'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -35,11 +33,7 @@ const supportedSubActions: string[] = ['pushToService']; export type ActionParamsType = CasesWebhookActionParamsType; export const ConnectorTypeId = '.cases-webhook'; // connector type definition -export function getConnectorType({ - logger, -}: { - logger: Logger; -}): ConnectorType< +export function getConnectorType(): ConnectorType< CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, ExecutorParams, @@ -63,23 +57,21 @@ export function getConnectorType({ }, connector: validate.connector, }, - executor: curry(executor)({ logger }), + executor, supportedFeatureIds: [CasesConnectorFeatureId], }; } // action executor export async function executor( - { logger }: { logger: Logger }, execOptions: ConnectorTypeExecutorOptions< CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, CasesWebhookActionParamsType > ): Promise> { - const actionId = execOptions.actionId; - const configurationUtilities = execOptions.configurationUtilities; - const { subAction, subActionParams } = execOptions.params; + const { actionId, configurationUtilities, params, logger } = execOptions; + const { subAction, subActionParams } = params; let data: CasesWebhookExecutorResultData | undefined; const externalService = createExternalService( diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/jira/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/jira/index.ts index d2130c085cda1..630c0973935cd 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/jira/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/jira/index.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { curry } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -43,9 +41,6 @@ import { import * as i18n from './translations'; export type ActionParamsType = TypeOf; -interface GetConnectorTypeParams { - logger: Logger; -} const supportedSubActions: string[] = [ 'getFields', @@ -59,15 +54,12 @@ const supportedSubActions: string[] = [ export const ConnectorTypeId = '.jira'; // connector type definition -export function getConnectorType( - params: GetConnectorTypeParams -): ConnectorType< +export function getConnectorType(): ConnectorType< JiraPublicConfigurationType, JiraSecretConfigurationType, ExecutorParams, JiraExecutorResultData | {} > { - const { logger } = params; return { id: ConnectorTypeId, minimumLicenseRequired: 'gold', @@ -91,20 +83,19 @@ export function getConnectorType( schema: ExecutorParamsSchema, }, }, - executor: curry(executor)({ logger }), + executor, }; } // action executor async function executor( - { logger }: { logger: Logger }, execOptions: ConnectorTypeExecutorOptions< JiraPublicConfigurationType, JiraSecretConfigurationType, ExecutorParams > ): Promise> { - const { actionId, config, params, secrets, configurationUtilities } = execOptions; + const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions; const { subAction, subActionParams } = params as ExecutorParams; let data: JiraExecutorResultData | null = null; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/resilient/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/resilient/index.ts index 081fbe92502d9..7ef85b84bfb86 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/resilient/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/resilient/index.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { curry } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -41,23 +39,16 @@ import * as i18n from './translations'; export type ActionParamsType = TypeOf; -interface GetConnectorTypeParams { - logger: Logger; -} - const supportedSubActions: string[] = ['getFields', 'pushToService', 'incidentTypes', 'severity']; export const ConnectorTypeId = '.resilient'; // connector type definition -export function getConnectorType( - params: GetConnectorTypeParams -): ConnectorType< +export function getConnectorType(): ConnectorType< ResilientPublicConfigurationType, ResilientSecretConfigurationType, ExecutorParams, ResilientExecutorResultData | {} > { - const { logger } = params; return { id: ConnectorTypeId, minimumLicenseRequired: 'platinum', @@ -80,20 +71,19 @@ export function getConnectorType( schema: ExecutorParamsSchema, }, }, - executor: curry(executor)({ logger }), + executor, }; } // action executor async function executor( - { logger }: { logger: Logger }, execOptions: ConnectorTypeExecutorOptions< ResilientPublicConfigurationType, ResilientSecretConfigurationType, ExecutorParams > ): Promise> { - const { actionId, config, params, secrets, configurationUtilities } = execOptions; + const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions; const { subAction, subActionParams } = params as ExecutorParams; let data: ResilientExecutorResultData | null = null; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts index 50ff4d8e0f1c9..422098e146945 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.test.ts @@ -49,9 +49,7 @@ describe('ServiceNow', () => { describe('ServiceNow ITSM', () => { let connectorType: ServiceNowConnectorType; beforeAll(() => { - connectorType = getServiceNowITSMConnectorType({ - logger: mockedLogger, - }); + connectorType = getServiceNowITSMConnectorType(); }); describe('execute()', () => { @@ -67,6 +65,7 @@ describe('ServiceNow', () => { secrets, params, services, + logger: mockedLogger, } as unknown as ServiceNowConnectorTypeExecutorOptions< ServiceNowPublicConfigurationType, ExecutorParams diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts index 40e9d1470950e..39cba22889ae2 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_itsm/index.ts @@ -8,7 +8,6 @@ import { curry } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -55,10 +54,6 @@ export { ServiceNowITSMConnectorTypeId, serviceNowITSMTable }; export type ActionParamsType = TypeOf; -interface GetConnectorTypeParams { - logger: Logger; -} - export type ServiceNowConnectorType< C extends Record = ServiceNowPublicConfigurationBaseType, T extends Record = ExecutorParams @@ -70,10 +65,10 @@ export type ServiceNowConnectorTypeExecutorOptions< > = ConnectorTypeExecutorOptions; // connector type definition -export function getServiceNowITSMConnectorType( - params: GetConnectorTypeParams -): ServiceNowConnectorType { - const { logger } = params; +export function getServiceNowITSMConnectorType(): ServiceNowConnectorType< + ServiceNowPublicConfigurationType, + ExecutorParams +> { return { id: ServiceNowITSMConnectorTypeId, minimumLicenseRequired: 'platinum', @@ -99,7 +94,6 @@ export function getServiceNowITSMConnectorType( }, }, executor: curry(executor)({ - logger, actionTypeId: ServiceNowITSMConnectorTypeId, createService: createExternalService, api: apiITSM, @@ -111,12 +105,10 @@ export function getServiceNowITSMConnectorType( const supportedSubActions: string[] = ['getFields', 'pushToService', 'getChoices', 'getIncident']; async function executor( { - logger, actionTypeId, createService, api, }: { - logger: Logger; actionTypeId: string; createService: ServiceFactory; api: ExternalServiceAPI; @@ -126,7 +118,8 @@ async function executor( ExecutorParams > ): Promise> { - const { actionId, config, params, secrets, services, configurationUtilities } = execOptions; + const { actionId, config, params, secrets, services, configurationUtilities, logger } = + execOptions; const { subAction, subActionParams } = params; const connectorTokenClient = services.connectorTokenClient; const externalServiceConfig = snExternalServiceConfig[actionTypeId]; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts index 3fff7ae0e389d..2389852334ef8 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.test.ts @@ -50,9 +50,7 @@ describe('ServiceNow', () => { let connectorType: ServiceNowConnectorType; beforeAll(() => { - connectorType = getServiceNowSIRConnectorType({ - logger: mockedLogger, - }); + connectorType = getServiceNowSIRConnectorType(); }); describe('execute()', () => { @@ -68,6 +66,7 @@ describe('ServiceNow', () => { secrets, params, services, + logger: mockedLogger, } as unknown as ServiceNowConnectorTypeExecutorOptions< ServiceNowPublicConfigurationType, ExecutorParams diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts index 16db999af8a1e..8f5cb9a86286d 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/servicenow_sir/index.ts @@ -8,7 +8,6 @@ import { curry } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -54,10 +53,6 @@ export { ServiceNowSIRConnectorTypeId, serviceNowSIRTable }; export type ActionParamsType = TypeOf; -interface GetConnectorTypeParams { - logger: Logger; -} - export type ServiceNowConnectorType< C extends Record = ServiceNowPublicConfigurationBaseType, T extends Record = ExecutorParams @@ -69,10 +64,10 @@ export type ServiceNowConnectorTypeExecutorOptions< > = ConnectorTypeExecutorOptions; // connector type definition -export function getServiceNowSIRConnectorType( - params: GetConnectorTypeParams -): ServiceNowConnectorType { - const { logger } = params; +export function getServiceNowSIRConnectorType(): ServiceNowConnectorType< + ServiceNowPublicConfigurationType, + ExecutorParams +> { return { id: ServiceNowSIRConnectorTypeId, minimumLicenseRequired: 'platinum', @@ -97,7 +92,6 @@ export function getServiceNowSIRConnectorType( }, }, executor: curry(executor)({ - logger, actionTypeId: ServiceNowSIRConnectorTypeId, createService: createExternalService, api: apiSIR, @@ -109,12 +103,10 @@ export function getServiceNowSIRConnectorType( const supportedSubActions: string[] = ['getFields', 'pushToService', 'getChoices', 'getIncident']; async function executor( { - logger, actionTypeId, createService, api, }: { - logger: Logger; actionTypeId: string; createService: ServiceFactory; api: ExternalServiceAPI; @@ -124,7 +116,8 @@ async function executor( ExecutorParams > ): Promise> { - const { actionId, config, params, secrets, services, configurationUtilities } = execOptions; + const { actionId, config, params, secrets, services, configurationUtilities, logger } = + execOptions; const { subAction, subActionParams } = params; const connectorTokenClient = services.connectorTokenClient; const externalServiceConfig = snExternalServiceConfig[actionTypeId]; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases/swimlane/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases/swimlane/index.ts index a033abd875ea4..d24febcccaad3 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases/swimlane/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases/swimlane/index.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Logger } from '@kbn/logging'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -34,23 +32,15 @@ import { import { createExternalService } from './service'; import { api } from './api'; -interface GetConnectorTypeParams { - logger: Logger; -} - const supportedSubActions: string[] = ['pushToService']; // connector type definition -export function getConnectorType( - params: GetConnectorTypeParams -): ConnectorType< +export function getConnectorType(): ConnectorType< SwimlanePublicConfigurationType, SwimlaneSecretConfigurationType, ExecutorParams, SwimlaneExecutorResultData | {} > { - const { logger } = params; - return { id: '.swimlane', minimumLicenseRequired: 'gold', @@ -75,19 +65,18 @@ export function getConnectorType( schema: ExecutorParamsSchema, }, }, - executor: curry(executor)({ logger }), + executor, }; } async function executor( - { logger }: { logger: Logger }, execOptions: ConnectorTypeExecutorOptions< SwimlanePublicConfigurationType, SwimlaneSecretConfigurationType, ExecutorParams > ): Promise> { - const { actionId, config, params, secrets, configurationUtilities } = execOptions; + const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions; const { subAction, subActionParams } = params as ExecutorParams; let data: SwimlaneExecutorResultData | null = null; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/index.test.ts index 55fd99c040941..ead127d7dd521 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/index.test.ts @@ -6,8 +6,6 @@ */ import { registerConnectorTypes } from '.'; -import { Logger } from '@kbn/core/server'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; const ACTION_TYPE_IDS = [ @@ -22,7 +20,6 @@ const ACTION_TYPE_IDS = [ '.xmatters', ]; -const logger = loggingSystemMock.create().get() as jest.Mocked; const mockedActions = actionsMock.createSetup(); beforeEach(() => { @@ -32,7 +29,6 @@ beforeEach(() => { describe('registers connectors', () => { test('calls registerType with expected connector types', () => { registerConnectorTypes({ - logger, actions: mockedActions, }); ACTION_TYPE_IDS.forEach((id) => 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 5aa352986939e..dd12f93bbc607 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Logger } from '@kbn/core/server'; import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server'; import { getEmailConnectorType, @@ -65,27 +64,25 @@ export { export function registerConnectorTypes({ actions, - logger, publicBaseUrl, }: { actions: ActionsPluginSetupContract; - logger: Logger; publicBaseUrl?: string; }) { - actions.registerType(getEmailConnectorType({ logger, publicBaseUrl })); - actions.registerType(getIndexConnectorType({ logger })); - actions.registerType(getPagerDutyConnectorType({ logger })); - actions.registerType(getSwimlaneConnectorType({ logger })); - actions.registerType(getServerLogConnectorType({ logger })); - actions.registerType(getSlackConnectorType({ logger })); - actions.registerType(getWebhookConnectorType({ logger })); - actions.registerType(getCasesWebhookConnectorType({ logger })); - actions.registerType(getXmattersConnectorType({ logger })); - actions.registerType(getServiceNowITSMConnectorType({ logger })); - actions.registerType(getServiceNowSIRConnectorType({ logger })); - actions.registerType(getServiceNowITOMConnectorType({ logger })); - actions.registerType(getJiraConnectorType({ logger })); - actions.registerType(getResilientConnectorType({ logger })); - actions.registerType(getTeamsConnectorType({ logger })); + actions.registerType(getEmailConnectorType({ publicBaseUrl })); + actions.registerType(getIndexConnectorType()); + actions.registerType(getPagerDutyConnectorType()); + actions.registerType(getSwimlaneConnectorType()); + actions.registerType(getServerLogConnectorType()); + actions.registerType(getSlackConnectorType({})); + actions.registerType(getWebhookConnectorType()); + actions.registerType(getCasesWebhookConnectorType()); + actions.registerType(getXmattersConnectorType()); + actions.registerType(getServiceNowITSMConnectorType()); + actions.registerType(getServiceNowSIRConnectorType()); + actions.registerType(getServiceNowITOMConnectorType()); + actions.registerType(getJiraConnectorType()); + actions.registerType(getResilientConnectorType()); + actions.registerType(getTeamsConnectorType()); actions.registerSubActionConnectorType(getOpsgenieConnectorType()); } diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.test.ts index ab5f909ecd4f7..4ce412e037df2 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.test.ts @@ -42,9 +42,7 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { jest.resetAllMocks(); configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType({}); }); describe('connector registration', () => { @@ -522,6 +520,7 @@ describe('execute()', () => { secrets, services, configurationUtilities: actionsConfigMock.create(), + logger: mockedLogger, }; test('ensure parameters are as expected', async () => { @@ -741,7 +740,6 @@ describe('execute()', () => { test('provides a footer link to Kibana when publicBaseUrl is defined', async () => { const connectorTypeWithPublicUrl = getConnectorType({ - logger: mockedLogger, publicBaseUrl: 'https://localhost:1234/foo/bar', }); @@ -760,7 +758,6 @@ describe('execute()', () => { test('allows to generate a deep link into Kibana when publicBaseUrl is defined', async () => { const connectorTypeWithPublicUrl = getConnectorType({ - logger: mockedLogger, publicBaseUrl: 'https://localhost:1234/foo/bar', }); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.ts index e146baba68fbc..4fdbc1106bb9a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/email/index.ts @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerGetService from 'nodemailer/lib/well-known'; import SMTPConnection from 'nodemailer/lib/smtp-connection'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -192,7 +191,6 @@ function validateParams(paramsObject: unknown, validatorServices: ValidatorServi } interface GetConnectorTypeParams { - logger: Logger; publicBaseUrl?: string; } @@ -218,7 +216,7 @@ function validateConnector( // connector type definition export const ConnectorTypeId = '.email'; export function getConnectorType(params: GetConnectorTypeParams): EmailConnectorType { - const { logger, publicBaseUrl } = params; + const { publicBaseUrl } = params; return { id: ConnectorTypeId, minimumLicenseRequired: 'gold', @@ -245,7 +243,7 @@ export function getConnectorType(params: GetConnectorTypeParams): EmailConnector connector: validateConnector, }, renderParameterTemplates, - executor: curry(executor)({ logger, publicBaseUrl }), + executor: curry(executor)({ publicBaseUrl }), }; } @@ -265,20 +263,15 @@ function renderParameterTemplates( async function executor( { - logger, publicBaseUrl, }: { - logger: GetConnectorTypeParams['logger']; publicBaseUrl: GetConnectorTypeParams['publicBaseUrl']; }, execOptions: EmailConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const config = execOptions.config; - const secrets = execOptions.secrets; - const params = execOptions.params; - const configurationUtilities = execOptions.configurationUtilities; - const connectorTokenClient = execOptions.services.connectorTokenClient; + const { actionId, config, secrets, params, configurationUtilities, services, logger } = + execOptions; + const connectorTokenClient = services.connectorTokenClient; const emails = params.to.concat(params.cc).concat(params.bcc); let invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.test.ts index 9968a67e87b45..d2961d4725d39 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.test.ts @@ -31,9 +31,7 @@ let configurationUtilities: ActionsConfigurationUtilities; beforeEach(() => { jest.resetAllMocks(); configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); }); describe('connector registration', () => { @@ -186,6 +184,7 @@ describe('execute()', () => { params, services, configurationUtilities, + logger: mockedLogger, }; const scopedClusterClient = elasticsearchClientMock .createClusterClient() @@ -223,7 +222,15 @@ describe('execute()', () => { indexOverride: null, }; - executorOptions = { actionId, config, secrets, params, services, configurationUtilities }; + executorOptions = { + actionId, + config, + secrets, + params, + services, + configurationUtilities, + logger: mockedLogger, + }; scopedClusterClient.bulk.mockClear(); await connectorType.executor({ ...executorOptions, @@ -265,7 +272,15 @@ describe('execute()', () => { indexOverride: null, }; - executorOptions = { actionId, config, secrets, params, services, configurationUtilities }; + executorOptions = { + actionId, + config, + secrets, + params, + services, + configurationUtilities, + logger: mockedLogger, + }; scopedClusterClient.bulk.mockClear(); await connectorType.executor({ @@ -301,7 +316,15 @@ describe('execute()', () => { indexOverride: null, }; - executorOptions = { actionId, config, secrets, params, services, configurationUtilities }; + executorOptions = { + actionId, + config, + secrets, + params, + services, + configurationUtilities, + logger: mockedLogger, + }; scopedClusterClient.bulk.mockClear(); await connectorType.executor({ ...executorOptions, @@ -612,6 +635,7 @@ describe('execute()', () => { params, services, configurationUtilities, + logger: mockedLogger, }) ).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.ts index d66f60233f49f..4f39111a4bb3d 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/es_index/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { curry, find } from 'lodash'; +import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { Logger } from '@kbn/core/server'; @@ -70,7 +70,7 @@ const ParamsSchema = schema.object({ export const ConnectorTypeId = '.index'; // connector type definition -export function getConnectorType({ logger }: { logger: Logger }): ESIndexConnectorType { +export function getConnectorType(): ESIndexConnectorType { return { id: ConnectorTypeId, minimumLicenseRequired: 'basic', @@ -90,7 +90,7 @@ export function getConnectorType({ logger }: { logger: Logger }): ESIndexConnect schema: ParamsSchema, }, }, - executor: curry(executor)({ logger }), + executor, renderParameterTemplates, }; } @@ -98,14 +98,9 @@ export function getConnectorType({ logger }: { logger: Logger }): ESIndexConnect // action executor async function executor( - { logger }: { logger: Logger }, execOptions: ESIndexConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const config = execOptions.config; - const params = execOptions.params; - const services = execOptions.services; - + const { actionId, config, params, services, logger } = execOptions; const index = params.indexOverride || config.index; const bulkBody = []; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.test.ts index eaf073f732f77..10752e53ae72a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.test.ts @@ -34,9 +34,7 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); }); describe('get()', () => { @@ -75,9 +73,7 @@ describe('validateConfig()', () => { expect(url).toEqual('https://events.pagerduty.com/v2/enqueue'); }, }; - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); expect( validateConfig( @@ -95,9 +91,7 @@ describe('validateConfig()', () => { throw new Error(`target url is not added to allowedHosts`); }, }; - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); expect(() => { validateConfig( @@ -274,6 +268,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -335,6 +330,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -401,6 +397,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -458,6 +455,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -500,6 +498,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` @@ -529,6 +528,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` @@ -558,6 +558,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` @@ -587,6 +588,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); expect(actionResponse).toMatchInlineSnapshot(` @@ -626,6 +628,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -688,6 +691,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -753,6 +757,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; @@ -817,6 +822,7 @@ describe('execute()', () => { secrets, services, configurationUtilities, + logger: mockedLogger, }; const actionResponse = await connectorType.executor(executorOptions); const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0]; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.ts index e09f6159d26d8..e2b7a6998bda3 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/pagerduty/index.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { curry, isUndefined, pick, omitBy } from 'lodash'; +import { isUndefined, pick, omitBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import moment from 'moment'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -138,7 +137,7 @@ function validateParams(paramsObject: unknown): string | void { export const ConnectorTypeId = '.pagerduty'; // connector type definition -export function getConnectorType({ logger }: { logger: Logger }): PagerDutyConnectorType { +export function getConnectorType(): PagerDutyConnectorType { return { id: ConnectorTypeId, minimumLicenseRequired: 'gold', @@ -162,7 +161,7 @@ export function getConnectorType({ logger }: { logger: Logger }): PagerDutyConne schema: ParamsSchema, }, }, - executor: curry(executor)({ logger }), + executor, }; } @@ -192,15 +191,10 @@ function getPagerDutyApiUrl(config: ConnectorTypeConfigType): string { // action executor async function executor( - { logger }: { logger: Logger }, execOptions: PagerDutyConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const config = execOptions.config; - const secrets = execOptions.secrets; - const params = execOptions.params; - const services = execOptions.services; - const configurationUtilities = execOptions.configurationUtilities; + const { actionId, config, secrets, params, services, configurationUtilities, logger } = + execOptions; const apiUrl = getPagerDutyApiUrl(config); const headers = { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.test.ts index 04c8f9e562f79..082098023fc3f 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.test.ts @@ -20,9 +20,7 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); }); describe('connectorType', () => { @@ -108,6 +106,7 @@ describe('execute()', () => { config: {}, secrets: {}, configurationUtilities, + logger: mockedLogger, }; await connectorType.executor(executorOptions); expect(mockedLogger.info).toHaveBeenCalledWith('Server log: message text here'); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.ts index 23bfe27466349..f3e1e8e8af856 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/server_log/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; @@ -49,7 +48,7 @@ const ParamsSchema = schema.object({ export const ConnectorTypeId = '.server-log'; // connector type definition -export function getConnectorType({ logger }: { logger: Logger }): ServerLogConnectorType { +export function getConnectorType(): ServerLogConnectorType { return { id: ConnectorTypeId, minimumLicenseRequired: 'basic', @@ -62,18 +61,16 @@ export function getConnectorType({ logger }: { logger: Logger }): ServerLogConne schema: ParamsSchema, }, }, - executor: curry(executor)({ logger }), + executor, }; } // action executor async function executor( - { logger }: { logger: Logger }, execOptions: ServerLogConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const params = execOptions.params; + const { actionId, params, logger } = execOptions; const sanitizedMessage = withoutControlCharacters(params.message); try { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts index c6d1c5d772899..b04b2fcbc60a8 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/servicenow_itom/index.ts @@ -7,7 +7,6 @@ import { curry } from 'lodash'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -47,10 +46,6 @@ import { createServiceWrapper } from '../../lib/servicenow/create_service_wrappe export { ServiceNowITOMConnectorTypeId }; -interface GetConnectorTypeParams { - logger: Logger; -} - export type ServiceNowConnectorType< C extends Record = ServiceNowPublicConfigurationBaseType, T extends Record = ExecutorParamsITOM @@ -62,10 +57,10 @@ export type ServiceNowConnectorTypeExecutorOptions< > = ConnectorTypeExecutorOptions; // connector type definition -export function getServiceNowITOMConnectorType( - params: GetConnectorTypeParams -): ServiceNowConnectorType { - const { logger } = params; +export function getServiceNowITOMConnectorType(): ServiceNowConnectorType< + ServiceNowPublicConfigurationBaseType, + ExecutorParamsITOM +> { return { id: ServiceNowITOMConnectorTypeId, minimumLicenseRequired: 'platinum', @@ -86,7 +81,6 @@ export function getServiceNowITOMConnectorType( }, }, executor: curry(executorITOM)({ - logger, actionTypeId: ServiceNowITOMConnectorTypeId, createService: createExternalService, api: apiITOM, @@ -98,12 +92,10 @@ export function getServiceNowITOMConnectorType( const supportedSubActionsITOM = ['addEvent', 'getChoices']; async function executorITOM( { - logger, actionTypeId, createService, api, }: { - logger: Logger; actionTypeId: string; createService: ServiceFactory; api: ExternalServiceApiITOM; @@ -113,7 +105,7 @@ async function executorITOM( ExecutorParamsITOM > ): Promise> { - const { actionId, config, params, secrets, configurationUtilities } = execOptions; + const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions; const { subAction, subActionParams } = params; const connectorTokenClient = execOptions.services.connectorTokenClient; const externalServiceConfig = snExternalServiceConfig[actionTypeId]; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.test.ts index 742cfd7ed1f60..f96cc176e467e 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.test.ts @@ -42,7 +42,6 @@ beforeEach(() => { async executor(options) { return { status: 'ok', actionId: options.actionId }; }, - logger: mockedLogger, }); }); @@ -170,7 +169,6 @@ describe('execute()', () => { connectorType = getConnectorType({ executor: mockSlackExecutor, - logger: mockedLogger, }); }); @@ -182,6 +180,7 @@ describe('execute()', () => { secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, configurationUtilities, + logger: mockedLogger, }); expect(response).toMatchInlineSnapshot(` Object { @@ -201,6 +200,7 @@ describe('execute()', () => { secrets: { webhookUrl: 'http://example.com' }, params: { message: 'failure: this invocation should fail' }, configurationUtilities, + logger: mockedLogger, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"slack mockExecutor failure: this invocation should fail"` @@ -217,9 +217,7 @@ describe('execute()', () => { proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); - const connectorTypeProxy = getConnectorType({ - logger: mockedLogger, - }); + const connectorTypeProxy = getConnectorType({}); await connectorTypeProxy.executor({ actionId: 'some-id', services, @@ -227,6 +225,7 @@ describe('execute()', () => { 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' @@ -244,9 +243,7 @@ describe('execute()', () => { proxyBypassHosts: new Set(['example.com']), proxyOnlyHosts: undefined, }); - const connectorTypeProxy = getConnectorType({ - logger: mockedLogger, - }); + const connectorTypeProxy = getConnectorType({}); await connectorTypeProxy.executor({ actionId: 'some-id', services, @@ -254,6 +251,7 @@ describe('execute()', () => { 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' @@ -271,9 +269,7 @@ describe('execute()', () => { proxyBypassHosts: new Set(['not-example.com']), proxyOnlyHosts: undefined, }); - const connectorTypeProxy = getConnectorType({ - logger: mockedLogger, - }); + const connectorTypeProxy = getConnectorType({}); await connectorTypeProxy.executor({ actionId: 'some-id', services, @@ -281,6 +277,7 @@ describe('execute()', () => { 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' @@ -298,9 +295,7 @@ describe('execute()', () => { proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['example.com']), }); - const connectorTypeProxy = getConnectorType({ - logger: mockedLogger, - }); + const connectorTypeProxy = getConnectorType({}); await connectorTypeProxy.executor({ actionId: 'some-id', services, @@ -308,6 +303,7 @@ describe('execute()', () => { 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' @@ -325,9 +321,7 @@ describe('execute()', () => { proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-example.com']), }); - const connectorTypeProxy = getConnectorType({ - logger: mockedLogger, - }); + const connectorTypeProxy = getConnectorType({}); await connectorTypeProxy.executor({ actionId: 'some-id', services, @@ -335,6 +329,7 @@ describe('execute()', () => { 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' diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.ts index 343d353443f51..11b8cc0ad7196 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/slack/index.ts @@ -6,7 +6,6 @@ */ import { URL } from 'url'; -import { curry } from 'lodash'; import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { i18n } from '@kbn/i18n'; @@ -14,7 +13,6 @@ 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 { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -65,10 +63,8 @@ const ParamsSchema = schema.object({ export const ConnectorTypeId = '.slack'; // customizing executor is only used for tests export function getConnectorType({ - logger, - executor = curry(slackExecutor)({ logger }), + executor = slackExecutor, }: { - logger: Logger; executor?: ExecutorType<{}, ConnectorTypeSecretsType, ActionParamsType, unknown>; }): SlackConnectorType { return { @@ -138,13 +134,9 @@ function validateConnectorTypeConfig( // action executor async function slackExecutor( - { logger }: { logger: Logger }, execOptions: SlackConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const secrets = execOptions.secrets; - const params = execOptions.params; - const configurationUtilities = execOptions.configurationUtilities; + const { actionId, secrets, params, configurationUtilities, logger } = execOptions; let result: IncomingWebhookResult; const { webhookUrl } = secrets; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.test.ts index a45db6021bfd7..6d1cbb2bfbbbe 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.test.ts @@ -37,9 +37,7 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); }); describe('connector registration', () => { @@ -168,6 +166,7 @@ describe('execute()', () => { secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, configurationUtilities, + logger: mockedLogger, }); delete requestMock.mock.calls[0][0].configurationUtilities; expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` @@ -222,6 +221,7 @@ describe('execute()', () => { secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, configurationUtilities, + logger: mockedLogger, }); delete requestMock.mock.calls[0][0].configurationUtilities; expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.ts index 3405f59a68f7a..df44d568a2f30 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/teams/index.ts @@ -6,13 +6,12 @@ */ import { URL } from 'url'; -import { curry, isString } from 'lodash'; +import { isString } from 'lodash'; import axios, { AxiosError, AxiosResponse } from 'axios'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -59,7 +58,7 @@ const ParamsSchema = schema.object({ export const ConnectorTypeId = '.teams'; // connector type definition -export function getConnectorType({ logger }: { logger: Logger }): TeamsConnectorType { +export function getConnectorType(): TeamsConnectorType { return { id: ConnectorTypeId, minimumLicenseRequired: 'gold', @@ -80,7 +79,7 @@ export function getConnectorType({ logger }: { logger: Logger }): TeamsConnector schema: ParamsSchema, }, }, - executor: curry(teamsExecutor)({ logger }), + executor: teamsExecutor, }; } @@ -117,13 +116,9 @@ function validateConnectorTypeConfig( // action executor async function teamsExecutor( - { logger }: { logger: Logger }, execOptions: TeamsConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const secrets = execOptions.secrets; - const params = execOptions.params; - const configurationUtilities = execOptions.configurationUtilities; + const { actionId, secrets, params, configurationUtilities, logger } = execOptions; const { webhookUrl } = secrets; const { message } = params; const data = { text: message }; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.test.ts index 5d82212e67179..4e3369af35c56 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.test.ts @@ -46,9 +46,7 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); }); describe('connectorType', () => { @@ -271,6 +269,7 @@ describe('execute()', () => { secrets: { user: 'abc', password: '123' }, params: { body: 'some data' }, configurationUtilities, + logger: mockedLogger, }); delete requestMock.mock.calls[0][0].configurationUtilities; @@ -336,6 +335,7 @@ describe('execute()', () => { secrets: { user: 'abc', password: '123' }, params: { body: 'some data' }, configurationUtilities, + logger: mockedLogger, }); expect(mockedLogger.error).toBeCalledWith( 'error on some-id webhook event: maxContentLength size of 1000000 exceeded' @@ -359,6 +359,7 @@ describe('execute()', () => { secrets, params: { body: 'some data' }, configurationUtilities, + logger: mockedLogger, }); delete requestMock.mock.calls[0][0].configurationUtilities; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.ts index bc25d65d45915..0656cb0692e37 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/webhook/index.ts @@ -6,12 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { curry, isString } from 'lodash'; +import { isString } from 'lodash'; import axios, { AxiosError, AxiosResponse } from 'axios'; import { schema, TypeOf } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -84,7 +83,7 @@ const ParamsSchema = schema.object({ export const ConnectorTypeId = '.webhook'; // connector type definition -export function getConnectorType({ logger }: { logger: Logger }): WebhookConnectorType { +export function getConnectorType(): WebhookConnectorType { return { id: ConnectorTypeId, minimumLicenseRequired: 'gold', @@ -109,7 +108,7 @@ export function getConnectorType({ logger }: { logger: Logger }): WebhookConnect }, }, renderParameterTemplates, - executor: curry(executor)({ logger }), + executor, }; } @@ -158,13 +157,11 @@ function validateConnectorTypeConfig( // action executor export async function executor( - { logger }: { logger: Logger }, execOptions: WebhookConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const { method, url, headers = {}, hasAuth } = execOptions.config; - const { body: data } = execOptions.params; - const configurationUtilities = execOptions.configurationUtilities; + const { actionId, config, params, configurationUtilities, logger } = execOptions; + const { method, url, headers = {}, hasAuth } = config; + const { body: data } = params; const secrets: ConnectorTypeSecretsType = execOptions.secrets; const basicAuth = diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.test.ts index 98cac875e6b55..f39f510282984 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.test.ts @@ -38,9 +38,7 @@ let configurationUtilities: jest.Mocked; beforeEach(() => { configurationUtilities = actionsConfigMock.create(); - connectorType = getConnectorType({ - logger: mockedLogger, - }); + connectorType = getConnectorType(); }); describe('connectorType', () => { @@ -414,6 +412,7 @@ describe('execute()', () => { tags: 'test1, test2', }, configurationUtilities, + logger: mockedLogger, }); expect(postxMattersMock.mock.calls[0][0]).toMatchInlineSnapshot(` @@ -462,6 +461,7 @@ describe('execute()', () => { tags: 'test1, test2', }, configurationUtilities, + logger: mockedLogger, }); expect(mockedLogger.warn).toBeCalledWith( 'Error thrown triggering xMatters workflow: maxContentLength size of 1000000 exceeded' @@ -493,6 +493,7 @@ describe('execute()', () => { tags: 'test1, test2', }, configurationUtilities, + logger: mockedLogger, }); expect(postxMattersMock.mock.calls[0][0]).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.ts index 0686d37a953f5..b34e300a496ed 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/xmatters/index.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { curry, isString } from 'lodash'; +import { isString } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { Logger } from '@kbn/core/server'; import type { ActionType as ConnectorType, ActionTypeExecutorOptions as ConnectorTypeExecutorOptions, @@ -60,7 +59,7 @@ const ParamsSchema = schema.object({ export const ConnectorTypeId = '.xmatters'; // connector type definition -export function getConnectorType({ logger }: { logger: Logger }): XmattersConnectorType { +export function getConnectorType(): XmattersConnectorType { return { id: ConnectorTypeId, minimumLicenseRequired: 'gold', @@ -82,7 +81,7 @@ export function getConnectorType({ logger }: { logger: Logger }): XmattersConnec }, connector: validateConnector, }, - executor: curry(executor)({ logger }), + executor, }; } @@ -243,13 +242,11 @@ function validateConnectorTypeSecrets( // action executor export async function executor( - { logger }: { logger: Logger }, execOptions: XmattersConnectorTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const configurationUtilities = execOptions.configurationUtilities; - const { configUrl, usesBasic } = execOptions.config; - const data = getPayloadForRequest(execOptions.params); + const { actionId, configurationUtilities, config, params, logger } = execOptions; + const { configUrl, usesBasic } = config; + const data = getPayloadForRequest(params); const secrets: ConnectorTypeSecretsType = execOptions.secrets; const basicAuth = diff --git a/x-pack/plugins/stack_connectors/server/plugin.ts b/x-pack/plugins/stack_connectors/server/plugin.ts index 0c8b32b49d9f4..ce1795b4eb7fb 100644 --- a/x-pack/plugins/stack_connectors/server/plugin.ts +++ b/x-pack/plugins/stack_connectors/server/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup, Logger } from '@kbn/core/server'; +import { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server'; import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server'; import { registerConnectorTypes } from './connector_types'; import { getWellKnownEmailServiceRoute } from './routes'; @@ -18,11 +18,7 @@ export interface ConnectorsPluginsStart { } export class StackConnectorsPlugin implements Plugin { - private readonly logger: Logger; - - constructor(context: PluginInitializerContext) { - this.logger = context.logger.get(); - } + constructor(context: PluginInitializerContext) {} public setup(core: CoreSetup, plugins: ConnectorsPluginsSetup) { const router = core.http.createRouter(); @@ -31,7 +27,6 @@ export class StackConnectorsPlugin implements Plugin { getWellKnownEmailServiceRoute(router); registerConnectorTypes({ - logger: this.logger, actions, publicBaseUrl: core.http.basePath.publicBaseUrl, }); From 0579901372f30fa7903fdc98b3becf3862efd457 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:10:52 -0400 Subject: [PATCH 18/74] [Security Solution] Unskip Trusted Apps ftr tests (#143473) --- .../apps/endpoint/trusted_apps_list.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 80053108b46fa..4643b91303be0 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -16,8 +16,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const endpointTestResources = getService('endpointTestResources'); const policyTestResources = getService('policyTestResources'); - // FLAKY: https://github.com/elastic/kibana/issues/114309 - describe.skip('When on the Trusted Apps list', function () { + describe('When on the Trusted Apps list', function () { let indexedData: IndexedHostsAndAlertsResponse; before(async () => { const endpointPackage = await policyTestResources.getEndpointPackage(); From 39d193444fe787532875d3ebb8d630f706b69628 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 17 Oct 2022 18:12:50 -0300 Subject: [PATCH 19/74] [Discover] Create unified histogram plugin (#141872) * [Discover] Create unifiedHistogram plugin * [Discover] Move discover resizable panels to unifiedHistogram * [Discover] Replace DiscoverPanels with unifiedHistogram Panels * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * [Discover] Fix types and limtis.yml for unifiedHistogram * [Discover] Begin migrating layout and chart to unified_histogram * [Discover] Update i18n keys from discover to unifiedHistogram * [Discover] Update data-test-subj tags from discover to unifiedHistogram * [Discover] Update classNames, ids, and scss to change discover to unifiedHistogram * [Discover] Remove more references to discover from unifiedHistogram * [Discover] Replace DiscoverServices with UnifiedHistogramServices * [Discover] Replacing CHART_HIDDEN_KEY with chartHiddenKey prop * [Discover] Add missing tsconfig references * [Discover] Remove remaining references to discover from unifiedHistogram * [Discover] Migrate HitsCounter to unifiedHistogram * [Discover] Continue removing discover dependencies from unifiedHistogram * [Discover] Replace SCSS with emotion * [Discover] Changing PANELS_MODE to be internal * [Discover] Clean up types * [Discover] Clean up props and types * [Discover] Update layout to use Chart component * [Discover] Update discover_main_content * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [Discover] Update discover_main_content to use UnifiedHistogramLayout, clean up unifiedHistogram implementation and props, add missing bundles * [Discover] Fix missing styles in unifiedHistogram * [Discover] Fix issue where mouse can get out of sync with the resize handle with the Discover resizable layout * [Discover] Fix some Jest tests * [Discover] Update discoverQueryHits to unifiedHistogramQueryHits in tests * [Discover] Finish decoupling discover_main_content from unified histogram layout * [Discover] Create useDiscoverHistogram hook and remove old histogram dependencies from Discover * [Discover] Move functions to create chart data from discover to unifiedHistogram * [Discover] Continue fixing broken Jest tests * Revert unifiedHistogram.reloadSavedSearchButton removal * [Discover] Add missing type export and a better suspense fallback * [Discover] Make callback names consistent * [Discover] Continue cleanup and add documentation to unifiedHistogram * [Discover] Update genChartAggConfigs to take object * [Discover] Update UnifiedHistogramHitsContext.number to total * [Discover] Cleanup imports * [Discover] Add support for hiding the entire top panel in the unified histogram by leaving all context props undefined * [Discover] Fix broken discover_layout unit tests * [Discover] Clean up naming in discover_main_content * [Discover] Continue fixing Jest tests and adding new tests * [Discover] Finish writing Jest tests * [Discover] Fix conflicts with getVisualizeInformation and triggerVisualizeActions after merge * [Discover] Fix hiding reset chart height button when default chart height * [Discover] Update CODEOWNER file * [Discover] Removed types for @link comments * [Discover] Fix broken discover_layout.test.tsx file Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .i18nrc.json | 3 +- docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + .../public/static/components/endzones.tsx | 32 +- src/plugins/discover/kibana.json | 3 +- .../components/chart/discover_chart.test.tsx | 181 ----------- .../main/components/chart/histogram.scss | 29 -- .../main/components/chart/histogram.test.tsx | 133 -------- .../main/components/chart/point_series.ts | 103 ------ .../components/hits_counter/hits_counter.scss | 3 - .../hits_counter/hits_counter.test.tsx | 71 ----- .../components/hits_counter/hits_counter.tsx | 110 ------- .../layout/__stories__/get_layout_props.ts | 48 +-- .../components/layout/discover_documents.tsx | 4 +- .../components/layout/discover_layout.scss | 38 --- .../layout/discover_layout.test.tsx | 158 ++++++---- .../layout/discover_main_content.test.tsx | 286 ++++++----------- .../layout/discover_main_content.tsx | 246 ++++++--------- .../layout/use_discover_histogram.test.ts | 297 ++++++++++++++++++ .../layout/use_discover_histogram.ts | 182 +++++++++++ .../application/main/hooks/use_data_state.ts | 4 +- .../main/hooks/use_saved_search.ts | 11 +- .../main/hooks/use_saved_search_messages.ts | 3 +- .../application/main/utils/fetch_all.test.ts | 12 +- .../application/main/utils/fetch_all.ts | 3 +- .../main/utils/fetch_chart.test.ts | 3 +- .../application/main/utils/fetch_chart.ts | 44 +-- .../main/utils/get_state_defaults.ts | 2 +- src/plugins/discover/tsconfig.json | 3 +- src/plugins/unified_histogram/README.md | 3 + src/plugins/unified_histogram/jest.config.js | 18 ++ src/plugins/unified_histogram/kibana.json | 15 + .../public/__mocks__/data_view.ts | 109 +++++++ .../__mocks__/data_view_with_timefield.ts | 59 ++++ .../public/__mocks__/services.ts | 28 ++ .../public/chart/build_chart_data.test.ts | 128 ++++++++ .../public/chart/build_chart_data.ts | 50 +++ .../chart/build_point_series_data.test.ts} | 4 +- .../public/chart/build_point_series_data.ts | 45 +++ .../public/chart/chart.test.tsx | 185 +++++++++++ .../public/chart/chart.tsx} | 193 ++++++------ .../chart}/get_chart_agg_config.test.ts | 18 +- .../public/chart}/get_chart_agg_configs.ts | 24 +- .../public/chart}/get_dimensions.test.ts | 20 +- .../public/chart}/get_dimensions.ts | 3 +- .../public/chart/histogram.test.tsx | 107 +++++++ .../public}/chart/histogram.tsx | 122 ++++--- .../public/chart}/index.ts | 3 +- .../public}/chart/use_chart_panels.test.ts | 37 ++- .../public}/chart/use_chart_panels.ts | 54 ++-- .../public/hits_counter/hits_counter.test.tsx | 64 ++++ .../public/hits_counter/hits_counter.tsx | 80 +++++ .../public}/hits_counter/index.ts | 0 src/plugins/unified_histogram/public/index.ts | 23 ++ .../unified_histogram/public/layout/index.tsx | 36 +++ .../public/layout/layout.test.tsx | 192 +++++++++++ .../public/layout/layout.tsx | 149 +++++++++ .../public/panels}/index.ts | 2 +- .../public/panels/panels.test.tsx} | 53 ++-- .../public/panels/panels.tsx} | 25 +- .../public/panels/panels_fixed.test.tsx} | 9 +- .../public/panels/panels_fixed.tsx} | 5 +- .../public/panels/panels_resizable.test.tsx} | 48 +-- .../public/panels/panels_resizable.tsx} | 73 +++-- .../unified_histogram/public/plugin.ts | 19 ++ src/plugins/unified_histogram/public/types.ts | 154 +++++++++ src/plugins/unified_histogram/tsconfig.json | 17 + test/accessibility/apps/discover.ts | 14 +- .../apps/discover/group1/_discover.ts | 6 +- .../discover/group1/_discover_histogram.ts | 28 +- .../apps/discover/group2/_sql_view.ts | 8 +- test/functional/page_objects/discover_page.ts | 30 +- tsconfig.base.json | 2 + .../translations/translations/fr-FR.json | 19 -- .../translations/translations/ja-JP.json | 19 -- .../translations/translations/zh-CN.json | 19 -- .../apps/discover/visualize_field.ts | 2 +- .../apps/lens/group1/ad_hoc_data_view.ts | 4 +- .../apps/lens/group2/show_underlying_data.ts | 10 +- .../group2/show_underlying_data_dashboard.ts | 2 +- .../functional/services/transform/discover.ts | 8 +- 82 files changed, 2747 insertions(+), 1616 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/chart/histogram.scss delete mode 100644 src/plugins/discover/public/application/main/components/chart/histogram.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/chart/point_series.ts delete mode 100644 src/plugins/discover/public/application/main/components/hits_counter/hits_counter.scss delete mode 100644 src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx create mode 100644 src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts create mode 100644 src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts create mode 100755 src/plugins/unified_histogram/README.md create mode 100644 src/plugins/unified_histogram/jest.config.js create mode 100755 src/plugins/unified_histogram/kibana.json create mode 100644 src/plugins/unified_histogram/public/__mocks__/data_view.ts create mode 100644 src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts create mode 100644 src/plugins/unified_histogram/public/__mocks__/services.ts create mode 100644 src/plugins/unified_histogram/public/chart/build_chart_data.test.ts create mode 100644 src/plugins/unified_histogram/public/chart/build_chart_data.ts rename src/plugins/{discover/public/application/main/components/chart/point_series.test.ts => unified_histogram/public/chart/build_point_series_data.test.ts} (96%) create mode 100644 src/plugins/unified_histogram/public/chart/build_point_series_data.ts create mode 100644 src/plugins/unified_histogram/public/chart/chart.test.tsx rename src/plugins/{discover/public/application/main/components/chart/discover_chart.tsx => unified_histogram/public/chart/chart.tsx} (53%) rename src/plugins/{discover/public/application/main/utils => unified_histogram/public/chart}/get_chart_agg_config.test.ts (75%) rename src/plugins/{discover/public/application/main/utils => unified_histogram/public/chart}/get_chart_agg_configs.ts (62%) rename src/plugins/{discover/public/application/main/utils => unified_histogram/public/chart}/get_dimensions.test.ts (78%) rename src/plugins/{discover/public/application/main/utils => unified_histogram/public/chart}/get_dimensions.ts (95%) create mode 100644 src/plugins/unified_histogram/public/chart/histogram.test.tsx rename src/plugins/{discover/public/application/main/components => unified_histogram/public}/chart/histogram.tsx (73%) rename src/plugins/{discover/public/application/main/utils => unified_histogram/public/chart}/index.ts (82%) rename src/plugins/{discover/public/application/main/components => unified_histogram/public}/chart/use_chart_panels.test.ts (81%) rename src/plugins/{discover/public/application/main/components => unified_histogram/public}/chart/use_chart_panels.ts (65%) create mode 100644 src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx create mode 100644 src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx rename src/plugins/{discover/public/application/main/components => unified_histogram/public}/hits_counter/index.ts (100%) create mode 100644 src/plugins/unified_histogram/public/index.ts create mode 100644 src/plugins/unified_histogram/public/layout/index.tsx create mode 100644 src/plugins/unified_histogram/public/layout/layout.test.tsx create mode 100644 src/plugins/unified_histogram/public/layout/layout.tsx rename src/plugins/{discover/public/application/main/components/chart => unified_histogram/public/panels}/index.ts (87%) rename src/plugins/{discover/public/application/main/components/layout/discover_panels.test.tsx => unified_histogram/public/panels/panels.test.tsx} (54%) rename src/plugins/{discover/public/application/main/components/layout/discover_panels.tsx => unified_histogram/public/panels/panels.tsx} (64%) rename src/plugins/{discover/public/application/main/components/layout/discover_panels_fixed.test.tsx => unified_histogram/public/panels/panels_fixed.test.tsx} (84%) rename src/plugins/{discover/public/application/main/components/layout/discover_panels_fixed.tsx => unified_histogram/public/panels/panels_fixed.tsx} (93%) rename src/plugins/{discover/public/application/main/components/layout/discover_panels_resizable.test.tsx => unified_histogram/public/panels/panels_resizable.test.tsx} (83%) rename src/plugins/{discover/public/application/main/components/layout/discover_panels_resizable.tsx => unified_histogram/public/panels/panels_resizable.tsx} (72%) create mode 100644 src/plugins/unified_histogram/public/plugin.ts create mode 100644 src/plugins/unified_histogram/public/types.ts create mode 100644 src/plugins/unified_histogram/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e19165cf4faa1..91e4463aa9905 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,7 @@ /x-pack/plugins/graph/ @elastic/kibana-data-discovery /x-pack/test/functional/apps/graph @elastic/kibana-data-discovery /src/plugins/unified_field_list/ @elastic/kibana-data-discovery +/src/plugins/unified_histogram/ @elastic/kibana-data-discovery /src/plugins/saved_objects_finder/ @elastic/kibana-data-discovery # Vis Editors diff --git a/.i18nrc.json b/.i18nrc.json index 5f0d6b1d2e30a..246f0ff484863 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -97,7 +97,8 @@ "visTypeXy": "src/plugins/vis_types/xy", "visualizations": "src/plugins/visualizations", "unifiedSearch": "src/plugins/unified_search", - "unifiedFieldList": "src/plugins/unified_field_list" + "unifiedFieldList": "src/plugins/unified_field_list", + "unifiedHistogram": "src/plugins/unified_histogram" }, "translations": [] } diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 4853d859b371a..ac883c5518c7a 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -311,6 +311,10 @@ In general this plugin provides: |This Kibana plugin contains components and services for field list UI (as in fields sidebar on Discover and Lens pages). +|{kib-repo}blob/{branch}/src/plugins/unified_histogram/README.md[unifiedHistogram] +|The unifiedHistogram plugin provides UI components to create a layout including a resizable histogram and a main display. + + |{kib-repo}blob/{branch}/src/plugins/unified_search/README.md[unifiedSearch] |Contains all the components of Kibana's unified search experience. Specifically: diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index ecfefeb3df818..5b96ef213bf3e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -122,6 +122,7 @@ pageLoadAssetSize: uiActions: 35121 uiActionsEnhanced: 38494 unifiedFieldList: 65500 + unifiedHistogram: 19928 unifiedSearch: 71059 upgradeAssistant: 81241 urlDrilldown: 30063 diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx index 727004993d171..e89ec60fd5a8b 100644 --- a/src/plugins/charts/public/static/components/endzones.tsx +++ b/src/plugins/charts/public/static/components/endzones.tsx @@ -16,8 +16,9 @@ import { RectAnnotationStyle, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { euiLightVars as lightEuiTheme, euiDarkVars as darkEuiTheme } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; interface EndzonesProps { isDarkMode: boolean; @@ -141,19 +142,22 @@ const partialDataText = i18n.translate('charts.partialData.bucketTooltipText', { 'The selected time range does not include this entire bucket. It might contain partial data.', }); -const Prompt = () => ( - - - - - {partialDataText} - -); +const Prompt = () => { + const { euiTheme } = useEuiTheme(); + const headerPartialCss = css` + font-weight: ${euiTheme.font.weight.regular}; + min-width: ${euiTheme.base * 12}; + `; + + return ( + + + + + {partialDataText} + + ); +}; export const renderEndzoneTooltip = ( diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index b2a42c351a102..4d0bb971c9239 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -19,7 +19,8 @@ "dataViewEditor", "expressions", "unifiedFieldList", - "unifiedSearch" + "unifiedSearch", + "unifiedHistogram" ], "optionalPlugins": [ "home", diff --git a/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx deleted file mode 100644 index 155d650583899..0000000000000 --- a/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx +++ /dev/null @@ -1,181 +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 { act } from 'react-dom/test-utils'; -import { Subject, BehaviorSubject } from 'rxjs'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { setHeaderActionMenuMounter, setUiActions } from '../../../../kibana_services'; -import { esHits } from '../../../../__mocks__/es_hits'; -import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; -import { GetStateReturn } from '../../services/discover_state'; -import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { FetchStatus } from '../../../types'; -import { Chart } from './point_series'; -import { DiscoverChart } from './discover_chart'; -import { VIEW_MODE } from '../../../../components/view_mode_toggle'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { ReactWrapper } from 'enzyme'; - -setHeaderActionMenuMounter(jest.fn()); - -async function mountComponent(isTimeBased: boolean = false) { - const searchSourceMock = createSearchSourceMock({}); - const services = discoverServiceMock; - services.data.query.timefilter.timefilter.getAbsoluteTime = () => { - return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; - }; - - const totalHits$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - result: Number(esHits.length), - }) as DataTotalHits$; - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as Chart; - - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - }) as DataCharts$; - - const props = { - dataView: { - isTimeBased: () => isTimeBased, - id: '123', - getFieldByName: () => ({ type: 'date', name: 'timefield', visualizable: true }), - timeFieldName: 'timefield', - toSpec: () => ({ id: '123', timeFieldName: 'timefield' }), - } as unknown as DataView, - resetSavedSearch: jest.fn(), - savedSearch: savedSearchMock, - savedSearchDataChart$: charts$, - savedSearchDataTotalHits$: totalHits$, - savedSearchRefetch$: new Subject(), - searchSource: searchSourceMock, - state: { columns: [] }, - stateContainer: { - appStateContainer: { - getState: () => ({ - interval: 'auto', - }), - }, - } as unknown as GetStateReturn, - viewMode: VIEW_MODE.DOCUMENT_LEVEL, - setDiscoverViewMode: jest.fn(), - isTimeBased, - onResetChartHeight: jest.fn(), - }; - - let instance: ReactWrapper = {} as ReactWrapper; - await act(async () => { - instance = mountWithIntl( - - - - ); - // wait for initial async loading to complete - await new Promise((r) => setTimeout(r, 0)); - await instance.update(); - }); - return instance; -} - -describe('Discover chart', () => { - let triggerActions: unknown[] = []; - beforeEach(() => { - setUiActions({ - getTriggerCompatibleActions: () => { - return triggerActions; - }, - } as unknown as UiActionsStart); - }); - test('render without timefield', async () => { - const component = await mountComponent(); - expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy(); - }); - - test('render with timefield without visualize permissions', async () => { - const component = await mountComponent(true); - expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy(); - expect(component.find('[data-test-subj="discoverEditVisualization"]').exists()).toBeFalsy(); - }); - - test('render with timefield with visualize permissions', async () => { - triggerActions = [{}]; - const component = await mountComponent(true); - expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy(); - expect(component.find('[data-test-subj="discoverEditVisualization"]').exists()).toBeTruthy(); - }); - - test('triggers ui action on click', async () => { - const fn = jest.fn(); - setUiActions({ - getTrigger: () => ({ - exec: fn, - }), - getTriggerCompatibleActions: () => { - return [{}]; - }, - } as unknown as UiActionsStart); - const component = await mountComponent(true); - await act(async () => { - await component - .find('[data-test-subj="discoverEditVisualization"]') - .first() - .simulate('click'); - }); - - expect(fn).toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/chart/histogram.scss b/src/plugins/discover/public/application/main/components/chart/histogram.scss deleted file mode 100644 index c70aaeeeac7b3..0000000000000 --- a/src/plugins/discover/public/application/main/components/chart/histogram.scss +++ /dev/null @@ -1,29 +0,0 @@ -.dscChart__loading { - display: flex; - flex-direction: column; - justify-content: center; - flex: 1 0 100%; - text-align: center; - height: 100%; - width: 100%; -} -.dscHistogram__header--partial { - font-weight: $euiFontWeightRegular; - min-width: $euiSize * 12; -} - -.dscHistogram__errorChartContainer { - padding: 0 $euiSizeS 0 $euiSizeS; -} - -.dscHistogram__errorChart { - margin-left: $euiSizeXS !important; -} - -.dscHistogram__errorChart__text { - margin-top: $euiSizeS; -} - -.dscHistogram__errorChart__icon { - padding-top: .5 * $euiSizeXS; -} diff --git a/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx b/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx deleted file mode 100644 index 655925e3effd7..0000000000000 --- a/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx +++ /dev/null @@ -1,133 +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 { mountWithIntl } from '@kbn/test-jest-helpers'; -import { BehaviorSubject } from 'rxjs'; -import { FetchStatus } from '../../../types'; -import { DataCharts$ } from '../../hooks/use_saved_search'; -import { discoverServiceMock } from '../../../../__mocks__/services'; -import { Chart } from './point_series'; -import { DiscoverHistogram } from './histogram'; -import React from 'react'; -import * as hooks from '../../hooks/use_data_state'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { GetStateReturn } from '../../services/discover_state'; - -const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], -} as unknown as Chart; - -function mountComponent(fetchStatus: FetchStatus) { - const services = discoverServiceMock; - services.data.query.timefilter.timefilter.getAbsoluteTime = () => { - return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; - }; - - const charts$ = new BehaviorSubject({ - fetchStatus, - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - }) as DataCharts$; - - const timefilterUpdateHandler = jest.fn(); - - const props = { - savedSearchData$: charts$, - timefilterUpdateHandler, - stateContainer: { - appStateContainer: { - getState: () => ({ - interval: 'auto', - }), - }, - } as unknown as GetStateReturn, - }; - - return mountWithIntl( - - - - ); -} - -describe('Histogram', () => { - it('renders correctly', () => { - jest.spyOn(hooks, 'useDataState').mockImplementation(() => ({ - fetchStatus: FetchStatus.COMPLETE, - chartData, - bucketInterval: { - scaled: true, - description: 'Bucket interval', - scale: 1, - }, - })); - const component = mountComponent(FetchStatus.COMPLETE); - expect(component.find('[data-test-subj="discoverChart"]').exists()).toBe(true); - }); - - it('renders error correctly', () => { - jest.spyOn(hooks, 'useDataState').mockImplementation(() => ({ - fetchStatus: FetchStatus.ERROR, - error: new Error('Loading error'), - })); - const component = mountComponent(FetchStatus.ERROR); - expect(component.find('[data-test-subj="discoverChart"]').exists()).toBe(false); - expect(component.find('.dscHistogram__errorChartContainer').exists()).toBe(true); - expect(component.find('.dscHistogram__errorChart__text').get(1).props.children).toBe( - 'Loading error' - ); - }); - - it('renders loading state correctly', () => { - jest.spyOn(hooks, 'useDataState').mockImplementation(() => ({ - fetchStatus: FetchStatus.LOADING, - chartData: null, - })); - const component = mountComponent(FetchStatus.LOADING); - expect(component.find('[data-test-subj="discoverChart"]').exists()).toBe(true); - expect(component.find('.dscChart__loading').exists()).toBe(true); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/chart/point_series.ts b/src/plugins/discover/public/application/main/components/chart/point_series.ts deleted file mode 100644 index c8795296d6fd8..0000000000000 --- a/src/plugins/discover/public/application/main/components/chart/point_series.ts +++ /dev/null @@ -1,103 +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 { uniq } from 'lodash'; -import { Duration, Moment } from 'moment'; -import { Unit } from '@kbn/datemath'; -import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; - -export interface Column { - id: string; - name: string; -} - -export interface Row { - [key: string]: number | 'NaN'; -} - -export interface Table { - columns: Column[]; - rows: Row[]; -} - -export interface HistogramParamsBounds { - min: Moment; - max: Moment; -} - -interface HistogramParams { - date: true; - interval: Duration; - intervalESValue: number; - intervalESUnit: Unit; - format: string; - bounds: HistogramParamsBounds; -} -export interface Dimension { - accessor: 0 | 1; - format: SerializedFieldFormat<{ pattern: string }>; - label: string; -} - -export interface Dimensions { - x: Dimension & { params: HistogramParams }; - y: Dimension; -} - -interface Ordered { - date: true; - interval: Duration; - intervalESUnit: string; - intervalESValue: number; - min: Moment; - max: Moment; -} -export interface Chart { - values: Array<{ - x: number; - y: number; - }>; - xAxisOrderedValues: number[]; - xAxisFormat: Dimension['format']; - yAxisFormat: Dimension['format']; - xAxisLabel: Column['name']; - yAxisLabel?: Column['name']; - ordered: Ordered; -} - -export const buildPointSeriesData = (table: Table, dimensions: Dimensions): Chart => { - const { x, y } = dimensions; - const xAccessor = table.columns[x.accessor].id; - const yAccessor = table.columns[y.accessor].id; - const chart = {} as Chart; - - chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); - chart.xAxisFormat = x.format; - chart.xAxisLabel = table.columns[x.accessor].name; - chart.yAxisFormat = y.format; - const { intervalESUnit, intervalESValue, interval, bounds } = x.params; - chart.ordered = { - date: true, - interval, - intervalESUnit, - intervalESValue, - min: bounds.min, - max: bounds.max, - }; - - chart.yAxisLabel = table.columns[y.accessor].name; - - chart.values = table.rows - .filter((row) => row && row[yAccessor] !== 'NaN') - .map((row) => ({ - x: row[xAccessor] as number, - y: row[yAccessor] as number, - })); - - return chart; -}; diff --git a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.scss b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.scss deleted file mode 100644 index 5a3999f129bf4..0000000000000 --- a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.scss +++ /dev/null @@ -1,3 +0,0 @@ -.dscHitsCounter { - flex-grow: 0; -} diff --git a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx deleted file mode 100644 index b8111b25a6ef2..0000000000000 --- a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx +++ /dev/null @@ -1,71 +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 { mountWithIntl } from '@kbn/test-jest-helpers'; -import { ReactWrapper } from 'enzyme'; -import { HitsCounter, HitsCounterProps } from './hits_counter'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { BehaviorSubject } from 'rxjs'; -import { FetchStatus } from '../../../types'; -import { DataTotalHits$ } from '../../hooks/use_saved_search'; - -describe('hits counter', function () { - let props: HitsCounterProps; - let component: ReactWrapper; - - beforeAll(() => { - props = { - onResetQuery: jest.fn(), - showResetButton: true, - savedSearchData$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - result: 2, - }) as DataTotalHits$, - }; - }); - - it('HitsCounter renders a button by providing the showResetButton property', () => { - component = mountWithIntl(); - expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); - }); - - it('HitsCounter not renders a button when the showResetButton property is false', () => { - component = mountWithIntl(); - expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); - }); - - it('expect to render the number of hits', function () { - component = mountWithIntl(); - const hits = findTestSubject(component, 'discoverQueryHits'); - expect(hits.text()).toBe('2'); - }); - - it('expect to render 1,899 hits if 1899 hits given', function () { - const data$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - result: 1899, - }) as DataTotalHits$; - component = mountWithIntl( - - ); - const hits = findTestSubject(component, 'discoverQueryHits'); - expect(hits.text()).toBe('1,899'); - }); - - it('should reset query', function () { - component = mountWithIntl(); - findTestSubject(component, 'resetSavedSearch').simulate('click'); - expect(props.onResetQuery).toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx deleted file mode 100644 index bb7681c2efa36..0000000000000 --- a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx +++ /dev/null @@ -1,110 +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 './hits_counter.scss'; -import React from 'react'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { DataTotalHits$, DataTotalHitsMsg } from '../../hooks/use_saved_search'; -import { FetchStatus } from '../../../types'; -import { useDataState } from '../../hooks/use_data_state'; - -export interface HitsCounterProps { - /** - * displays the reset button - */ - showResetButton: boolean; - /** - * resets the query - */ - onResetQuery: () => void; - /** - * saved search data observable - */ - savedSearchData$: DataTotalHits$; -} - -export function HitsCounter({ showResetButton, onResetQuery, savedSearchData$ }: HitsCounterProps) { - const data: DataTotalHitsMsg = useDataState(savedSearchData$); - - const hits = data.result || 0; - if (!hits && data.fetchStatus === FetchStatus.LOADING) { - return null; - } - - const formattedHits = ( - - - - ); - - return ( - - - - {data.fetchStatus === FetchStatus.PARTIAL && ( - - )} - {data.fetchStatus !== FetchStatus.PARTIAL && ( - - )} - - - {data.fetchStatus === FetchStatus.PARTIAL && ( - - - - )} - {showResetButton && ( - - - - - - )} - - ); -} diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index a4c9227a2c072..f78ed164493a5 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -11,6 +11,7 @@ import { SearchSource } from '@kbn/data-plugin/common'; import { BehaviorSubject, Subject } from 'rxjs'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { action } from '@storybook/addon-actions'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { FetchStatus } from '../../../../types'; import { AvailableFields$, @@ -22,50 +23,10 @@ import { } from '../../../hooks/use_saved_search'; import { buildDataTableRecordList } from '../../../../../utils/build_data_record'; import { esHits } from '../../../../../__mocks__/es_hits'; -import { Chart } from '../../chart/point_series'; import { SavedSearch } from '../../../../..'; import { DiscoverLayoutProps } from '../types'; import { GetStateReturn } from '../../../services/discover_state'; -const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: () => 1000, - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], -} as unknown as Chart; - const documentObservables = { main$: new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, @@ -89,12 +50,7 @@ const documentObservables = { charts$: new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, + response: {} as unknown as SearchResponse, }) as DataCharts$, }; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index dfff574659744..2aacf598e58c4 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -28,7 +28,7 @@ import { HIDE_ANNOUNCEMENTS, } from '../../../../../common'; import { useColumns } from '../../../../hooks/use_data_grid_columns'; -import { DataDocuments$, DataDocumentsMsg, RecordRawType } from '../../hooks/use_saved_search'; +import { DataDocuments$, RecordRawType } from '../../hooks/use_saved_search'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { useDataState } from '../../hooks/use_data_state'; import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite'; @@ -84,7 +84,7 @@ function DiscoverDocumentsComponent({ const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]); - const documentState: DataDocumentsMsg = useDataState(documents$); + const documentState = useDataState(documents$); const isLoading = documentState.fetchStatus === FetchStatus.LOADING; const isPlainRecord = useMemo( () => getRawRecordType(state.query) === RecordRawType.PLAIN, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index ee727779fc2a1..70963f50b96a7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -53,44 +53,6 @@ discover-app { height: auto; } -.dscResultCount { - padding: $euiSizeS; - min-height: $euiSize * 3; - - @include euiBreakpoint('xs', 's') { - .dscResultCount__toggle { - align-items: flex-end; - } - - .dscResultCount__title, - .dscResultCount__actions { - margin-bottom: 0 !important; - } - } -} - -.dscTimechart { - flex-grow: 1; - display: flex; - flex-direction: column; - position: relative; - - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: .5; - stroke-width: 1; - } -} - -.dscHistogram { - flex-grow: 1; - padding: 0 $euiSizeS $euiSizeS $euiSizeS; -} - -.dscHistogramTimeRange { - padding: 0 $euiSizeS 0 $euiSizeS; -} - .dscTable { // needs for scroll container of lagacy table min-height: 0; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 32aa0b0999bbe..0e9c7f8449520 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -31,16 +31,72 @@ import { import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { Chart } from '../chart/point_series'; import { DiscoverSidebar } from '../sidebar/discover_sidebar'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverServices } from '../../../../build_services'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; +import type { UnifiedHistogramChartData } from '@kbn/unified-histogram-plugin/public'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { setTimeout } from 'timers/promises'; +import { act } from 'react-dom/test-utils'; -setHeaderActionMenuMounter(jest.fn()); +jest.mock('@kbn/unified-histogram-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/unified-histogram-plugin/public'); + + const chartData = { + xAxisOrderedValues: [ + 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, + 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, + 1624917600000, 1625004000000, 1625090400000, + ], + xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + xAxisLabel: 'order_date per day', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: { + asMilliseconds: jest.fn(), + }, + intervalESUnit: 'd', + intervalESValue: 1, + min: '2021-03-18T08:28:56.411Z', + max: '2021-07-01T07:28:56.411Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1623880800000, y: 134 }, + { x: 1623967200000, y: 152 }, + { x: 1624053600000, y: 141 }, + { x: 1624140000000, y: 138 }, + { x: 1624226400000, y: 142 }, + { x: 1624312800000, y: 157 }, + { x: 1624399200000, y: 149 }, + { x: 1624485600000, y: 146 }, + { x: 1624572000000, y: 170 }, + { x: 1624658400000, y: 137 }, + { x: 1624744800000, y: 150 }, + { x: 1624831200000, y: 144 }, + { x: 1624917600000, y: 147 }, + { x: 1625004000000, y: 137 }, + { x: 1625090400000, y: 66 }, + ], + } as unknown as UnifiedHistogramChartData; + + return { + ...originalModule, + buildChartData: jest.fn().mockImplementation(() => ({ + chartData, + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + })), + }; +}); function getAppStateContainer() { const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; @@ -51,7 +107,9 @@ function getAppStateContainer() { return appStateContainer; } -function mountComponent( +setHeaderActionMenuMounter(jest.fn()); + +async function mountComponent( dataView: DataView, prevSidebarClosed?: boolean, mountOptions: { attachTo?: HTMLElement } = {}, @@ -92,53 +150,9 @@ function mountComponent( result: Number(esHits.length), }) as DataTotalHits$; - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as Chart; - const charts$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, + response: {} as unknown as SearchResponse, }) as DataCharts$; const savedSearchData$ = { @@ -176,7 +190,7 @@ function mountComponent( adHocDataViewList: [], }; - return mountWithIntl( + const component = mountWithIntl( @@ -184,37 +198,49 @@ function mountComponent( , mountOptions ); + + // DiscoverMainContent uses UnifiedHistogramLayout which + // is lazy loaded, so we need to wait for it to be loaded + await act(() => setTimeout(0)); + + return component; } describe('Discover component', () => { - test('selected data view without time field displays no chart toggle', () => { + test('selected data view without time field displays no chart toggle', async () => { const container = document.createElement('div'); - mountComponent(dataViewMock, undefined, { attachTo: container }); - expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).toBeNull(); + await mountComponent(dataViewMock, undefined, { attachTo: container }); + expect( + container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') + ).toBeNull(); }); - test('selected data view with time field displays chart toggle', () => { + test('selected data view with time field displays chart toggle', async () => { const container = document.createElement('div'); - mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container }); - expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).not.toBeNull(); + await mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container }); + expect( + container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') + ).not.toBeNull(); }); - test('sql query displays no chart toggle', () => { + test('sql query displays no chart toggle', async () => { const container = document.createElement('div'); - mountComponent( + await mountComponent( dataViewWithTimefieldMock, false, { attachTo: container }, { sql: 'SELECT * FROM test' }, true ); - expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).toBeNull(); + expect( + container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') + ).toBeNull(); }); - test('the saved search title h1 gains focus on navigate', () => { + test('the saved search title h1 gains focus on navigate', async () => { const container = document.createElement('div'); document.body.appendChild(container); - const component = mountComponent(dataViewWithTimefieldMock, undefined, { + const component = await mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container, }); expect( @@ -223,18 +249,18 @@ describe('Discover component', () => { }); describe('sidebar', () => { - test('should be opened if discover:sidebarClosed was not set', () => { - const component = mountComponent(dataViewWithTimefieldMock, undefined); + test('should be opened if discover:sidebarClosed was not set', async () => { + const component = await mountComponent(dataViewWithTimefieldMock, undefined); expect(component.find(DiscoverSidebar).length).toBe(1); }); - test('should be opened if discover:sidebarClosed is false', () => { - const component = mountComponent(dataViewWithTimefieldMock, false); + test('should be opened if discover:sidebarClosed is false', async () => { + const component = await mountComponent(dataViewWithTimefieldMock, false); expect(component.find(DiscoverSidebar).length).toBe(1); }); - test('should be closed if discover:sidebarClosed is true', () => { - const component = mountComponent(dataViewWithTimefieldMock, true); + test('should be closed if discover:sidebarClosed is true', async () => { + const component = await mountComponent(dataViewWithTimefieldMock, true); expect(component.find(DiscoverSidebar).length).toBe(0); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 6f66b1f613b22..54e3fa0b19ca5 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Subject, BehaviorSubject } from 'rxjs'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; @@ -23,36 +23,92 @@ import { } from '../../hooks/use_saved_search'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; -import { Chart } from '../chart/point_series'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; -import { - DiscoverMainContent, - DiscoverMainContentProps, - HISTOGRAM_HEIGHT_KEY, -} from './discover_main_content'; -import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; -import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels'; -import { euiThemeVars } from '@kbn/ui-theme'; +import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; +import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { CoreTheme } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; import { setTimeout } from 'timers/promises'; -import { DiscoverChart } from '../chart'; -import { ReactWrapper } from 'enzyme'; import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; +import { + UnifiedHistogramChartData, + UnifiedHistogramLayout, +} from '@kbn/unified-histogram-plugin/public'; +import { HISTOGRAM_HEIGHT_KEY } from './use_discover_histogram'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; + +jest.mock('@kbn/unified-histogram-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/unified-histogram-plugin/public'); + + const chartData = { + xAxisOrderedValues: [ + 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, + 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, + 1624917600000, 1625004000000, 1625090400000, + ], + xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + xAxisLabel: 'order_date per day', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: { + asMilliseconds: jest.fn(), + }, + intervalESUnit: 'd', + intervalESValue: 1, + min: '2021-03-18T08:28:56.411Z', + max: '2021-07-01T07:28:56.411Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1623880800000, y: 134 }, + { x: 1623967200000, y: 152 }, + { x: 1624053600000, y: 141 }, + { x: 1624140000000, y: 138 }, + { x: 1624226400000, y: 142 }, + { x: 1624312800000, y: 157 }, + { x: 1624399200000, y: 149 }, + { x: 1624485600000, y: 146 }, + { x: 1624572000000, y: 170 }, + { x: 1624658400000, y: 137 }, + { x: 1624744800000, y: 150 }, + { x: 1624831200000, y: 144 }, + { x: 1624917600000, y: 147 }, + { x: 1625004000000, y: 137 }, + { x: 1625090400000, y: 66 }, + ], + } as unknown as UnifiedHistogramChartData; + + return { + ...originalModule, + buildChartData: jest.fn().mockImplementation(() => ({ + chartData, + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + })), + }; +}); const mountComponent = async ({ isPlainRecord = false, hideChart = false, isTimeBased = true, storage, + savedSearch = savedSearchMock, + resetSavedSearch = jest.fn(), }: { isPlainRecord?: boolean; hideChart?: boolean; isTimeBased?: boolean; storage?: Storage; + savedSearch?: SavedSearch; + resetSavedSearch?: () => void; } = {}) => { let services = discoverServiceMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { @@ -84,53 +140,9 @@ const mountComponent = async ({ result: Number(esHits.length), }) as DataTotalHits$; - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as Chart; - const charts$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, + response: {} as unknown as SearchResponse, }) as DataCharts$; const savedSearchData$ = { @@ -145,9 +157,9 @@ const mountComponent = async ({ isPlainRecord, dataView: dataViewMock, navigateTo: jest.fn(), - resetSavedSearch: jest.fn(), + resetSavedSearch, setExpandedDoc: jest.fn(), - savedSearch: savedSearchMock, + savedSearch, savedSearchData$, savedSearchRefetch$: new Subject(), state: { columns: [], hideChart }, @@ -177,109 +189,30 @@ const mountComponent = async ({ ); - // useIsWithinBreakpoints triggers state updates which cause act - // issues and prevent our resize events from being fired correctly - // https://github.com/enzymejs/enzyme/issues/2073 + // DiscoverMainContent uses UnifiedHistogramLayout which + // is lazy loaded, so we need to wait for it to be loaded await act(() => setTimeout(0)); return component; }; -const setWindowWidth = (component: ReactWrapper, width: string) => { - window.innerWidth = parseInt(width, 10); - act(() => { - window.dispatchEvent(new Event('resize')); - }); - component.update(); -}; - describe('Discover main content component', () => { - const windowWidth = window.innerWidth; - - beforeEach(() => { - window.innerWidth = windowWidth; - }); - - describe('DISCOVER_PANELS_MODE', () => { - it('should set the panels mode to DISCOVER_PANELS_MODE.RESIZABLE when viewing on medium screens and above', async () => { - const component = await mountComponent(); - setWindowWidth(component, euiThemeVars.euiBreakpoints.m); - expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.RESIZABLE); - }); - - it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED when viewing on small screens and below', async () => { - const component = await mountComponent(); - setWindowWidth(component, euiThemeVars.euiBreakpoints.s); - expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED); - }); - - it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED if hideChart is true', async () => { - const component = await mountComponent({ hideChart: true }); - expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED); - }); - - it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED if isTimeBased is false', async () => { - const component = await mountComponent({ isTimeBased: false }); - expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED); - }); - - it('should set the panels mode to DISCOVER_PANELS_MODE.SINGLE if isPlainRecord is true', async () => { - const component = await mountComponent({ isPlainRecord: true }); - expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.SINGLE); - }); - - it('should set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and hideChart is false', async () => { - const component = await mountComponent(); - setWindowWidth(component, euiThemeVars.euiBreakpoints.s); - const expectedHeight = component.find(DiscoverPanels).prop('topPanelHeight'); - expect(component.find(DiscoverChart).childAt(0).getDOMNode()).toHaveStyle({ - height: `${expectedHeight}px`, - }); - }); - - it('should not set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and hideChart is true', async () => { - const component = await mountComponent({ hideChart: true }); - setWindowWidth(component, euiThemeVars.euiBreakpoints.s); - const expectedHeight = component.find(DiscoverPanels).prop('topPanelHeight'); - expect(component.find(DiscoverChart).childAt(0).getDOMNode()).not.toHaveStyle({ - height: `${expectedHeight}px`, - }); - }); - - it('should not set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and isTimeBased is false', async () => { - const component = await mountComponent({ isTimeBased: false }); - setWindowWidth(component, euiThemeVars.euiBreakpoints.s); - const expectedHeight = component.find(DiscoverPanels).prop('topPanelHeight'); - expect(component.find(DiscoverChart).childAt(0).getDOMNode()).not.toHaveStyle({ - height: `${expectedHeight}px`, - }); - }); - - it('should pass undefined for onResetChartHeight to DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const topPanelHeight = 123; - storage.get = jest.fn().mockImplementation(() => topPanelHeight); - const component = await mountComponent({ storage }); - expect(component.find(DiscoverChart).prop('onResetChartHeight')).toBeDefined(); - setWindowWidth(component, euiThemeVars.euiBreakpoints.s); - expect(component.find(DiscoverChart).prop('onResetChartHeight')).toBeUndefined(); - }); - }); - describe('DocumentViewModeToggle', () => { it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { const component = await mountComponent(); + component.update(); expect(component.find(DocumentViewModeToggle).exists()).toBe(true); }); it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { const component = await mountComponent({ isPlainRecord: true }); + component.update(); expect(component.find(DocumentViewModeToggle).exists()).toBe(false); }); }); describe('topPanelHeight persistence', () => { - it('should try to get the initial topPanelHeight for DiscoverPanels from storage', async () => { + it('should try to get the initial topPanelHeight for UnifiedHistogramLayout from storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; const originalGet = storage.get; storage.get = jest.fn().mockImplementation(originalGet); @@ -287,77 +220,62 @@ describe('Discover main content component', () => { expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); }); - it('should pass a default topPanelHeight to DiscoverPanels if no value is found in storage', async () => { + it('should pass undefined to UnifiedHistogramLayout if no value is found in storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; const originalGet = storage.get; storage.get = jest.fn().mockImplementation(originalGet); const component = await mountComponent({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(storage.get).toHaveReturnedWith(null); - expect(component.find(DiscoverPanels).prop('topPanelHeight')).toBeGreaterThan(0); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(undefined); }); - it('should pass the stored topPanelHeight to DiscoverPanels if a value is found in storage', async () => { + it('should pass the stored topPanelHeight to UnifiedHistogramLayout if a value is found in storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; const topPanelHeight = 123; storage.get = jest.fn().mockImplementation(() => topPanelHeight); const component = await mountComponent({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(storage.get).toHaveReturnedWith(topPanelHeight); - expect(component.find(DiscoverPanels).prop('topPanelHeight')).toBe(topPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(topPanelHeight); }); - it('should update the topPanelHeight in storage and pass the new value to DiscoverPanels when the topPanelHeight changes', async () => { + it('should update the topPanelHeight in storage and pass the new value to UnifiedHistogramLayout when the topPanelHeight changes', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; const originalSet = storage.set; storage.set = jest.fn().mockImplementation(originalSet); const component = await mountComponent({ storage }); const newTopPanelHeight = 123; - expect(component.find(DiscoverPanels).prop('topPanelHeight')).not.toBe(newTopPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).not.toBe( + newTopPanelHeight + ); act(() => { - component.find(DiscoverPanels).prop('onTopPanelHeightChange')(newTopPanelHeight); + component.find(UnifiedHistogramLayout).prop('onTopPanelHeightChange')!(newTopPanelHeight); }); component.update(); expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); - expect(component.find(DiscoverPanels).prop('topPanelHeight')).toBe(newTopPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(newTopPanelHeight); }); + }); - it('should reset the topPanelHeight to the default when onResetChartHeight is called on DiscoverChart', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const originalSet = storage.set; - storage.set = jest.fn().mockImplementation(originalSet); - const component = await mountComponent({ storage }); - const defaultTopPanelHeight = component.find(DiscoverPanels).prop('topPanelHeight'); - const newTopPanelHeight = 123; - expect(component.find(DiscoverPanels).prop('topPanelHeight')).not.toBe(newTopPanelHeight); - act(() => { - component.find(DiscoverPanels).prop('onTopPanelHeightChange')(newTopPanelHeight); - }); - component.update(); - expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); - expect(component.find(DiscoverPanels).prop('topPanelHeight')).toBe(newTopPanelHeight); - act(() => { - component.find(DiscoverChart).prop('onResetChartHeight')!(); - }); - component.update(); - expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, defaultTopPanelHeight); - expect(component.find(DiscoverPanels).prop('topPanelHeight')).toBe(defaultTopPanelHeight); + describe('reset search button', () => { + it('renders the button when there is a saved search', async () => { + const component = await mountComponent(); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); }); - it('should pass undefined for onResetChartHeight to DiscoverChart when the chart is the default height', async () => { - const component = await mountComponent(); - const defaultTopPanelHeight = component.find(DiscoverPanels).prop('topPanelHeight'); - const newTopPanelHeight = 123; - act(() => { - component.find(DiscoverPanels).prop('onTopPanelHeightChange')(newTopPanelHeight); + it('does not render the button when there is no saved search', async () => { + const component = await mountComponent({ + savedSearch: { ...savedSearchMock, id: undefined }, }); - component.update(); - expect(component.find(DiscoverChart).prop('onResetChartHeight')).toBeDefined(); - act(() => { - component.find(DiscoverPanels).prop('onTopPanelHeightChange')(defaultTopPanelHeight); - }); - component.update(); - expect(component.find(DiscoverChart).prop('onResetChartHeight')).toBeUndefined(); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); + }); + + it('should call resetSavedSearch when clicked', async () => { + const resetSavedSearch = jest.fn(); + const component = await mountComponent({ resetSavedSearch }); + findTestSubject(component, 'resetSavedSearch').simulate('click'); + expect(resetSavedSearch).toHaveBeenCalled(); }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 4998e561f5dd7..100412c8f7930 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -6,37 +6,27 @@ * Side Public License, v 1. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiSpacer, - useEuiTheme, - useIsWithinBreakpoints, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; -import React, { RefObject, useCallback, useMemo, useState } from 'react'; +import React, { RefObject, useCallback } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; -import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -import { css } from '@emotion/css'; +import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; import { AppState, GetStateReturn } from '../../services/discover_state'; -import { DiscoverChart } from '../chart'; import { FieldStatisticsTable } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; -import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels'; +import { useDiscoverHistogram } from './use_discover_histogram'; -const DiscoverChartMemoized = React.memo(DiscoverChart); const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); -export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; - export interface DiscoverMainContentProps { isPlainRecord: boolean; dataView: DataView; @@ -76,7 +66,8 @@ export const DiscoverMainContent = ({ columns, resizeRef, }: DiscoverMainContentProps) => { - const { trackUiMetric, storage } = useDiscoverServices(); + const services = useDiscoverServices(); + const { trackUiMetric } = services; const setDiscoverViewMode = useCallback( (mode: VIEW_MODE) => { @@ -93,139 +84,98 @@ export const DiscoverMainContent = ({ [trackUiMetric, stateContainer] ); - const topPanelNode = useMemo( - () => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }), - [] - ); - - const mainPanelNode = useMemo( - () => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }), - [] - ); - - const hideChart = state.hideChart || !isTimeBased; - const showFixedPanels = useIsWithinBreakpoints(['xs', 's']) || isPlainRecord || hideChart; - const { euiTheme } = useEuiTheme(); - const defaultTopPanelHeight = euiTheme.base * 12; - const minTopPanelHeight = euiTheme.base * 8; - const minMainPanelHeight = euiTheme.base * 10; - - const [topPanelHeight, setTopPanelHeight] = useState( - Number(storage.get(HISTOGRAM_HEIGHT_KEY)) || defaultTopPanelHeight - ); - - const storeTopPanelHeight = useCallback( - (newTopPanelHeight: number) => { - storage.set(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); - setTopPanelHeight(newTopPanelHeight); - }, - [storage] - ); - - const resetTopPanelHeight = useCallback( - () => storeTopPanelHeight(defaultTopPanelHeight), - [storeTopPanelHeight, defaultTopPanelHeight] - ); - - const onTopPanelHeightChange = useCallback( - (newTopPanelHeight: number) => storeTopPanelHeight(newTopPanelHeight), - [storeTopPanelHeight] - ); - - const chartClassName = - showFixedPanels && !hideChart - ? css` - height: ${defaultTopPanelHeight}px; - ` - : 'eui-fullHeight'; - - const panelsMode = isPlainRecord - ? DISCOVER_PANELS_MODE.SINGLE - : showFixedPanels - ? DISCOVER_PANELS_MODE.FIXED - : DISCOVER_PANELS_MODE.RESIZABLE; + const { + topPanelHeight, + hits, + chart, + onEditVisualization, + onTopPanelHeightChange, + onChartHiddenChange, + onTimeIntervalChange, + } = useDiscoverHistogram({ + stateContainer, + state, + savedSearchData$, + dataView, + savedSearch, + isTimeBased, + isPlainRecord, + }); return ( - <> - - : } - onResetChartHeight={ - topPanelHeight !== defaultTopPanelHeight && - panelsMode === DISCOVER_PANELS_MODE.RESIZABLE - ? resetTopPanelHeight - : undefined - } - /> - - - - {!isPlainRecord && ( - - {!showFixedPanels && } - - + + - - )} - {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( - - ) : ( - - )} - - - } - mainPanel={} - onTopPanelHeightChange={onTopPanelHeightChange} - /> - + + + ) : undefined + } + onTopPanelHeightChange={onTopPanelHeightChange} + onEditVisualization={onEditVisualization} + onChartHiddenChange={onChartHiddenChange} + onTimeIntervalChange={onTimeIntervalChange} + > + + {!isPlainRecord && ( + + + + + )} + {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( + + ) : ( + + )} + + ); }; diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts new file mode 100644 index 0000000000000..12fde6a5b1061 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -0,0 +1,297 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { buildDataTableRecord } from '../../../../utils/build_data_record'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { BehaviorSubject } from 'rxjs'; +import { FetchStatus } from '../../../types'; +import { + AvailableFields$, + DataCharts$, + DataDocuments$, + DataMain$, + DataTotalHits$, + RecordRawType, +} from '../../hooks/use_saved_search'; +import type { GetStateReturn } from '../../services/discover_state'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; +import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; +import { + CHART_HIDDEN_KEY, + HISTOGRAM_HEIGHT_KEY, + useDiscoverHistogram, +} from './use_discover_histogram'; +import { setTimeout } from 'timers/promises'; +import { calculateBounds } from '@kbn/data-plugin/public'; + +const mockData = dataPluginMock.createStartContract(); + +mockData.query.timefilter.timefilter.getTime = () => { + return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; +}; +mockData.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); +}; + +let mockStorage = new LocalStorageMock({}) as unknown as Storage; +let mockCanVisualize = true; + +jest.mock('../../../../hooks/use_discover_services', () => { + const originalModule = jest.requireActual('../../../../hooks/use_discover_services'); + return { + ...originalModule, + useDiscoverServices: () => ({ storage: mockStorage, data: mockData }), + }; +}); + +jest.mock('@kbn/unified-field-list-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/unified-field-list-plugin/public'); + return { + ...originalModule, + getVisualizeInformation: jest.fn(() => Promise.resolve(mockCanVisualize)), + }; +}); + +describe('useDiscoverHistogram', () => { + const renderUseDiscoverHistogram = async ({ + isPlainRecord = false, + isTimeBased = true, + canVisualize = true, + storage = new LocalStorageMock({}) as unknown as Storage, + stateContainer = {}, + }: { + isPlainRecord?: boolean; + isTimeBased?: boolean; + canVisualize?: boolean; + storage?: Storage; + stateContainer?: unknown; + } = {}) => { + mockStorage = storage; + mockCanVisualize = canVisualize; + + const main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$; + + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)), + }) as DataDocuments$; + + const availableFields$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + fields: [] as string[], + }) as AvailableFields$; + + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: Number(esHits.length), + }) as DataTotalHits$; + + const charts$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + response: { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 29, + max_score: null, + hits: [], + }, + aggregations: { + '2': { + buckets: [ + { + key_as_string: '2022-10-05T16:00:00.000-03:00', + key: 1664996400000, + doc_count: 6, + }, + { + key_as_string: '2022-10-05T16:30:00.000-03:00', + key: 1664998200000, + doc_count: 2, + }, + { + key_as_string: '2022-10-05T17:00:00.000-03:00', + key: 1665000000000, + doc_count: 3, + }, + { + key_as_string: '2022-10-05T17:30:00.000-03:00', + key: 1665001800000, + doc_count: 8, + }, + { + key_as_string: '2022-10-05T18:00:00.000-03:00', + key: 1665003600000, + doc_count: 10, + }, + ], + }, + }, + } as SearchResponse, + }) as DataCharts$; + + const savedSearchData$ = { + main$, + documents$, + totalHits$, + charts$, + availableFields$, + }; + + const hook = renderHook(() => { + return useDiscoverHistogram({ + stateContainer: stateContainer as GetStateReturn, + state: { interval: 'auto', hideChart: false }, + savedSearchData$, + dataView: dataViewWithTimefieldMock, + savedSearch: savedSearchMock, + isTimeBased, + isPlainRecord, + }); + }); + + await act(() => setTimeout(0)); + + return hook; + }; + + const expectedChartData = { + xAxisOrderedValues: [1664996400000, 1664998200000, 1665000000000, 1665001800000, 1665003600000], + xAxisFormat: { id: 'date', params: { pattern: 'HH:mm:ss.SSS' } }, + xAxisLabel: 'timestamp per 0 milliseconds', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: 'P0D', + intervalESUnit: 'ms', + intervalESValue: 0, + min: '1991-03-29T08:04:00.694Z', + max: '2021-03-29T07:04:00.695Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1664996400000, y: 6 }, + { x: 1664998200000, y: 2 }, + { x: 1665000000000, y: 3 }, + { x: 1665001800000, y: 8 }, + { x: 1665003600000, y: 10 }, + ], + }; + + describe('contexts', () => { + it('should output the correct hits context', async () => { + const { result } = await renderUseDiscoverHistogram(); + expect(result.current.hits?.status).toBe(FetchStatus.COMPLETE); + expect(result.current.hits?.total).toEqual(esHits.length); + }); + + it('should output the correct chart context', async () => { + const { result } = await renderUseDiscoverHistogram(); + expect(result.current.chart?.status).toBe(FetchStatus.COMPLETE); + expect(result.current.chart?.hidden).toBe(false); + expect(result.current.chart?.timeInterval).toBe('auto'); + expect(result.current.chart?.bucketInterval?.toString()).toBe('P0D'); + expect(JSON.stringify(result.current.chart?.data)).toBe(JSON.stringify(expectedChartData)); + expect(result.current.chart?.error).toBeUndefined(); + }); + + it('should output undefined for hits and chart if isPlainRecord is true', async () => { + const { result } = await renderUseDiscoverHistogram({ isPlainRecord: true }); + expect(result.current.hits).toBeUndefined(); + expect(result.current.chart).toBeUndefined(); + }); + + it('should output undefined for chart if isTimeBased is false', async () => { + const { result } = await renderUseDiscoverHistogram({ isTimeBased: false }); + expect(result.current.hits).not.toBeUndefined(); + expect(result.current.chart).toBeUndefined(); + }); + }); + + describe('onEditVisualization', () => { + it('returns a callback for onEditVisualization when the data view can be visualized', async () => { + const { result } = await renderUseDiscoverHistogram(); + expect(result.current.onEditVisualization).toBeDefined(); + }); + + it('returns undefined for onEditVisualization when the data view cannot be visualized', async () => { + const { result } = await renderUseDiscoverHistogram({ canVisualize: false }); + expect(result.current.onEditVisualization).toBeUndefined(); + }); + }); + + describe('topPanelHeight', () => { + it('should try to get the topPanelHeight from storage', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + storage.get = jest.fn(() => 100); + const { result } = await renderUseDiscoverHistogram({ storage }); + expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); + expect(result.current.topPanelHeight).toBe(100); + }); + + it('should update topPanelHeight when onTopPanelHeightChange is called', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + storage.get = jest.fn(() => 100); + storage.set = jest.fn(); + const { result } = await renderUseDiscoverHistogram({ storage }); + expect(result.current.topPanelHeight).toBe(100); + act(() => { + result.current.onTopPanelHeightChange(200); + }); + expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, 200); + expect(result.current.topPanelHeight).toBe(200); + }); + }); + + describe('callbacks', () => { + it('should update chartHidden when onChartHiddenChange is called', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + storage.set = jest.fn(); + const stateContainer = { + setAppState: jest.fn(), + }; + const { result } = await renderUseDiscoverHistogram({ + storage, + stateContainer, + }); + act(() => { + result.current.onChartHiddenChange(true); + }); + expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, true); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: true }); + }); + + it('should update interval when onTimeIntervalChange is called', async () => { + const stateContainer = { + setAppState: jest.fn(), + }; + const { result } = await renderUseDiscoverHistogram({ + stateContainer, + }); + act(() => { + result.current.onTimeIntervalChange('auto'); + }); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ interval: 'auto' }); + }); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts new file mode 100644 index 0000000000000..3032fa13af043 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { + getVisualizeInformation, + triggerVisualizeActions, +} from '@kbn/unified-field-list-plugin/public'; +import { buildChartData } from '@kbn/unified-histogram-plugin/public'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { getUiActions } from '../../../../kibana_services'; +import { PLUGIN_ID } from '../../../../../common'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { useDataState } from '../../hooks/use_data_state'; +import type { SavedSearchData } from '../../hooks/use_saved_search'; +import type { AppState, GetStateReturn } from '../../services/discover_state'; + +export const CHART_HIDDEN_KEY = 'discover:chartHidden'; +export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; + +export const useDiscoverHistogram = ({ + stateContainer, + state, + savedSearchData$, + dataView, + savedSearch, + isTimeBased, + isPlainRecord, +}: { + stateContainer: GetStateReturn; + state: AppState; + savedSearchData$: SavedSearchData; + dataView: DataView; + savedSearch: SavedSearch; + isTimeBased: boolean; + isPlainRecord: boolean; +}) => { + const { storage, data } = useDiscoverServices(); + + /** + * Visualize + */ + + const timeField = dataView.timeFieldName && dataView.getFieldByName(dataView.timeFieldName); + const [canVisualize, setCanVisualize] = useState(false); + + useEffect(() => { + if (!timeField) { + return; + } + getVisualizeInformation( + getUiActions(), + timeField, + dataView, + savedSearch.columns || [], + [] + ).then((info) => { + setCanVisualize(Boolean(info)); + }); + }, [dataView, savedSearch.columns, timeField]); + + const onEditVisualization = useCallback(() => { + if (!timeField) { + return; + } + triggerVisualizeActions( + getUiActions(), + timeField, + savedSearch.columns || [], + PLUGIN_ID, + dataView + ); + }, [dataView, savedSearch.columns, timeField]); + + /** + * Height + */ + + const [topPanelHeight, setTopPanelHeight] = useState(() => { + const storedHeight = storage.get(HISTOGRAM_HEIGHT_KEY); + return storedHeight ? Number(storedHeight) : undefined; + }); + + const onTopPanelHeightChange = useCallback( + (newTopPanelHeight: number | undefined) => { + storage.set(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); + setTopPanelHeight(newTopPanelHeight); + }, + [storage] + ); + + /** + * Other callbacks + */ + + const onChartHiddenChange = useCallback( + (chartHidden: boolean) => { + storage.set(CHART_HIDDEN_KEY, chartHidden); + stateContainer.setAppState({ hideChart: chartHidden }); + }, + [stateContainer, storage] + ); + + const onTimeIntervalChange = useCallback( + (newInterval: string) => { + stateContainer.setAppState({ interval: newInterval }); + }, + [stateContainer] + ); + + /** + * Data + */ + + const { fetchStatus: hitsFetchStatus, result: hitsTotal } = useDataState( + savedSearchData$.totalHits$ + ); + + const hits = useMemo( + () => + isPlainRecord + ? undefined + : { + status: hitsFetchStatus, + total: hitsTotal, + }, + [hitsFetchStatus, hitsTotal, isPlainRecord] + ); + + const { fetchStatus: chartFetchStatus, response, error } = useDataState(savedSearchData$.charts$); + + const { bucketInterval, chartData } = useMemo( + () => + buildChartData({ + data, + dataView, + timeInterval: state.interval, + response, + }), + [data, dataView, response, state.interval] + ); + + const chart = useMemo( + () => + isPlainRecord || !isTimeBased + ? undefined + : { + status: chartFetchStatus, + hidden: state.hideChart, + timeInterval: state.interval, + bucketInterval, + data: chartData, + error, + }, + [ + bucketInterval, + chartData, + chartFetchStatus, + error, + isPlainRecord, + isTimeBased, + state.hideChart, + state.interval, + ] + ); + + return { + topPanelHeight, + hits, + chart, + onEditVisualization: canVisualize ? onEditVisualization : undefined, + onTopPanelHeightChange, + onChartHiddenChange, + onTimeIntervalChange, + }; +}; diff --git a/src/plugins/discover/public/application/main/hooks/use_data_state.ts b/src/plugins/discover/public/application/main/hooks/use_data_state.ts index 7bfa4205081e9..fe512e747b4a1 100644 --- a/src/plugins/discover/public/application/main/hooks/use_data_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_data_state.ts @@ -9,8 +9,8 @@ import { useState, useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; import { DataMsg } from './use_saved_search'; -export function useDataState(data$: BehaviorSubject) { - const [fetchState, setFetchState] = useState(data$.getValue()); +export function useDataState(data$: BehaviorSubject) { + const [fetchState, setFetchState] = useState(data$.getValue()); useEffect(() => { const subscription = data$.subscribe((next) => { diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 4762748e2618b..2f097daac982d 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -12,12 +12,12 @@ import { ISearchSource } from '@kbn/data-plugin/public'; import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { AggregateQuery, Query } from '@kbn/es-query'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { getRawRecordType } from '../utils/get_raw_record_type'; import { DiscoverServices } from '../../../build_services'; import { DiscoverSearchSessionManager } from '../services/discover_search_session'; import { GetStateReturn } from '../services/discover_state'; import { validateTimeRange } from '../utils/validate_time_range'; -import { Chart } from '../components/chart/point_series'; import { useSingleton } from './use_singleton'; import { FetchStatus } from '../../types'; import { fetchAll } from '../utils/fetch_all'; @@ -34,12 +34,6 @@ export interface SavedSearchData { availableFields$: AvailableFields$; } -export interface TimechartBucketInterval { - scaled?: boolean; - description?: string; - scale?: number; -} - export type DataMain$ = BehaviorSubject; export type DataDocuments$ = BehaviorSubject; export type DataTotalHits$ = BehaviorSubject; @@ -90,8 +84,7 @@ export interface DataTotalHitsMsg extends DataMsg { } export interface DataChartsMessage extends DataMsg { - bucketInterval?: TimechartBucketInterval; - chartData?: Chart; + response?: SearchResponse; } export interface DataAvailableFieldsMsg extends DataMsg { diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index 0d6caf180a3f6..ae5abb36378a8 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -107,8 +107,7 @@ export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchSta }); data.charts$.next({ fetchStatus: initialFetchStatus, - chartData: undefined, - bucketInterval: undefined, + response: undefined, recordRawType, }); data.totalHits$.next({ diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 288595fa5f7a5..59dbc3ffe73d8 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -30,6 +30,7 @@ import { fetchChart } from './fetch_chart'; import { fetchTotalHits } from './fetch_total_hits'; import { buildDataTableRecord } from '../../../utils/build_data_record'; import { dataViewMock } from '../../../__mocks__/data_view'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; jest.mock('./fetch_documents', () => ({ fetchDocuments: jest.fn().mockResolvedValue([]), @@ -99,8 +100,7 @@ describe('test fetchAll', () => { mockFetchTotalHits.mockReset().mockResolvedValue(42); mockFetchChart .mockReset() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockResolvedValue({ totalHits: 42, chartData: {} as any, bucketInterval: {} }); + .mockResolvedValue({ totalHits: 42, response: {} as unknown as SearchResponse }); }); test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { @@ -157,7 +157,7 @@ describe('test fetchAll', () => { ]); }); - test('emits loading and chartData on charts$ correctly', async () => { + test('emits loading and response on charts$ correctly', async () => { const collect = subjectCollector(subjects.charts$); searchSource.getField('index')!.isTimeBased = () => true; await fetchAll(subjects, searchSource, false, deps); @@ -167,8 +167,7 @@ describe('test fetchAll', () => { { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', - bucketInterval: {}, - chartData: {}, + response: {}, }, ]); }); @@ -176,8 +175,7 @@ describe('test fetchAll', () => { test('should use charts query to fetch total hit count when chart is visible', async () => { const collect = subjectCollector(subjects.totalHits$); searchSource.getField('index')!.isTimeBased = () => true; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockFetchChart.mockResolvedValue({ bucketInterval: {}, chartData: {} as any, totalHits: 32 }); + mockFetchChart.mockResolvedValue({ totalHits: 32, response: {} as unknown as SearchResponse }); await fetchAll(subjects, searchSource, false, deps); expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 853ae6cebfb76..d530da1492fac 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -172,8 +172,7 @@ export function fetchAll( dataSubjects.charts$.next({ fetchStatus: FetchStatus.COMPLETE, - chartData: chart.chartData, - bucketInterval: chart.bucketInterval, + response: chart.response, recordRawType, }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index d2e23dc227564..e1020404d3996 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -107,8 +107,7 @@ describe('test fetchCharts', () => { const result = await fetchChart(savedSearchMockWithTimeField.searchSource, getDeps()); expect(result).toHaveProperty('totalHits', 42); - expect(result).toHaveProperty('bucketInterval.description', '0 milliseconds'); - expect(result).toHaveProperty('chartData'); + expect(result).toHaveProperty('response'); }); test('rejects promise on query failure', async () => { diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts index 66cd66f1418d8..e4e5b67782cb9 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.ts @@ -5,33 +5,27 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import { i18n } from '@kbn/i18n'; import { filter, map } from 'rxjs/operators'; import { lastValueFrom } from 'rxjs'; -import { - DataPublicPluginStart, - isCompleteResponse, - search, - ISearchSource, - tabifyAggResponse, -} from '@kbn/data-plugin/public'; -import { getChartAggConfigs, getDimensions } from '.'; -import { buildPointSeriesData, Chart } from '../components/chart/point_series'; -import { TimechartBucketInterval } from '../hooks/use_saved_search'; +import { DataPublicPluginStart, isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { getChartAggConfigs } from '@kbn/unified-histogram-plugin/public'; import { FetchDeps } from './fetch_all'; interface Result { totalHits: number; - chartData: Chart; - bucketInterval: TimechartBucketInterval | undefined; + response: SearchResponse; } export function fetchChart( searchSource: ISearchSource, { abortController, appStateContainer, data, inspectorAdapters, searchSessionId }: FetchDeps ): Promise { - const interval = appStateContainer.getState().interval ?? 'auto'; - const chartAggConfigs = updateSearchSource(searchSource, interval, data); + const timeInterval = appStateContainer.getState().interval ?? 'auto'; + + updateSearchSource(searchSource, timeInterval, data); const executionContext = { description: 'fetch chart data and total hits', @@ -55,20 +49,10 @@ export function fetchChart( }) .pipe( filter((res) => isCompleteResponse(res)), - map((res) => { - const bucketAggConfig = chartAggConfigs.aggs[1]; - const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse); - const dimensions = getDimensions(chartAggConfigs, data); - const bucketInterval = search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) - ? bucketAggConfig?.buckets?.getInterval() - : undefined; - const chartData = buildPointSeriesData(tabifiedData, dimensions!); - return { - chartData, - bucketInterval, - totalHits: res.rawResponse.hits.total as number, - }; - }) + map((res) => ({ + response: res.rawResponse, + totalHits: res.rawResponse.hits.total as number, + })) ); return lastValueFrom(fetch$); @@ -76,14 +60,14 @@ export function fetchChart( export function updateSearchSource( searchSource: ISearchSource, - interval: string, + timeInterval: string, data: DataPublicPluginStart ) { const dataView = searchSource.getField('index')!; searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(dataView)); searchSource.setField('size', 0); searchSource.setField('trackTotalHits', true); - const chartAggConfigs = getChartAggConfigs(searchSource, interval, data); + const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); searchSource.setField('aggs', chartAggConfigs.toDsl()); searchSource.removeField('sort'); searchSource.removeField('fields'); diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts index a4a43fa342715..b8b3c3579f343 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -19,7 +19,7 @@ import { } from '../../../../common'; import { AppState } from '../services/discover_state'; -import { CHART_HIDDEN_KEY } from '../components/chart/discover_chart'; +import { CHART_HIDDEN_KEY } from '../components/layout/use_discover_histogram'; function getDefaultColumns(savedSearch: SavedSearch, uiSettings: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 27cfcb9079f21..93488793f8237 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -33,6 +33,7 @@ { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "../saved_objects_tagging_oss/tsconfig.json" } + { "path": "../saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../unified_histogram/tsconfig.json" } ] } diff --git a/src/plugins/unified_histogram/README.md b/src/plugins/unified_histogram/README.md new file mode 100755 index 0000000000000..301216dfefdbf --- /dev/null +++ b/src/plugins/unified_histogram/README.md @@ -0,0 +1,3 @@ +# unifiedHistogram + +The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. diff --git a/src/plugins/unified_histogram/jest.config.js b/src/plugins/unified_histogram/jest.config.js new file mode 100644 index 0000000000000..3b8213b533691 --- /dev/null +++ b/src/plugins/unified_histogram/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/unified_histogram'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/unified_histogram', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/unified_histogram/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/unified_histogram/kibana.json b/src/plugins/unified_histogram/kibana.json new file mode 100755 index 0000000000000..f89f9c9d4c714 --- /dev/null +++ b/src/plugins/unified_histogram/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "unifiedHistogram", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Data Discovery", + "githubTeam": "kibana-data-discovery" + }, + "description": "The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display.", + "server": false, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [], + "requiredBundles": ["charts", "data"] +} diff --git a/src/plugins/unified_histogram/public/__mocks__/data_view.ts b/src/plugins/unified_histogram/public/__mocks__/data_view.ts new file mode 100644 index 0000000000000..e51b9560949ab --- /dev/null +++ b/src/plugins/unified_histogram/public/__mocks__/data_view.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DataView } from '@kbn/data-views-plugin/public'; + +const fields = [ + { + name: '_source', + type: '_source', + scripted: false, + filterable: false, + aggregatable: false, + }, + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + aggregatable: false, + }, + { + name: 'message', + type: 'string', + displayName: 'message', + scripted: false, + filterable: false, + aggregatable: false, + }, + { + name: 'extension', + type: 'string', + displayName: 'extension', + scripted: false, + filterable: true, + aggregatable: true, + }, + { + name: 'bytes', + type: 'number', + displayName: 'bytesDisplayName', + scripted: false, + filterable: true, + aggregatable: true, + sortable: true, + }, + { + name: 'scripted', + type: 'number', + displayName: 'scripted', + scripted: true, + filterable: false, + }, + { + name: 'object.value', + type: 'number', + displayName: 'object.value', + scripted: false, + filterable: true, + aggregatable: true, + }, +] as DataView['fields']; + +export const buildDataViewMock = ({ + name, + fields: definedFields, + timeFieldName, +}: { + name: string; + fields: DataView['fields']; + timeFieldName?: string; +}): DataView => { + const dataViewFields = [...definedFields] as DataView['fields']; + + dataViewFields.getByName = (fieldName: string) => { + return dataViewFields.find((field) => field.name === fieldName); + }; + + dataViewFields.getAll = () => { + return dataViewFields; + }; + + const dataView = { + id: `${name}-id`, + title: `${name}-title`, + name, + metaFields: ['_index', '_score'], + fields: dataViewFields, + getName: () => name, + getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), + getSourceFiltering: () => ({}), + getFieldByName: jest.fn((fieldName: string) => dataViewFields.getByName(fieldName)), + timeFieldName: timeFieldName || '', + docvalueFields: [], + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + isTimeNanosBased: () => false, + isPersisted: () => true, + } as unknown as DataView; + + dataView.isTimeBased = () => !!timeFieldName; + + return dataView; +}; + +export const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields }); diff --git a/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts b/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts new file mode 100644 index 0000000000000..158d697d67c71 --- /dev/null +++ b/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DataView } from '@kbn/data-views-plugin/public'; +import { buildDataViewMock } from './data_view'; + +const fields = [ + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'timestamp', + type: 'date', + scripted: false, + filterable: true, + aggregatable: true, + sortable: true, + }, + { + name: 'message', + type: 'string', + scripted: false, + filterable: false, + }, + { + name: 'extension', + type: 'string', + scripted: false, + filterable: true, + aggregatable: true, + }, + { + name: 'bytes', + type: 'number', + scripted: false, + filterable: true, + aggregatable: true, + }, + { + name: 'scripted', + type: 'number', + scripted: true, + filterable: false, + }, +] as DataView['fields']; + +export const dataViewWithTimefieldMock = buildDataViewMock({ + name: 'index-pattern-with-timefield', + fields, + timeFieldName: 'timestamp', +}); diff --git a/src/plugins/unified_histogram/public/__mocks__/services.ts b/src/plugins/unified_histogram/public/__mocks__/services.ts new file mode 100644 index 0000000000000..e827596d88feb --- /dev/null +++ b/src/plugins/unified_histogram/public/__mocks__/services.ts @@ -0,0 +1,28 @@ +/* + * 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 { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; +import type { UnifiedHistogramServices } from '../types'; + +const dataPlugin = dataPluginMock.createStartContract(); +dataPlugin.query.filterManager.getFilters = jest.fn(() => []); + +export const unifiedHistogramServicesMock = { + data: dataPlugin, + fieldFormats: fieldFormatsMock, + uiSettings: { + get: jest.fn(), + isDefault: jest.fn(() => true), + }, + theme: { + useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), + useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), + }, +} as unknown as UnifiedHistogramServices; diff --git a/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts b/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts new file mode 100644 index 0000000000000..6c920a4a1a5ab --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { calculateBounds } from '@kbn/data-plugin/public'; +import { buildChartData } from './build_chart_data'; + +describe('buildChartData', () => { + const getOptions = () => { + const response = { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 29, + max_score: null, + hits: [], + }, + aggregations: { + '2': { + buckets: [ + { + key_as_string: '2022-10-05T16:00:00.000-03:00', + key: 1664996400000, + doc_count: 6, + }, + { + key_as_string: '2022-10-05T16:30:00.000-03:00', + key: 1664998200000, + doc_count: 2, + }, + { + key_as_string: '2022-10-05T17:00:00.000-03:00', + key: 1665000000000, + doc_count: 3, + }, + { + key_as_string: '2022-10-05T17:30:00.000-03:00', + key: 1665001800000, + doc_count: 8, + }, + { + key_as_string: '2022-10-05T18:00:00.000-03:00', + key: 1665003600000, + doc_count: 10, + }, + ], + }, + }, + }; + const dataView = dataViewWithTimefieldMock; + const dataMock = dataPluginMock.createStartContract(); + dataMock.query.timefilter.timefilter.getTime = () => { + return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; + }; + dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); + }; + return { + data: dataMock, + dataView, + timeInterval: 'auto', + response, + }; + }; + + const expectedChartData = { + xAxisOrderedValues: [1664996400000, 1664998200000, 1665000000000, 1665001800000, 1665003600000], + xAxisFormat: { id: 'date', params: { pattern: 'HH:mm:ss.SSS' } }, + xAxisLabel: 'timestamp per 0 milliseconds', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: 'P0D', + intervalESUnit: 'ms', + intervalESValue: 0, + min: '1991-03-29T08:04:00.694Z', + max: '2021-03-29T07:04:00.695Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1664996400000, y: 6 }, + { x: 1664998200000, y: 2 }, + { x: 1665000000000, y: 3 }, + { x: 1665001800000, y: 8 }, + { x: 1665003600000, y: 10 }, + ], + }; + + it('should return the correct data', () => { + const { bucketInterval, chartData } = buildChartData(getOptions()); + expect(bucketInterval!.toString()).toEqual('P0D'); + expect(JSON.stringify(chartData)).toEqual(JSON.stringify(expectedChartData)); + }); + + it('should return an empty object if response or timeInterval is undefined', () => { + expect( + buildChartData({ + ...getOptions(), + response: undefined, + timeInterval: undefined, + }) + ).toEqual({}); + expect( + buildChartData({ + ...getOptions(), + response: undefined, + }) + ).toEqual({}); + expect( + buildChartData({ + ...getOptions(), + timeInterval: undefined, + }) + ).toEqual({}); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/build_chart_data.ts b/src/plugins/unified_histogram/public/chart/build_chart_data.ts new file mode 100644 index 0000000000000..03b208802ac4d --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/build_chart_data.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { UnifiedHistogramBucketInterval } from '../types'; +import { buildPointSeriesData } from './build_point_series_data'; +import { getChartAggConfigs } from './get_chart_agg_configs'; +import { getDimensions } from './get_dimensions'; + +/** + * Convert the response from the chart request into a format that can be used + * by the unified histogram chart. The returned object should be used to update + * {@link UnifiedHistogramChartContext.bucketInterval} and {@link UnifiedHistogramChartContext.data}. + */ +export const buildChartData = ({ + data, + dataView, + timeInterval, + response, +}: { + data: DataPublicPluginStart; + dataView: DataView; + timeInterval?: string; + response?: SearchResponse; +}) => { + if (!timeInterval || !response) { + return {}; + } + + const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); + const bucketAggConfig = chartAggConfigs.aggs[1]; + const tabifiedData = tabifyAggResponse(chartAggConfigs, response); + const dimensions = getDimensions(chartAggConfigs, data); + const bucketInterval = search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + ? (bucketAggConfig?.buckets?.getInterval() as UnifiedHistogramBucketInterval) + : undefined; + const chartData = buildPointSeriesData(tabifiedData, dimensions!); + + return { + bucketInterval, + chartData, + }; +}; diff --git a/src/plugins/discover/public/application/main/components/chart/point_series.test.ts b/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts similarity index 96% rename from src/plugins/discover/public/application/main/components/chart/point_series.test.ts rename to src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts index e351daa1930f8..3a7f81aa4cd40 100644 --- a/src/plugins/discover/public/application/main/components/chart/point_series.test.ts +++ b/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { buildPointSeriesData } from './point_series'; +import { buildPointSeriesData } from './build_point_series_data'; import moment from 'moment'; -import { Unit } from '@kbn/datemath'; +import type { Unit } from '@kbn/datemath'; describe('buildPointSeriesData', () => { test('with valid data', () => { diff --git a/src/plugins/unified_histogram/public/chart/build_point_series_data.ts b/src/plugins/unified_histogram/public/chart/build_point_series_data.ts new file mode 100644 index 0000000000000..dc9d97fd0708f --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/build_point_series_data.ts @@ -0,0 +1,45 @@ +/* + * 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 { uniq } from 'lodash'; +import type { UnifiedHistogramChartData, Dimensions, Table } from '../types'; + +export const buildPointSeriesData = ( + table: Table, + dimensions: Dimensions +): UnifiedHistogramChartData => { + const { x, y } = dimensions; + const xAccessor = table.columns[x.accessor].id; + const yAccessor = table.columns[y.accessor].id; + const chart = {} as UnifiedHistogramChartData; + + chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); + chart.xAxisFormat = x.format; + chart.xAxisLabel = table.columns[x.accessor].name; + chart.yAxisFormat = y.format; + const { intervalESUnit, intervalESValue, interval, bounds } = x.params; + chart.ordered = { + date: true, + interval, + intervalESUnit, + intervalESValue, + min: bounds.min, + max: bounds.max, + }; + + chart.yAxisLabel = table.columns[y.accessor].name; + + chart.values = table.rows + .filter((row) => row && row[yAccessor] !== 'NaN') + .map((row) => ({ + x: row[xAccessor] as number, + y: row[yAccessor] as number, + })); + + return chart; +}; diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx new file mode 100644 index 0000000000000..41de0687acfa6 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -0,0 +1,185 @@ +/* + * 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, { ReactElement } from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import type { UnifiedHistogramChartData, UnifiedHistogramFetchStatus } from '../types'; +import { Chart } from './chart'; +import type { ReactWrapper } from 'enzyme'; +import { unifiedHistogramServicesMock } from '../__mocks__/services'; +import { HitsCounter } from '../hits_counter'; + +async function mountComponent({ + noChart, + noHits, + chartHidden = false, + appendHistogram, + onEditVisualization = jest.fn(), +}: { + noChart?: boolean; + noHits?: boolean; + chartHidden?: boolean; + appendHistogram?: ReactElement; + onEditVisualization?: null | (() => void); +} = {}) { + const services = unifiedHistogramServicesMock; + services.data.query.timefilter.timefilter.getAbsoluteTime = () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + const chartData = { + xAxisOrderedValues: [ + 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, + 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, + 1624917600000, 1625004000000, 1625090400000, + ], + xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + xAxisLabel: 'order_date per day', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: { + asMilliseconds: jest.fn(), + }, + intervalESUnit: 'd', + intervalESValue: 1, + min: '2021-03-18T08:28:56.411Z', + max: '2021-07-01T07:28:56.411Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1623880800000, y: 134 }, + { x: 1623967200000, y: 152 }, + { x: 1624053600000, y: 141 }, + { x: 1624140000000, y: 138 }, + { x: 1624226400000, y: 142 }, + { x: 1624312800000, y: 157 }, + { x: 1624399200000, y: 149 }, + { x: 1624485600000, y: 146 }, + { x: 1624572000000, y: 170 }, + { x: 1624658400000, y: 137 }, + { x: 1624744800000, y: 150 }, + { x: 1624831200000, y: 144 }, + { x: 1624917600000, y: 147 }, + { x: 1625004000000, y: 137 }, + { x: 1625090400000, y: 66 }, + ], + } as unknown as UnifiedHistogramChartData; + + const props = { + services: unifiedHistogramServicesMock, + hits: noHits + ? undefined + : { + status: 'complete' as UnifiedHistogramFetchStatus, + number: 2, + }, + chart: noChart + ? undefined + : { + status: 'complete' as UnifiedHistogramFetchStatus, + hidden: chartHidden, + timeInterval: 'auto', + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + data: chartData, + }, + appendHistogram, + onEditVisualization: onEditVisualization || undefined, + onResetChartHeight: jest.fn(), + onChartHiddenChange: jest.fn(), + onTimeIntervalChange: jest.fn(), + }; + + let instance: ReactWrapper = {} as ReactWrapper; + await act(async () => { + instance = mountWithIntl(); + // wait for initial async loading to complete + await new Promise((r) => setTimeout(r, 0)); + await instance.update(); + }); + return instance; +} + +describe('Chart', () => { + test('render when chart is undefined', async () => { + const component = await mountComponent({ noChart: true }); + expect( + component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + ).toBeFalsy(); + }); + + test('render when chart is defined and onEditVisualization is undefined', async () => { + const component = await mountComponent({ onEditVisualization: null }); + expect( + component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + ).toBeTruthy(); + expect( + component.find('[data-test-subj="unifiedHistogramEditVisualization"]').exists() + ).toBeFalsy(); + }); + + test('render when chart is defined and onEditVisualization is defined', async () => { + const component = await mountComponent(); + expect( + component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + ).toBeTruthy(); + expect( + component.find('[data-test-subj="unifiedHistogramEditVisualization"]').exists() + ).toBeTruthy(); + }); + + test('render when chart.hidden is true', async () => { + const component = await mountComponent({ chartHidden: true }); + expect( + component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + ).toBeTruthy(); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy(); + }); + + test('render when chart.hidden is false', async () => { + const component = await mountComponent({ chartHidden: false }); + expect( + component.find('[data-test-subj="unifiedHistogramChartOptionsToggle"]').exists() + ).toBeTruthy(); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); + }); + + test('triggers onEditVisualization on click', async () => { + const fn = jest.fn(); + const component = await mountComponent({ onEditVisualization: fn }); + await act(async () => { + await component + .find('[data-test-subj="unifiedHistogramEditVisualization"]') + .first() + .simulate('click'); + }); + + expect(fn).toHaveBeenCalled(); + }); + + it('should render HitsCounter when hits is defined', async () => { + const component = await mountComponent(); + expect(component.find(HitsCounter).exists()).toBeTruthy(); + }); + + it('should not render HitsCounter when hits is undefined', async () => { + const component = await mountComponent({ noHits: true }); + expect(component.find(HitsCounter).exists()).toBeFalsy(); + }); + + it('should render the element passed to appendHistogram', async () => { + const appendHistogram =
; + const component = await mountComponent({ appendHistogram }); + expect(component.find('[data-test-subj="appendHistogram"]').exists()).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx similarity index 53% rename from src/plugins/discover/public/application/main/components/chart/discover_chart.tsx rename to src/plugins/unified_histogram/public/chart/chart.tsx index e7cc01fb00eaa..0f6c47f8a532e 100644 --- a/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -5,7 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { memo, ReactElement, useCallback, useEffect, useRef, useState } from 'react'; + +import type { ReactElement } from 'react'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import moment from 'moment'; import { EuiButtonIcon, @@ -14,54 +16,48 @@ import { EuiFlexItem, EuiPopover, EuiToolTip, + useEuiBreakpoint, + useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { SavedSearch } from '@kbn/saved-search-plugin/public'; -import { - getVisualizeInformation, - triggerVisualizeActions, -} from '@kbn/unified-field-list-plugin/public'; +import { css } from '@emotion/react'; import { HitsCounter } from '../hits_counter'; -import { GetStateReturn } from '../../services/discover_state'; -import { DiscoverHistogram } from './histogram'; -import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search'; +import { Histogram } from './histogram'; import { useChartPanels } from './use_chart_panels'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; -import { getUiActions } from '../../../../kibana_services'; -import { PLUGIN_ID } from '../../../../../common'; +import type { + UnifiedHistogramChartContext, + UnifiedHistogramHitsContext, + UnifiedHistogramServices, +} from '../types'; -const DiscoverHistogramMemoized = memo(DiscoverHistogram); -export const CHART_HIDDEN_KEY = 'discover:chartHidden'; +export interface ChartProps { + className?: string; + services: UnifiedHistogramServices; + hits?: UnifiedHistogramHitsContext; + chart?: UnifiedHistogramChartContext; + appendHitsCounter?: ReactElement; + appendHistogram?: ReactElement; + onEditVisualization?: () => void; + onResetChartHeight?: () => void; + onChartHiddenChange?: (chartHidden: boolean) => void; + onTimeIntervalChange?: (timeInterval: string) => void; +} -export function DiscoverChart({ +const HistogramMemoized = memo(Histogram); + +export function Chart({ className, - resetSavedSearch, - savedSearch, - savedSearchDataChart$, - savedSearchDataTotalHits$, - stateContainer, - dataView, - hideChart, - interval, - isTimeBased, + services, + hits, + chart, + appendHitsCounter, appendHistogram, + onEditVisualization, onResetChartHeight, -}: { - className?: string; - resetSavedSearch: () => void; - savedSearch: SavedSearch; - savedSearchDataChart$: DataCharts$; - savedSearchDataTotalHits$: DataTotalHits$; - stateContainer: GetStateReturn; - dataView: DataView; - isTimeBased: boolean; - hideChart?: boolean; - interval?: string; - appendHistogram?: ReactElement; - onResetChartHeight?: () => void; -}) { - const { data, storage } = useDiscoverServices(); + onChartHiddenChange, + onTimeIntervalChange, +}: ChartProps) { + const { data } = services; const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ @@ -69,35 +65,6 @@ export function DiscoverChart({ moveFocus: false, }); - const timeField = dataView.timeFieldName && dataView.getFieldByName(dataView.timeFieldName); - const [canVisualize, setCanVisualize] = useState(false); - - useEffect(() => { - if (!timeField) return; - getVisualizeInformation( - getUiActions(), - timeField, - dataView, - savedSearch.columns || [], - [] - ).then((info) => { - setCanVisualize(Boolean(info)); - }); - }, [dataView, savedSearch.columns, timeField]); - - const onEditVisualization = useCallback(() => { - if (!timeField) { - return; - } - triggerVisualizeActions( - getUiActions(), - timeField, - savedSearch.columns || [], - PLUGIN_ID, - dataView - ); - }, [dataView, savedSearch.columns, timeField]); - const onShowChartOptions = useCallback(() => { setShowChartOptionsPopover(!showChartOptionsPopover); }, [showChartOptionsPopover]); @@ -110,14 +77,13 @@ export function DiscoverChart({ if (chartRef.current.moveFocus && chartRef.current.element) { chartRef.current.element.focus(); } - }, [hideChart]); + }, [chart?.hidden]); const toggleHideChart = useCallback(() => { - const newHideChart = !hideChart; - chartRef.current.moveFocus = !newHideChart; - storage.set(CHART_HIDDEN_KEY, newHideChart); - stateContainer.setAppState({ hideChart: newHideChart }); - }, [hideChart, stateContainer, storage]); + const chartHidden = !chart?.hidden; + chartRef.current.moveFocus = !chartHidden; + onChartHiddenChange?.(chartHidden); + }, [chart?.hidden, onChartHiddenChange]); const timefilterUpdateHandler = useCallback( (ranges: { from: number; to: number }) => { @@ -129,15 +95,43 @@ export function DiscoverChart({ }, [data] ); + const panels = useChartPanels({ + chart, toggleHideChart, - onChangeInterval: (newInterval) => stateContainer.setAppState({ interval: newInterval }), + onTimeIntervalChange: (timeInterval) => onTimeIntervalChange?.(timeInterval), closePopover: () => setShowChartOptionsPopover(false), onResetChartHeight, - hideChart, - interval, }); + const { euiTheme } = useEuiTheme(); + const resultCountCss = css` + padding: ${euiTheme.size.s}; + min-height: ${euiTheme.base * 3}px; + `; + const resultCountTitleCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + margin-bottom: 0 !important; + } + `; + const resultCountToggleCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + align-items: flex-end; + } + `; + const timechartCss = css` + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } + `; + return ( - + - + {hits && } - {isTimeBased && ( - + {chart && ( + - {canVisualize && ( + {onEditVisualization && ( @@ -172,8 +163,8 @@ export function DiscoverChart({ size="xs" iconType="lensApp" onClick={onEditVisualization} - data-test-subj="discoverEditVisualization" - aria-label={i18n.translate('discover.editVisualizationButton', { + data-test-subj="unifiedHistogramEditVisualization" + aria-label={i18n.translate('unifiedHistogram.editVisualizationButton', { defaultMessage: 'Edit visualization', })} /> @@ -182,10 +173,10 @@ export function DiscoverChart({ )} @@ -193,8 +184,8 @@ export function DiscoverChart({ size="xs" iconType="gear" onClick={onShowChartOptions} - data-test-subj="discoverChartOptionsToggle" - aria-label={i18n.translate('discover.chartOptionsButton', { + data-test-subj="unifiedHistogramChartOptionsToggle" + aria-label={i18n.translate('unifiedHistogram.chartOptionsButton', { defaultMessage: 'Chart options', })} /> @@ -213,20 +204,20 @@ export function DiscoverChart({ )} - {isTimeBased && !hideChart && ( + {chart && !chart.hidden && (
(chartRef.current.element = element)} tabIndex={-1} - aria-label={i18n.translate('discover.histogramOfFoundDocumentsAriaLabel', { + aria-label={i18n.translate('unifiedHistogram.histogramOfFoundDocumentsAriaLabel', { defaultMessage: 'Histogram of found documents', })} - className="dscTimechart" + css={timechartCss} > -
{appendHistogram} diff --git a/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts b/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts similarity index 75% rename from src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts rename to src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts index 67a7fe5285ffe..3b4f470ba6119 100644 --- a/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts +++ b/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts @@ -5,28 +5,16 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; -import { ISearchSource } from '@kbn/data-plugin/public'; + +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { getChartAggConfigs } from './get_chart_agg_configs'; describe('getChartAggConfigs', () => { test('is working', () => { const dataView = dataViewWithTimefieldMock; - const setField = jest.fn(); - const searchSource = { - setField, - getField: (name: string) => { - if (name === 'index') { - return dataView; - } - }, - removeField: jest.fn(), - } as unknown as ISearchSource; - const dataMock = dataPluginMock.createStartContract(); - - const aggsConfig = getChartAggConfigs(searchSource, 'auto', dataMock); + const aggsConfig = getChartAggConfigs({ dataView, timeInterval: 'auto', data: dataMock }); expect(aggsConfig!.aggs).toMatchInlineSnapshot(` Array [ diff --git a/src/plugins/discover/public/application/main/utils/get_chart_agg_configs.ts b/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts similarity index 62% rename from src/plugins/discover/public/application/main/utils/get_chart_agg_configs.ts rename to src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts index 035a56fa4be16..93ef7f3dd9188 100644 --- a/src/plugins/discover/public/application/main/utils/get_chart_agg_configs.ts +++ b/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts @@ -5,18 +5,22 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; + +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; /** - * Helper function to apply or remove aggregations to a given search source used for gaining data - * for Discover's histogram vis + * Helper function to get the agg configs required for the unified histogram chart request */ -export function getChartAggConfigs( - searchSource: ISearchSource, - histogramInterval: string, - data: DataPublicPluginStart -) { - const dataView = searchSource.getField('index')!; +export function getChartAggConfigs({ + dataView, + timeInterval, + data, +}: { + dataView: DataView; + timeInterval: string; + data: DataPublicPluginStart; +}) { const visStateAggs = [ { type: 'count', @@ -27,7 +31,7 @@ export function getChartAggConfigs( schema: 'segment', params: { field: dataView.timeFieldName!, - interval: histogramInterval, + interval: timeInterval, timeRange: data.query.timefilter.timefilter.getTime(), }, }, diff --git a/src/plugins/discover/public/application/main/utils/get_dimensions.test.ts b/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts similarity index 78% rename from src/plugins/discover/public/application/main/utils/get_dimensions.test.ts rename to src/plugins/unified_histogram/public/chart/get_dimensions.test.ts index 2de9acace965c..fd26fa20ce793 100644 --- a/src/plugins/discover/public/application/main/utils/get_dimensions.test.ts +++ b/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts @@ -5,26 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { getDimensions } from './get_dimensions'; -import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; -import { ISearchSource, calculateBounds } from '@kbn/data-plugin/public'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { calculateBounds } from '@kbn/data-plugin/public'; import { getChartAggConfigs } from './get_chart_agg_configs'; test('getDimensions', () => { const dataView = dataViewWithTimefieldMock; - const setField = jest.fn(); - const searchSource = { - setField, - removeField: jest.fn(), - getField: (name: string) => { - if (name === 'index') { - return dataView; - } - }, - } as unknown as ISearchSource; - const dataMock = dataPluginMock.createStartContract(); dataMock.query.timefilter.timefilter.getTime = () => { return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; @@ -32,8 +21,7 @@ test('getDimensions', () => { dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { return calculateBounds(timeRange); }; - - const aggsConfig = getChartAggConfigs(searchSource, 'auto', dataMock); + const aggsConfig = getChartAggConfigs({ dataView, timeInterval: 'auto', data: dataMock }); const actual = getDimensions(aggsConfig!, dataMock); expect(actual).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/discover/public/application/main/utils/get_dimensions.ts b/src/plugins/unified_histogram/public/chart/get_dimensions.ts similarity index 95% rename from src/plugins/discover/public/application/main/utils/get_dimensions.ts rename to src/plugins/unified_histogram/public/chart/get_dimensions.ts index a1ea11609b8ca..94ed3d4540d21 100644 --- a/src/plugins/discover/public/application/main/utils/get_dimensions.ts +++ b/src/plugins/unified_histogram/public/chart/get_dimensions.ts @@ -5,10 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import moment from 'moment'; import dateMath from '@kbn/datemath'; import { DataPublicPluginStart, search, IAggConfigs } from '@kbn/data-plugin/public'; -import { Dimensions, HistogramParamsBounds } from '../components/chart/point_series'; +import type { Dimensions, HistogramParamsBounds } from '../types'; export function getDimensions( aggs: IAggConfigs, diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx new file mode 100644 index 0000000000000..3e1213978e385 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import type { UnifiedHistogramChartData, UnifiedHistogramFetchStatus } from '../types'; +import { Histogram } from './histogram'; +import React from 'react'; +import { unifiedHistogramServicesMock } from '../__mocks__/services'; + +const chartData = { + xAxisOrderedValues: [ + 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, + 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, + 1624917600000, 1625004000000, 1625090400000, + ], + xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + xAxisLabel: 'order_date per day', + yAxisFormat: { id: 'number' }, + ordered: { + date: true, + interval: { + asMilliseconds: jest.fn(), + }, + intervalESUnit: 'd', + intervalESValue: 1, + min: '2021-03-18T08:28:56.411Z', + max: '2021-07-01T07:28:56.411Z', + }, + yAxisLabel: 'Count', + values: [ + { x: 1623880800000, y: 134 }, + { x: 1623967200000, y: 152 }, + { x: 1624053600000, y: 141 }, + { x: 1624140000000, y: 138 }, + { x: 1624226400000, y: 142 }, + { x: 1624312800000, y: 157 }, + { x: 1624399200000, y: 149 }, + { x: 1624485600000, y: 146 }, + { x: 1624572000000, y: 170 }, + { x: 1624658400000, y: 137 }, + { x: 1624744800000, y: 150 }, + { x: 1624831200000, y: 144 }, + { x: 1624917600000, y: 147 }, + { x: 1625004000000, y: 137 }, + { x: 1625090400000, y: 66 }, + ], +} as unknown as UnifiedHistogramChartData; + +function mountComponent( + status: UnifiedHistogramFetchStatus, + data: UnifiedHistogramChartData | null = chartData, + error?: Error +) { + const services = unifiedHistogramServicesMock; + services.data.query.timefilter.timefilter.getAbsoluteTime = () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + const timefilterUpdateHandler = jest.fn(); + + const props = { + services: unifiedHistogramServicesMock, + chart: { + status, + hidden: false, + timeInterval: 'auto', + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + data: data ?? undefined, + error, + }, + timefilterUpdateHandler, + }; + + return mountWithIntl(); +} + +describe('Histogram', () => { + it('renders correctly', () => { + const component = mountComponent('complete'); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); + }); + + it('renders error correctly', () => { + const component = mountComponent('error', null, new Error('Loading error')); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(false); + expect(component.find('[data-test-subj="unifiedHistogramErrorChartContainer"]').exists()).toBe( + true + ); + expect( + component.find('[data-test-subj="unifiedHistogramErrorChartText"]').get(1).props.children + ).toBe('Loading error'); + }); + + it('renders loading state correctly', () => { + const component = mountComponent('loading', null); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); + expect(component.find('[data-test-subj="unifiedHistogramChartLoading"]').exists()).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx similarity index 73% rename from src/plugins/discover/public/application/main/components/chart/histogram.tsx rename to src/plugins/unified_histogram/public/chart/histogram.tsx index 62ade96fceb86..a201258e49bf2 100644 --- a/src/plugins/discover/public/application/main/components/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import './histogram.scss'; + import moment, { unitOfTime } from 'moment-timezone'; import React, { useCallback, useMemo } from 'react'; import { @@ -16,23 +16,26 @@ import { EuiLoadingChart, EuiSpacer, EuiText, + useEuiTheme, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import dateMath from '@kbn/datemath'; +import type { + BrushEndListener, + ElementClickListener, + XYBrushEvent, + XYChartElementEvent, +} from '@elastic/charts'; import { Axis, - BrushEndListener, Chart, - ElementClickListener, HistogramBarSeries, Position, ScaleType, Settings, TooltipType, - XYBrushEvent, - XYChartElementEvent, } from '@elastic/charts'; -import { IUiSettingsClient } from '@kbn/core/public'; +import type { IUiSettingsClient } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { CurrentTime, @@ -42,16 +45,12 @@ import { } from '@kbn/charts-plugin/public'; import { LEGACY_TIME_AXIS, MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; import { css } from '@emotion/react'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; -import { DataCharts$, DataChartsMessage } from '../../hooks/use_saved_search'; -import { FetchStatus } from '../../../types'; -import { useDataState } from '../../hooks/use_data_state'; -import { GetStateReturn } from '../../services/discover_state'; +import type { UnifiedHistogramChartContext, UnifiedHistogramServices } from '../types'; -export interface DiscoverHistogramProps { - savedSearchData$: DataCharts$; +export interface HistogramProps { + services: UnifiedHistogramServices; + chart: UnifiedHistogramChartContext; timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; - stateContainer: GetStateReturn; } function getTimezone(uiSettings: IUiSettingsClient) { @@ -64,19 +63,14 @@ function getTimezone(uiSettings: IUiSettingsClient) { } } -export function DiscoverHistogram({ - savedSearchData$, +export function Histogram({ + services: { data, theme, uiSettings, fieldFormats }, + chart: { status, timeInterval, bucketInterval, data: chartData, error }, timefilterUpdateHandler, - stateContainer, -}: DiscoverHistogramProps) { - const { data, theme, uiSettings, fieldFormats } = useDiscoverServices(); +}: HistogramProps) { const chartTheme = theme.useChartsTheme(); const chartBaseTheme = theme.useChartsBaseTheme(); - - const dataState: DataChartsMessage = useDataState(savedSearchData$); - const timeZone = getTimezone(uiSettings); - const { chartData, bucketInterval, fetchStatus, error } = dataState; const onBrushEnd = useCallback( ({ x }: XYBrushEvent) => { @@ -105,7 +99,6 @@ export function DiscoverHistogram({ ); const { timefilter } = data.query.timefilter; - const { from, to } = timefilter.getAbsoluteTime(); const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]); @@ -127,12 +120,12 @@ export function DiscoverHistogram({ from: dateMath.parse(from), to: dateMath.parse(to, { roundUp: true }), }; - const intervalText = i18n.translate('discover.histogramTimeRangeIntervalDescription', { + const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', { defaultMessage: '(interval: {value})', values: { value: `${ - stateContainer.appStateContainer.getState().interval === 'auto' - ? `${i18n.translate('discover.histogramTimeRangeIntervalAuto', { + timeInterval === 'auto' + ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', { defaultMessage: 'Auto', })} - ` : '' @@ -140,39 +133,71 @@ export function DiscoverHistogram({ }, }); return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`; - }, [from, to, toMoment, bucketInterval, stateContainer]); + }, [from, to, timeInterval, bucketInterval?.description, toMoment]); + + const { euiTheme } = useEuiTheme(); + const chartCss = css` + flex-grow: 1; + padding: 0 ${euiTheme.size.s} ${euiTheme.size.s} ${euiTheme.size.s}; + `; + + if (!chartData && status === 'loading') { + const chartLoadingCss = css` + display: flex; + flex-direction: column; + justify-content: center; + flex: 1 0 100%; + text-align: center; + height: 100%; + width: 100%; + `; - if (!chartData && fetchStatus === FetchStatus.LOADING) { return ( -
-
+
+
- +
); } - if (fetchStatus === FetchStatus.ERROR && error) { + if (status === 'error' && error) { + const chartErrorContainerCss = css` + padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; + `; + const chartErrorIconCss = css` + padding-top: 0.5 * ${euiTheme.size.xs}; + `; + const chartErrorCss = css` + margin-left: ${euiTheme.size.xs} !important; + `; + const chartErrorTextCss = css` + margin-top: ${euiTheme.size.s}; + `; + return ( -
+
- + - + - + {error.message}
@@ -227,28 +252,31 @@ export function DiscoverHistogram({ const useLegacyTimeAxis = uiSettings.get(LEGACY_TIME_AXIS, false); - const toolTipTitle = i18n.translate('discover.timeIntervalWithValueWarning', { + const toolTipTitle = i18n.translate('unifiedHistogram.timeIntervalWithValueWarning', { defaultMessage: 'Warning', }); - const toolTipContent = i18n.translate('discover.bucketIntervalTooltip', { + const toolTipContent = i18n.translate('unifiedHistogram.bucketIntervalTooltip', { defaultMessage: 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', values: { bucketsDescription: bucketInterval!.scale && bucketInterval!.scale > 1 - ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { + ? i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText', { defaultMessage: 'buckets that are too large', }) - : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { + : i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText', { defaultMessage: 'too many buckets', }), bucketIntervalDescription: bucketInterval?.description, }, }); + const timeRangeCss = css` + padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; + `; let timeRange = ( - + {timeRangeText} ); @@ -274,7 +302,7 @@ export function DiscoverHistogram({ return ( -
+
xAxisFormatter.convert(value)} /> { const { result } = renderHook(() => { return useChartPanels({ toggleHideChart: jest.fn(), - onChangeInterval: jest.fn(), + onTimeIntervalChange: jest.fn(), closePopover: jest.fn(), onResetChartHeight: jest.fn(), - hideChart: true, - interval: 'auto', + chart: { + status: 'complete', + hidden: true, + timeInterval: 'auto', + }, }); }); const panels: EuiContextMenuPanelDescriptor[] = result.current; @@ -33,11 +35,14 @@ describe('test useChartPanels', () => { const { result } = renderHook(() => { return useChartPanels({ toggleHideChart: jest.fn(), - onChangeInterval: jest.fn(), + onTimeIntervalChange: jest.fn(), closePopover: jest.fn(), onResetChartHeight: jest.fn(), - hideChart: false, - interval: 'auto', + chart: { + status: 'complete', + hidden: false, + timeInterval: 'auto', + }, }); }); const panels: EuiContextMenuPanelDescriptor[] = result.current; @@ -51,10 +56,13 @@ describe('test useChartPanels', () => { const { result } = renderHook(() => { return useChartPanels({ toggleHideChart: jest.fn(), - onChangeInterval: jest.fn(), + onTimeIntervalChange: jest.fn(), closePopover: jest.fn(), - hideChart: false, - interval: 'auto', + chart: { + status: 'complete', + hidden: false, + timeInterval: 'auto', + }, }); }); const panel0: EuiContextMenuPanelDescriptor = result.current[0]; @@ -66,11 +74,14 @@ describe('test useChartPanels', () => { const { result } = renderHook(() => { return useChartPanels({ toggleHideChart: jest.fn(), - onChangeInterval: jest.fn(), + onTimeIntervalChange: jest.fn(), closePopover: jest.fn(), onResetChartHeight, - hideChart: false, - interval: 'auto', + chart: { + status: 'complete', + hidden: false, + timeInterval: 'auto', + }, }); }); const panel0: EuiContextMenuPanelDescriptor = result.current[0]; diff --git a/src/plugins/discover/public/application/main/components/chart/use_chart_panels.ts b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts similarity index 65% rename from src/plugins/discover/public/application/main/components/chart/use_chart_panels.ts rename to src/plugins/unified_histogram/public/chart/use_chart_panels.ts index f01c72aaee997..dd6f162b352f6 100644 --- a/src/plugins/discover/public/application/main/components/chart/use_chart_panels.ts +++ b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts @@ -5,29 +5,35 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import { i18n } from '@kbn/i18n'; import type { EuiContextMenuPanelItemDescriptor, EuiContextMenuPanelDescriptor, } from '@elastic/eui'; import { search } from '@kbn/data-plugin/public'; +import type { UnifiedHistogramChartContext } from '../types'; export function useChartPanels({ + chart, toggleHideChart, - onChangeInterval, + onTimeIntervalChange, closePopover, onResetChartHeight, - hideChart, - interval, }: { + chart?: UnifiedHistogramChartContext; toggleHideChart: () => void; - onChangeInterval: (value: string) => void; + onTimeIntervalChange: (timeInterval: string) => void; closePopover: () => void; onResetChartHeight?: () => void; - hideChart?: boolean; - interval?: string; }) { - const selectedOptionIdx = search.aggs.intervalOptions.findIndex((opt) => opt.val === interval); + if (!chart) { + return []; + } + + const selectedOptionIdx = search.aggs.intervalOptions.findIndex( + (opt) => opt.val === chart.timeInterval + ); const intervalDisplay = selectedOptionIdx > -1 ? search.aggs.intervalOptions[selectedOptionIdx].display @@ -35,25 +41,25 @@ export function useChartPanels({ const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [ { - name: !hideChart - ? i18n.translate('discover.hideChart', { + name: !chart.hidden + ? i18n.translate('unifiedHistogram.hideChart', { defaultMessage: 'Hide chart', }) - : i18n.translate('discover.showChart', { + : i18n.translate('unifiedHistogram.showChart', { defaultMessage: 'Show chart', }), - icon: !hideChart ? 'eyeClosed' : 'eye', + icon: !chart.hidden ? 'eyeClosed' : 'eye', onClick: () => { toggleHideChart(); closePopover(); }, - 'data-test-subj': 'discoverChartToggle', + 'data-test-subj': 'unifiedHistogramChartToggle', }, ]; - if (!hideChart) { + if (!chart.hidden) { if (onResetChartHeight) { mainPanelItems.push({ - name: i18n.translate('discover.resetChartHeight', { + name: i18n.translate('unifiedHistogram.resetChartHeight', { defaultMessage: 'Reset to default height', }), icon: 'refresh', @@ -61,36 +67,36 @@ export function useChartPanels({ onResetChartHeight(); closePopover(); }, - 'data-test-subj': 'discoverChartResetHeight', + 'data-test-subj': 'unifiedHistogramChartResetHeight', }); } mainPanelItems.push({ - name: i18n.translate('discover.timeIntervalWithValue', { + name: i18n.translate('unifiedHistogram.timeIntervalWithValue', { defaultMessage: 'Time interval: {timeInterval}', values: { timeInterval: intervalDisplay, }, }), panel: 1, - 'data-test-subj': 'discoverTimeIntervalPanel', + 'data-test-subj': 'unifiedHistogramTimeIntervalPanel', }); } const panels: EuiContextMenuPanelDescriptor[] = [ { id: 0, - title: i18n.translate('discover.chartOptions', { + title: i18n.translate('unifiedHistogram.chartOptions', { defaultMessage: 'Chart options', }), items: mainPanelItems, }, ]; - if (!hideChart) { + if (!chart.hidden) { panels.push({ id: 1, initialFocusedItemIndex: selectedOptionIdx > -1 ? selectedOptionIdx : 0, - title: i18n.translate('discover.timeIntervals', { + title: i18n.translate('unifiedHistogram.timeIntervals', { defaultMessage: 'Time intervals', }), items: search.aggs.intervalOptions @@ -99,13 +105,13 @@ export function useChartPanels({ return { name: display, label: display, - icon: val === interval ? 'check' : 'empty', + icon: val === chart.timeInterval ? 'check' : 'empty', onClick: () => { - onChangeInterval(val); + onTimeIntervalChange(val); closePopover(); }, - 'data-test-subj': `discoverTimeInterval-${display}`, - className: val === interval ? 'discoverIntervalSelected' : '', + 'data-test-subj': `unifiedHistogramTimeInterval-${display}`, + className: val === chart.timeInterval ? 'unifiedHistogramIntervalSelected' : '', }; }), }); diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx new file mode 100644 index 0000000000000..d094fef953af8 --- /dev/null +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import type { ReactWrapper } from 'enzyme'; +import type { HitsCounterProps } from './hits_counter'; +import { HitsCounter } from './hits_counter'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +describe('hits counter', function () { + let props: HitsCounterProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + hits: { + status: 'complete', + total: 2, + }, + }; + }); + + it('expect to render the number of hits', function () { + component = mountWithIntl(); + const hits = findTestSubject(component, 'unifiedHistogramQueryHits'); + expect(hits.text()).toBe('2'); + }); + + it('expect to render 1,899 hits if 1899 hits given', function () { + component = mountWithIntl( + + ); + const hits = findTestSubject(component, 'unifiedHistogramQueryHits'); + expect(hits.text()).toBe('1,899'); + }); + + it('should render the element passed to the append prop', () => { + const appendHitsCounter =
appendHitsCounter
; + component = mountWithIntl(); + expect(findTestSubject(component, 'appendHitsCounter').length).toBe(1); + }); + + it('should render a EuiLoadingSpinner when status is partial', () => { + component = mountWithIntl(); + expect(component.find(EuiLoadingSpinner).length).toBe(1); + }); + + it('should render unifiedHistogramQueryHitsPartial when status is partial', () => { + component = mountWithIntl(); + expect(component.find('[data-test-subj="unifiedHistogramQueryHitsPartial"]').length).toBe(1); + }); + + it('should render unifiedHistogramQueryHits when status is complete', () => { + component = mountWithIntl(); + expect(component.find('[data-test-subj="unifiedHistogramQueryHits"]').length).toBe(1); + }); +}); diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx new file mode 100644 index 0000000000000..39df40650557c --- /dev/null +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ReactElement } from 'react'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import type { UnifiedHistogramHitsContext } from '../types'; + +export interface HitsCounterProps { + hits: UnifiedHistogramHitsContext; + append?: ReactElement; +} + +export function HitsCounter({ hits, append }: HitsCounterProps) { + if (!hits.total && hits.status === 'loading') { + return null; + } + + const formattedHits = ( + + + + ); + + const hitsCounterCss = css` + flex-grow: 0; + `; + + return ( + + + + {hits.status === 'partial' && ( + + )} + {hits.status !== 'partial' && ( + + )} + + + {hits.status === 'partial' && ( + + + + )} + {append} + + ); +} diff --git a/src/plugins/discover/public/application/main/components/hits_counter/index.ts b/src/plugins/unified_histogram/public/hits_counter/index.ts similarity index 100% rename from src/plugins/discover/public/application/main/components/hits_counter/index.ts rename to src/plugins/unified_histogram/public/hits_counter/index.ts diff --git a/src/plugins/unified_histogram/public/index.ts b/src/plugins/unified_histogram/public/index.ts new file mode 100644 index 0000000000000..4d3ab5d097831 --- /dev/null +++ b/src/plugins/unified_histogram/public/index.ts @@ -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 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 { UnifiedHistogramPublicPlugin } from './plugin'; + +export type { UnifiedHistogramLayoutProps } from './layout'; +export { UnifiedHistogramLayout } from './layout'; +export { getChartAggConfigs, buildChartData } from './chart'; +export type { + UnifiedHistogramServices, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramChartContext, + UnifiedHistogramChartData, + UnifiedHistogramBucketInterval, +} from './types'; + +export const plugin = () => new UnifiedHistogramPublicPlugin(); diff --git a/src/plugins/unified_histogram/public/layout/index.tsx b/src/plugins/unified_histogram/public/layout/index.tsx new file mode 100644 index 0000000000000..a729bdff0871c --- /dev/null +++ b/src/plugins/unified_histogram/public/layout/index.tsx @@ -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 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 { EuiDelayRender, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; +import { withSuspense } from '@kbn/shared-ux-utility'; +import React, { lazy } from 'react'; + +export type { UnifiedHistogramLayoutProps } from './layout'; + +const LazyUnifiedHistogramLayout = lazy(() => import('./layout')); + +/** + * A resizable layout component with two panels that renders a histogram with a hits + * counter in the top panel, and a main display (data table, etc.) in the bottom panel. + * If all context props are left undefined, the layout will render in a single panel + * mode including only the main display. + */ +export const UnifiedHistogramLayout = withSuspense( + LazyUnifiedHistogramLayout, + + + + + +); diff --git a/src/plugins/unified_histogram/public/layout/layout.test.tsx b/src/plugins/unified_histogram/public/layout/layout.test.tsx new file mode 100644 index 0000000000000..73b97c8f64def --- /dev/null +++ b/src/plugins/unified_histogram/public/layout/layout.test.tsx @@ -0,0 +1,192 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import type { ReactWrapper } from 'enzyme'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Chart } from '../chart'; +import { Panels, PANELS_MODE } from '../panels'; +import type { UnifiedHistogramChartContext, UnifiedHistogramHitsContext } from '../types'; +import { unifiedHistogramServicesMock } from '../__mocks__/services'; +import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout'; + +let mockBreakpoint = 'l'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + useIsWithinBreakpoints: (breakpoints: string[]) => { + return breakpoints.includes(mockBreakpoint); + }, + }; +}); + +describe('Layout', () => { + const createHits = (): UnifiedHistogramHitsContext => ({ + status: 'complete', + total: 10, + }); + + const createChart = (): UnifiedHistogramChartContext => ({ + status: 'complete', + hidden: false, + timeInterval: 'auto', + bucketInterval: { + scaled: true, + description: 'test', + scale: 2, + }, + }); + + const mountComponent = async ({ + services = unifiedHistogramServicesMock, + hits = createHits(), + chart = createChart(), + resizeRef = { current: null }, + ...rest + }: Partial> & { + hits?: UnifiedHistogramHitsContext | null; + chart?: UnifiedHistogramChartContext | null; + } = {}) => { + services.data.query.timefilter.timefilter.getAbsoluteTime = () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + const component = mountWithIntl( + + ); + + return component; + }; + + const setBreakpoint = (component: ReactWrapper, breakpoint: string) => { + mockBreakpoint = breakpoint; + component.setProps({}).update(); + }; + + beforeEach(() => { + mockBreakpoint = 'l'; + }); + + describe('PANELS_MODE', () => { + it('should set the panels mode to PANELS_MODE.RESIZABLE when viewing on medium screens and above', async () => { + const component = await mountComponent(); + setBreakpoint(component, 'm'); + expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.RESIZABLE); + }); + + it('should set the panels mode to PANELS_MODE.FIXED when viewing on small screens and below', async () => { + const component = await mountComponent(); + setBreakpoint(component, 's'); + expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.FIXED); + }); + + it('should set the panels mode to PANELS_MODE.FIXED if chart.hidden is true', async () => { + const component = await mountComponent({ + chart: { + ...createChart(), + hidden: true, + }, + }); + expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.FIXED); + }); + + it('should set the panels mode to PANELS_MODE.FIXED if chart is undefined', async () => { + const component = await mountComponent({ chart: null }); + expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.FIXED); + }); + + it('should set the panels mode to PANELS_MODE.SINGLE if chart and hits are undefined', async () => { + const component = await mountComponent({ chart: null, hits: null }); + expect(component.find(Panels).prop('mode')).toBe(PANELS_MODE.SINGLE); + }); + + it('should set a fixed height for Chart when panels mode is PANELS_MODE.FIXED and chart.hidden is false', async () => { + const component = await mountComponent(); + setBreakpoint(component, 's'); + const expectedHeight = component.find(Panels).prop('topPanelHeight'); + expect(component.find(Chart).childAt(0).getDOMNode()).toHaveStyle({ + height: `${expectedHeight}px`, + }); + }); + + it('should not set a fixed height for Chart when panels mode is PANELS_MODE.FIXED and chart.hidden is true', async () => { + const component = await mountComponent({ chart: { ...createChart(), hidden: true } }); + setBreakpoint(component, 's'); + const expectedHeight = component.find(Panels).prop('topPanelHeight'); + expect(component.find(Chart).childAt(0).getDOMNode()).not.toHaveStyle({ + height: `${expectedHeight}px`, + }); + }); + + it('should not set a fixed height for Chart when panels mode is PANELS_MODE.FIXED and chart is undefined', async () => { + const component = await mountComponent({ chart: null }); + setBreakpoint(component, 's'); + const expectedHeight = component.find(Panels).prop('topPanelHeight'); + expect(component.find(Chart).childAt(0).getDOMNode()).not.toHaveStyle({ + height: `${expectedHeight}px`, + }); + }); + + it('should pass undefined for onResetChartHeight to Chart when panels mode is PANELS_MODE.FIXED', async () => { + const component = await mountComponent({ topPanelHeight: 123 }); + expect(component.find(Chart).prop('onResetChartHeight')).toBeDefined(); + setBreakpoint(component, 's'); + expect(component.find(Chart).prop('onResetChartHeight')).toBeUndefined(); + }); + }); + + describe('topPanelHeight', () => { + it('should pass a default topPanelHeight to Panels when the topPanelHeight prop is undefined', async () => { + const component = await mountComponent({ topPanelHeight: undefined }); + expect(component.find(Panels).prop('topPanelHeight')).toBeGreaterThan(0); + }); + + it('should reset the topPanelHeight to the default when onResetChartHeight is called on Chart', async () => { + const component: ReactWrapper = await mountComponent({ + onTopPanelHeightChange: jest.fn((topPanelHeight) => { + component.setProps({ topPanelHeight }); + }), + }); + const defaultTopPanelHeight = component.find(Panels).prop('topPanelHeight'); + const newTopPanelHeight = 123; + expect(component.find(Panels).prop('topPanelHeight')).not.toBe(newTopPanelHeight); + act(() => { + component.find(Panels).prop('onTopPanelHeightChange')!(newTopPanelHeight); + }); + expect(component.find(Panels).prop('topPanelHeight')).toBe(newTopPanelHeight); + act(() => { + component.find(Chart).prop('onResetChartHeight')!(); + }); + expect(component.find(Panels).prop('topPanelHeight')).toBe(defaultTopPanelHeight); + }); + + it('should pass undefined for onResetChartHeight to Chart when the chart is the default height', async () => { + const component = await mountComponent({ + topPanelHeight: 123, + onTopPanelHeightChange: jest.fn((topPanelHeight) => { + component.setProps({ topPanelHeight }); + }), + }); + expect(component.find(Chart).prop('onResetChartHeight')).toBeDefined(); + act(() => { + component.find(Chart).prop('onResetChartHeight')!(); + }); + component.update(); + expect(component.find(Chart).prop('onResetChartHeight')).toBeUndefined(); + }); + }); +}); diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx new file mode 100644 index 0000000000000..229d8a922e465 --- /dev/null +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -0,0 +1,149 @@ +/* + * 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 { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui'; +import type { PropsWithChildren, ReactElement, RefObject } from 'react'; +import React, { useMemo } from 'react'; +import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; +import { css } from '@emotion/css'; +import { Chart } from '../chart'; +import { Panels, PANELS_MODE } from '../panels'; +import type { + UnifiedHistogramChartContext, + UnifiedHistogramServices, + UnifiedHistogramHitsContext, +} from '../types'; + +export interface UnifiedHistogramLayoutProps extends PropsWithChildren { + className?: string; + services: UnifiedHistogramServices; + /** + * Context object for the hits count -- leave undefined to hide the hits count + */ + hits?: UnifiedHistogramHitsContext; + /** + * Context object for the chart -- leave undefined to hide the chart + */ + chart?: UnifiedHistogramChartContext; + /** + * Ref to the element wrapping the layout which will be used for resize calculations + */ + resizeRef: RefObject; + /** + * Current top panel height -- leave undefined to use the default + */ + topPanelHeight?: number; + /** + * Append a custom element to the right of the hits count + */ + appendHitsCounter?: ReactElement; + /** + * Callback to update the topPanelHeight prop when a resize is triggered + */ + onTopPanelHeightChange?: (topPanelHeight: number | undefined) => void; + /** + * Callback to invoke when the user clicks the edit visualization button -- leave undefined to hide the button + */ + onEditVisualization?: () => void; + /** + * Callback to hide or show the chart -- should set {@link UnifiedHistogramChartContext.hidden} to chartHidden + */ + onChartHiddenChange?: (chartHidden: boolean) => void; + /** + * Callback to update the time interval -- should set {@link UnifiedHistogramChartContext.timeInterval} to timeInterval + */ + onTimeIntervalChange?: (timeInterval: string) => void; +} + +export const UnifiedHistogramLayout = ({ + className, + services, + hits, + chart, + resizeRef, + topPanelHeight, + appendHitsCounter, + onTopPanelHeightChange, + onEditVisualization, + onChartHiddenChange, + onTimeIntervalChange, + children, +}: UnifiedHistogramLayoutProps) => { + const topPanelNode = useMemo( + () => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }), + [] + ); + + const mainPanelNode = useMemo( + () => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }), + [] + ); + + const isMobile = useIsWithinBreakpoints(['xs', 's']); + const showFixedPanels = isMobile || !chart || chart.hidden; + const { euiTheme } = useEuiTheme(); + const defaultTopPanelHeight = euiTheme.base * 12; + const minTopPanelHeight = euiTheme.base * 8; + const minMainPanelHeight = euiTheme.base * 10; + + const chartClassName = + isMobile && chart && !chart.hidden + ? css` + height: ${defaultTopPanelHeight}px; + ` + : 'eui-fullHeight'; + + const panelsMode = + chart || hits + ? showFixedPanels + ? PANELS_MODE.FIXED + : PANELS_MODE.RESIZABLE + : PANELS_MODE.SINGLE; + + const currentTopPanelHeight = topPanelHeight ?? defaultTopPanelHeight; + + const onResetChartHeight = useMemo(() => { + return currentTopPanelHeight !== defaultTopPanelHeight && panelsMode === PANELS_MODE.RESIZABLE + ? () => onTopPanelHeightChange?.(undefined) + : undefined; + }, [currentTopPanelHeight, defaultTopPanelHeight, onTopPanelHeightChange, panelsMode]); + + return ( + <> + + : } + onEditVisualization={onEditVisualization} + onResetChartHeight={onResetChartHeight} + onChartHiddenChange={onChartHiddenChange} + onTimeIntervalChange={onTimeIntervalChange} + /> + + {children} + } + mainPanel={} + onTopPanelHeightChange={onTopPanelHeightChange} + /> + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default UnifiedHistogramLayout; diff --git a/src/plugins/discover/public/application/main/components/chart/index.ts b/src/plugins/unified_histogram/public/panels/index.ts similarity index 87% rename from src/plugins/discover/public/application/main/components/chart/index.ts rename to src/plugins/unified_histogram/public/panels/index.ts index d5d5a85d1d0f2..ba3e73cb5a35a 100644 --- a/src/plugins/discover/public/application/main/components/chart/index.ts +++ b/src/plugins/unified_histogram/public/panels/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { DiscoverChart } from './discover_chart'; +export { Panels, PANELS_MODE } from './panels'; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels.test.tsx b/src/plugins/unified_histogram/public/panels/panels.test.tsx similarity index 54% rename from src/plugins/discover/public/application/main/components/layout/discover_panels.test.tsx rename to src/plugins/unified_histogram/public/panels/panels.test.tsx index c136675494fb3..e0e2de24b4083 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_panels.test.tsx +++ b/src/plugins/unified_histogram/public/panels/panels.test.tsx @@ -7,14 +7,15 @@ */ import { mount } from 'enzyme'; -import React, { ReactElement, RefObject } from 'react'; -import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels'; -import { DiscoverPanelsResizable } from './discover_panels_resizable'; -import { DiscoverPanelsFixed } from './discover_panels_fixed'; +import type { ReactElement, RefObject } from 'react'; +import React from 'react'; +import { Panels, PANELS_MODE } from './panels'; +import { PanelsResizable } from './panels_resizable'; +import { PanelsFixed } from './panels_fixed'; -describe('Discover panels component', () => { +describe('Panels component', () => { const mountComponent = ({ - mode = DISCOVER_PANELS_MODE.RESIZABLE, + mode = PANELS_MODE.RESIZABLE, resizeRef = { current: null }, initialTopPanelHeight = 200, minTopPanelHeight = 100, @@ -22,7 +23,7 @@ describe('Discover panels component', () => { topPanel = <>, mainPanel = <>, }: { - mode?: DISCOVER_PANELS_MODE; + mode?: PANELS_MODE; resizeRef?: RefObject; initialTopPanelHeight?: number; minTopPanelHeight?: number; @@ -31,7 +32,7 @@ describe('Discover panels component', () => { topPanel?: ReactElement; }) => { return mount( - { ); }; - it('should show DiscoverPanelsFixed when mode is DISCOVER_PANELS_MODE.SINGLE', () => { + it('should show PanelsFixed when mode is PANELS_MODE.SINGLE', () => { const topPanel =
; const mainPanel =
; - const component = mountComponent({ mode: DISCOVER_PANELS_MODE.SINGLE, topPanel, mainPanel }); - expect(component.find(DiscoverPanelsFixed).exists()).toBe(true); - expect(component.find(DiscoverPanelsResizable).exists()).toBe(false); + const component = mountComponent({ mode: PANELS_MODE.SINGLE, topPanel, mainPanel }); + expect(component.find(PanelsFixed).exists()).toBe(true); + expect(component.find(PanelsResizable).exists()).toBe(false); expect(component.contains(topPanel)).toBe(false); expect(component.contains(mainPanel)).toBe(true); }); - it('should show DiscoverPanelsFixed when mode is DISCOVER_PANELS_MODE.FIXED', () => { + it('should show PanelsFixed when mode is PANELS_MODE.FIXED', () => { const topPanel =
; const mainPanel =
; - const component = mountComponent({ mode: DISCOVER_PANELS_MODE.FIXED, topPanel, mainPanel }); - expect(component.find(DiscoverPanelsFixed).exists()).toBe(true); - expect(component.find(DiscoverPanelsResizable).exists()).toBe(false); + const component = mountComponent({ mode: PANELS_MODE.FIXED, topPanel, mainPanel }); + expect(component.find(PanelsFixed).exists()).toBe(true); + expect(component.find(PanelsResizable).exists()).toBe(false); expect(component.contains(topPanel)).toBe(true); expect(component.contains(mainPanel)).toBe(true); }); - it('should show DiscoverPanelsResizable when mode is DISCOVER_PANELS_MODE.RESIZABLE', () => { + it('should show PanelsResizable when mode is PANELS_MODE.RESIZABLE', () => { const topPanel =
; const mainPanel =
; - const component = mountComponent({ mode: DISCOVER_PANELS_MODE.RESIZABLE, topPanel, mainPanel }); - expect(component.find(DiscoverPanelsFixed).exists()).toBe(false); - expect(component.find(DiscoverPanelsResizable).exists()).toBe(true); + const component = mountComponent({ mode: PANELS_MODE.RESIZABLE, topPanel, mainPanel }); + expect(component.find(PanelsFixed).exists()).toBe(false); + expect(component.find(PanelsResizable).exists()).toBe(true); expect(component.contains(topPanel)).toBe(true); expect(component.contains(mainPanel)).toBe(true); }); - it('should pass true for hideTopPanel when mode is DISCOVER_PANELS_MODE.SINGLE', () => { + it('should pass true for hideTopPanel when mode is PANELS_MODE.SINGLE', () => { const topPanel =
; const mainPanel =
; - const component = mountComponent({ mode: DISCOVER_PANELS_MODE.SINGLE, topPanel, mainPanel }); - expect(component.find(DiscoverPanelsFixed).prop('hideTopPanel')).toBe(true); + const component = mountComponent({ mode: PANELS_MODE.SINGLE, topPanel, mainPanel }); + expect(component.find(PanelsFixed).prop('hideTopPanel')).toBe(true); expect(component.contains(topPanel)).toBe(false); expect(component.contains(mainPanel)).toBe(true); }); - it('should pass false for hideTopPanel when mode is DISCOVER_PANELS_MODE.FIXED', () => { + it('should pass false for hideTopPanel when mode is PANELS_MODE.FIXED', () => { const topPanel =
; const mainPanel =
; - const component = mountComponent({ mode: DISCOVER_PANELS_MODE.FIXED, topPanel, mainPanel }); - expect(component.find(DiscoverPanelsFixed).prop('hideTopPanel')).toBe(false); + const component = mountComponent({ mode: PANELS_MODE.FIXED, topPanel, mainPanel }); + expect(component.find(PanelsFixed).prop('hideTopPanel')).toBe(false); expect(component.contains(topPanel)).toBe(true); expect(component.contains(mainPanel)).toBe(true); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels.tsx b/src/plugins/unified_histogram/public/panels/panels.tsx similarity index 64% rename from src/plugins/discover/public/application/main/components/layout/discover_panels.tsx rename to src/plugins/unified_histogram/public/panels/panels.tsx index b79d9fb96aaeb..609219ab28666 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_panels.tsx +++ b/src/plugins/unified_histogram/public/panels/panels.tsx @@ -6,31 +6,32 @@ * Side Public License, v 1. */ -import React, { ReactElement, RefObject } from 'react'; -import { DiscoverPanelsResizable } from './discover_panels_resizable'; -import { DiscoverPanelsFixed } from './discover_panels_fixed'; +import type { ReactElement, RefObject } from 'react'; +import React from 'react'; +import { PanelsResizable } from './panels_resizable'; +import { PanelsFixed } from './panels_fixed'; -export enum DISCOVER_PANELS_MODE { +export enum PANELS_MODE { SINGLE = 'single', FIXED = 'fixed', RESIZABLE = 'resizable', } -export interface DiscoverPanelsProps { +export interface PanelsProps { className?: string; - mode: DISCOVER_PANELS_MODE; + mode: PANELS_MODE; resizeRef: RefObject; topPanelHeight: number; minTopPanelHeight: number; minMainPanelHeight: number; topPanel: ReactElement; mainPanel: ReactElement; - onTopPanelHeightChange: (height: number) => void; + onTopPanelHeightChange?: (topPanelHeight: number) => void; } -const fixedModes = [DISCOVER_PANELS_MODE.SINGLE, DISCOVER_PANELS_MODE.FIXED]; +const fixedModes = [PANELS_MODE.SINGLE, PANELS_MODE.FIXED]; -export const DiscoverPanels = ({ +export const Panels = ({ className, mode, resizeRef, @@ -40,13 +41,13 @@ export const DiscoverPanels = ({ topPanel, mainPanel, onTopPanelHeightChange, -}: DiscoverPanelsProps) => { +}: PanelsProps) => { const panelsProps = { className, topPanel, mainPanel }; return fixedModes.includes(mode) ? ( - + ) : ( - { +describe('Panels fixed', () => { const mountComponent = ({ hideTopPanel = false, topPanel = <>, @@ -21,7 +22,7 @@ describe('Discover panels fixed', () => { mainPanel: ReactElement; }) => { return mount( - + ); }; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.tsx b/src/plugins/unified_histogram/public/panels/panels_fixed.tsx similarity index 93% rename from src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.tsx rename to src/plugins/unified_histogram/public/panels/panels_fixed.tsx index 1db99e61fb8c5..1b7d8bf9bf68e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_panels_fixed.tsx +++ b/src/plugins/unified_histogram/public/panels/panels_fixed.tsx @@ -8,9 +8,10 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { ReactElement } from 'react'; +import type { ReactElement } from 'react'; +import React from 'react'; -export const DiscoverPanelsFixed = ({ +export const PanelsFixed = ({ className, hideTopPanel, topPanel, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.test.tsx b/src/plugins/unified_histogram/public/panels/panels_resizable.test.tsx similarity index 83% rename from src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.test.tsx rename to src/plugins/unified_histogram/public/panels/panels_resizable.test.tsx index c919504091c21..a21e137e87ed7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.test.tsx +++ b/src/plugins/unified_histogram/public/panels/panels_resizable.test.tsx @@ -6,9 +6,11 @@ * Side Public License, v 1. */ -import { mount, ReactWrapper } from 'enzyme'; -import React, { ReactElement, RefObject } from 'react'; -import { DiscoverPanelsResizable } from './discover_panels_resizable'; +import type { ReactWrapper } from 'enzyme'; +import { mount } from 'enzyme'; +import type { ReactElement, RefObject } from 'react'; +import React from 'react'; +import { PanelsResizable } from './panels_resizable'; import { act } from 'react-dom/test-utils'; const containerHeight = 1000; @@ -23,7 +25,7 @@ jest.mock('@elastic/eui', () => ({ import * as eui from '@elastic/eui'; import { waitFor } from '@testing-library/dom'; -describe('Discover panels resizable', () => { +describe('Panels resizable', () => { const mountComponent = ({ className = '', resizeRef = { current: null }, @@ -43,10 +45,10 @@ describe('Discover panels resizable', () => { topPanel?: ReactElement; mainPanel?: ReactElement; attachTo?: HTMLElement; - onTopPanelHeightChange?: (height: number) => void; + onTopPanelHeightChange?: (topPanelHeight: number) => void; }) => { return mount( - { topPanelHeight: number ) => { const topPanelSize = (topPanelHeight / currentContainerHeight) * 100; - expect(component.find('[data-test-subj="dscResizablePanelTop"]').at(0).prop('size')).toBe( - topPanelSize - ); - expect(component.find('[data-test-subj="dscResizablePanelMain"]').at(0).prop('size')).toBe( - 100 - topPanelSize - ); + expect( + component.find('[data-test-subj="unifiedHistogramResizablePanelTop"]').at(0).prop('size') + ).toBe(topPanelSize); + expect( + component.find('[data-test-subj="unifiedHistogramResizablePanelMain"]').at(0).prop('size') + ).toBe(100 - topPanelSize); }; const forceRender = (component: ReactWrapper) => { @@ -105,7 +107,7 @@ describe('Discover panels resizable', () => { expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); const newTopPanelSize = 30; const onPanelSizeChange = component - .find('[data-test-subj="dscResizableContainer"]') + .find('[data-test-subj="unifiedHistogramResizableContainer"]') .at(0) .prop('onPanelWidthChange') as Function; act(() => { @@ -159,12 +161,12 @@ describe('Discover panels resizable', () => { const newContainerHeight = 200; jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 }); forceRender(component); - expect(component.find('[data-test-subj="dscResizablePanelTop"]').at(0).prop('size')).toBe( - (minTopPanelHeight / newContainerHeight) * 100 - ); - expect(component.find('[data-test-subj="dscResizablePanelMain"]').at(0).prop('size')).toBe( - (minMainPanelHeight / newContainerHeight) * 100 - ); + expect( + component.find('[data-test-subj="unifiedHistogramResizablePanelTop"]').at(0).prop('size') + ).toBe((minTopPanelHeight / newContainerHeight) * 100); + expect( + component.find('[data-test-subj="unifiedHistogramResizablePanelMain"]').at(0).prop('size') + ).toBe((minMainPanelHeight / newContainerHeight) * 100); jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 }); forceRender(component); expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight); @@ -174,9 +176,11 @@ describe('Discover panels resizable', () => { const attachTo = document.createElement('div'); document.body.appendChild(attachTo); const component = mountComponent({ attachTo }); - const wrapper = component.find('[data-test-subj="dscResizableContainerWrapper"]'); - const resizeButton = component.find('button[data-test-subj="dsc-resizable-button"]'); - const resizeButtonInner = component.find('[data-test-subj="dscResizableButtonInner"]'); + const wrapper = component.find('[data-test-subj="unifiedHistogramResizableContainerWrapper"]'); + const resizeButton = component.find('button[data-test-subj="unifiedHistogramResizableButton"]'); + const resizeButtonInner = component.find( + '[data-test-subj="unifiedHistogramResizableButtonInner"]' + ); const mouseEvent = { pageX: 0, pageY: 0, diff --git a/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.tsx b/src/plugins/unified_histogram/public/panels/panels_resizable.tsx similarity index 72% rename from src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.tsx rename to src/plugins/unified_histogram/public/panels/panels_resizable.tsx index b65da1eb0bf68..76fecd42d2aed 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_panels_resizable.tsx +++ b/src/plugins/unified_histogram/public/panels/panels_resizable.tsx @@ -6,17 +6,24 @@ * Side Public License, v 1. */ -import { EuiResizableContainer, useGeneratedHtmlId, useResizeObserver } from '@elastic/eui'; +import { + EuiResizableContainer, + useEuiTheme, + useGeneratedHtmlId, + useResizeObserver, +} from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { ReactElement, RefObject, useCallback, useEffect, useState } from 'react'; +import { isEqual, round } from 'lodash'; +import type { ReactElement, RefObject } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; const percentToPixels = (containerHeight: number, percentage: number) => Math.round(containerHeight * (percentage / 100)); const pixelsToPercent = (containerHeight: number, pixels: number) => - +((pixels / containerHeight) * 100).toFixed(4); + (pixels / containerHeight) * 100; -export const DiscoverPanelsResizable = ({ +export const PanelsResizable = ({ className, resizeRef, topPanelHeight, @@ -33,7 +40,7 @@ export const DiscoverPanelsResizable = ({ minMainPanelHeight: number; topPanel: ReactElement; mainPanel: ReactElement; - onTopPanelHeightChange: (height: number) => void; + onTopPanelHeightChange?: (topPanelHeight: number) => void; }) => { const topPanelId = useGeneratedHtmlId({ prefix: 'topPanel' }); const { height: containerHeight } = useResizeObserver(resizeRef.current); @@ -65,23 +72,28 @@ export const DiscoverPanelsResizable = ({ z-index: 2; `; - // Instead of setting the panel sizes directly, we convert the top panel height - // from a percentage of the container height to a pixel value. This will trigger - // the effect below to update the panel sizes. + // We convert the top panel height from a percentage of the container height + // to a pixel value and emit the change to the parent component. We also convert + // the pixel value back to a percentage before updating the panel sizes to avoid + // rounding issues with the isEqual check in the effect below. const onPanelSizeChange = useCallback( ({ [topPanelId]: topPanelSize }: { [key: string]: number }) => { const newTopPanelHeight = percentToPixels(containerHeight, topPanelSize); + const newTopPanelSize = pixelsToPercent(containerHeight, newTopPanelHeight); - if (newTopPanelHeight !== topPanelHeight) { - onTopPanelHeightChange(newTopPanelHeight); - } + setPanelSizes({ + topPanelSize: round(newTopPanelSize, 4), + mainPanelSize: round(100 - newTopPanelSize, 4), + }); + + onTopPanelHeightChange?.(newTopPanelHeight); }, - [containerHeight, onTopPanelHeightChange, topPanelHeight, topPanelId] + [containerHeight, onTopPanelHeightChange, topPanelId] ); // This effect will update the panel sizes based on the top panel height whenever // it or the container height changes. This allows us to keep the height of the - // top panel panel fixed when the window is resized. + // top panel fixed when the window is resized. useEffect(() => { if (!containerHeight) { return; @@ -109,8 +121,17 @@ export const DiscoverPanelsResizable = ({ mainPanelSize = 100 - topPanelSize; } - setPanelSizes({ topPanelSize, mainPanelSize }); - }, [containerHeight, topPanelHeight, minTopPanelHeight, minMainPanelHeight]); + const newPanelSizes = { + topPanelSize: round(topPanelSize, 4), + mainPanelSize: round(mainPanelSize, 4), + }; + + // Skip updating the panel sizes if they haven't changed + // since onPanelSizeChange will also trigger this effect. + if (!isEqual(panelSizes, newPanelSizes)) { + setPanelSizes(newPanelSizes); + } + }, [containerHeight, minMainPanelHeight, minTopPanelHeight, panelSizes, topPanelHeight]); const onResizeEnd = () => { // We don't want the resize button to retain focus after the resize is complete, @@ -126,19 +147,27 @@ export const DiscoverPanelsResizable = ({ disableResizeWithPortalsHack(); }; + const { euiTheme } = useEuiTheme(); + const buttonCss = css` + && { + margin-top: -${euiTheme.size.base}; + margin-bottom: 0; + } + `; + return (
{(EuiResizablePanel, EuiResizableButton) => ( <> @@ -147,26 +176,26 @@ export const DiscoverPanelsResizable = ({ minSize={`${minTopPanelHeight}px`} size={panelSizes.topPanelSize} paddingSize="none" - data-test-subj="dscResizablePanelTop" + data-test-subj="unifiedHistogramResizablePanelTop" > {topPanel} {mainPanel} diff --git a/src/plugins/unified_histogram/public/plugin.ts b/src/plugins/unified_histogram/public/plugin.ts new file mode 100644 index 0000000000000..e1503be5f5b09 --- /dev/null +++ b/src/plugins/unified_histogram/public/plugin.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Plugin } from '@kbn/core/public'; + +export class UnifiedHistogramPublicPlugin implements Plugin<{}, {}, object, {}> { + public setup() { + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts new file mode 100644 index 0000000000000..53f81b0819900 --- /dev/null +++ b/src/plugins/unified_histogram/public/types.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Theme } from '@kbn/charts-plugin/public/plugin'; +import type { IUiSettingsClient } from '@kbn/core/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { Duration, Moment } from 'moment'; +import type { Unit } from '@kbn/datemath'; +import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; + +/** + * The fetch status of a unified histogram request + */ +export type UnifiedHistogramFetchStatus = + | 'uninitialized' + | 'loading' + | 'partial' + | 'complete' + | 'error'; + +/** + * The services required by the unified histogram components + */ +export interface UnifiedHistogramServices { + data: DataPublicPluginStart; + theme: Theme; + uiSettings: IUiSettingsClient; + fieldFormats: FieldFormatsStart; +} + +interface Column { + id: string; + name: string; +} + +interface Row { + [key: string]: number | 'NaN'; +} + +interface Dimension { + accessor: 0 | 1; + format: SerializedFieldFormat<{ pattern: string }>; + label: string; +} + +interface Ordered { + date: true; + interval: Duration; + intervalESUnit: string; + intervalESValue: number; + min: Moment; + max: Moment; +} + +interface HistogramParams { + date: true; + interval: Duration; + intervalESValue: number; + intervalESUnit: Unit; + format: string; + bounds: HistogramParamsBounds; +} + +export interface HistogramParamsBounds { + min: Moment; + max: Moment; +} + +export interface Table { + columns: Column[]; + rows: Row[]; +} + +export interface Dimensions { + x: Dimension & { params: HistogramParams }; + y: Dimension; +} + +/** + * The chartData object returned by {@link buildChartData} that + * should be used to set {@link UnifiedHistogramChartContext.data} + */ +export interface UnifiedHistogramChartData { + values: Array<{ + x: number; + y: number; + }>; + xAxisOrderedValues: number[]; + xAxisFormat: Dimension['format']; + yAxisFormat: Dimension['format']; + xAxisLabel: Column['name']; + yAxisLabel?: Column['name']; + ordered: Ordered; +} + +/** + * The bucketInterval object returned by {@link buildChartData} that + * should be used to set {@link UnifiedHistogramChartContext.bucketInterval} + */ +export interface UnifiedHistogramBucketInterval { + scaled?: boolean; + description?: string; + scale?: number; +} + +/** + * Context object for the hits count + */ +export interface UnifiedHistogramHitsContext { + /** + * The fetch status of the hits count request + */ + status: UnifiedHistogramFetchStatus; + /** + * The total number of hits + */ + total?: number; +} + +/** + * Context object for the chart + */ +export interface UnifiedHistogramChartContext { + /** + * The fetch status of the chart request + */ + status: UnifiedHistogramFetchStatus; + /** + * Controls whether or not the chart is hidden + */ + hidden?: boolean; + /** + * Controls the time interval of the chart + */ + timeInterval?: string; + /** + * The bucketInterval object returned by {@link buildChartData} + */ + bucketInterval?: UnifiedHistogramBucketInterval; + /** + * The chartData object returned by {@link buildChartData} + */ + data?: UnifiedHistogramChartData; + /** + * Error from failed chart request + */ + error?: Error; +} diff --git a/src/plugins/unified_histogram/tsconfig.json b/src/plugins/unified_histogram/tsconfig.json new file mode 100644 index 0000000000000..af8f24161fd31 --- /dev/null +++ b/src/plugins/unified_histogram/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, + { "path": "../saved_search/tsconfig.json" } + ] +} diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 841724347c004..face35f4e6730 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -144,23 +144,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('a11y test for chart options panel', async () => { - await testSubjects.click('discoverChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); await a11y.testAppSnapshot(); }); it('a11y test for data grid with hidden chart', async () => { - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); await a11y.testAppSnapshot(); - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); }); it('a11y test for time interval panel', async () => { - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverTimeIntervalPanel'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramTimeIntervalPanel'); await a11y.testAppSnapshot(); await testSubjects.click('contextMenuPanelTitleButton'); - await testSubjects.click('discoverChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); }); it('a11y test for data grid sort panel', async () => { diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index 1ac0ad6fe013f..d6035d0a28a6e 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -347,9 +347,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('resizable layout panels', () => { it('should allow resizing the layout panels', async () => { const resizeDistance = 100; - const topPanel = await testSubjects.find('dscResizablePanelTop'); - const mainPanel = await testSubjects.find('dscResizablePanelMain'); - const resizeButton = await testSubjects.find('dsc-resizable-button'); + const topPanel = await testSubjects.find('unifiedHistogramResizablePanelTop'); + const mainPanel = await testSubjects.find('unifiedHistogramResizablePanelMain'); + const resizeButton = await testSubjects.find('unifiedHistogramResizableButton'); const topPanelSize = (await topPanel.getPosition()).height; const mainPanelSize = (await mainPanel.getPosition()).height; await browser.dragAndDrop( diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts index 5257b5edbe235..12effb75cb7f3 100644 --- a/test/functional/apps/discover/group1/_discover_histogram.ts +++ b/test/functional/apps/discover/group1/_discover_histogram.ts @@ -81,16 +81,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await prepareTest({ from, to }); let canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); // histogram is hidden, when reloading the page it should remain hidden await browser.refresh(); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); await PageObjects.header.waitUntilLoadingHasFinished(); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); @@ -102,8 +102,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await prepareTest({ from, to }); // close chart for saved search - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); let canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); @@ -122,8 +122,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(false); // open chart for saved search - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); await retry.waitFor(`Discover histogram to be displayed`, async () => { canvasExists = await elasticChart.canvasExists(); return canvasExists; @@ -145,8 +145,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show permitted hidden histogram state when returning back to discover', async () => { // close chart - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); let canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); @@ -155,8 +155,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); // open chart - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); @@ -171,8 +171,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(true); // close chart - await testSubjects.click('discoverChartOptionsToggle'); - await testSubjects.click('discoverChartToggle'); + await testSubjects.click('unifiedHistogramChartOptionsToggle'); + await testSubjects.click('unifiedHistogramChartToggle'); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); }); diff --git a/test/functional/apps/discover/group2/_sql_view.ts b/test/functional/apps/discover/group2/_sql_view.ts index d8ec69db66ee4..325752bc39fad 100644 --- a/test/functional/apps/discover/group2/_sql_view.ts +++ b/test/functional/apps/discover/group2/_sql_view.ts @@ -43,8 +43,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('superDatePickerToggleQuickMenuButton')).to.be(true); expect(await testSubjects.exists('addFilter')).to.be(true); expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(true); - expect(await testSubjects.exists('discoverChart')).to.be(true); - expect(await testSubjects.exists('discoverQueryHits')).to.be(true); + expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); + expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true); expect(await testSubjects.exists('discoverAlertsButton')).to.be(true); expect(await testSubjects.exists('shareTopNavButton')).to.be(true); expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(true); @@ -64,8 +64,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('showQueryBarMenu')).to.be(false); expect(await testSubjects.exists('addFilter')).to.be(false); expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false); - expect(await testSubjects.exists('discoverChart')).to.be(false); - expect(await testSubjects.exists('discoverQueryHits')).to.be(false); + expect(await testSubjects.exists('unifiedHistogramChart')).to.be(false); + expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(false); expect(await testSubjects.exists('discoverAlertsButton')).to.be(false); expect(await testSubjects.exists('shareTopNavButton')).to.be(false); expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(false); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index d9bc8c46a4acc..85c93c0fc2847 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -29,7 +29,7 @@ export class DiscoverPageObject extends FtrService { private readonly defaultFindTimeout = this.config.get('timeouts.find'); public async getChartTimespan() { - return await this.testSubjects.getAttribute('discoverChart', 'data-time-range'); + return await this.testSubjects.getAttribute('unifiedHistogramChart', 'data-time-range'); } public async getDocTable() { @@ -207,40 +207,40 @@ export class DiscoverPageObject extends FtrService { } public async isChartVisible() { - return await this.testSubjects.exists('discoverChart'); + return await this.testSubjects.exists('unifiedHistogramChart'); } public async toggleChartVisibility() { - await this.testSubjects.moveMouseTo('discoverChartOptionsToggle'); - await this.testSubjects.click('discoverChartOptionsToggle'); - await this.testSubjects.exists('discoverChartToggle'); - await this.testSubjects.click('discoverChartToggle'); + await this.testSubjects.moveMouseTo('unifiedHistogramChartOptionsToggle'); + await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); + await this.testSubjects.exists('unifiedHistogramChartToggle'); + await this.testSubjects.click('unifiedHistogramChartToggle'); await this.header.waitUntilLoadingHasFinished(); } public async getChartInterval() { - await this.testSubjects.click('discoverChartOptionsToggle'); - await this.testSubjects.click('discoverTimeIntervalPanel'); - const selectedOption = await this.find.byCssSelector(`.discoverIntervalSelected`); + await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); + await this.testSubjects.click('unifiedHistogramTimeIntervalPanel'); + const selectedOption = await this.find.byCssSelector(`.unifiedHistogramIntervalSelected`); return selectedOption.getVisibleText(); } public async getChartIntervalWarningIcon() { - await this.testSubjects.click('discoverChartOptionsToggle'); + await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); await this.header.waitUntilLoadingHasFinished(); return await this.find.existsByCssSelector('.euiToolTipAnchor'); } public async setChartInterval(interval: string) { - await this.testSubjects.click('discoverChartOptionsToggle'); - await this.testSubjects.click('discoverTimeIntervalPanel'); - await this.testSubjects.click(`discoverTimeInterval-${interval}`); + await this.testSubjects.click('unifiedHistogramChartOptionsToggle'); + await this.testSubjects.click('unifiedHistogramTimeIntervalPanel'); + await this.testSubjects.click(`unifiedHistogramTimeInterval-${interval}`); return await this.header.waitUntilLoadingHasFinished(); } public async getHitCount() { await this.header.waitUntilLoadingHasFinished(); - return await this.testSubjects.getVisibleText('discoverQueryHits'); + return await this.testSubjects.getVisibleText('unifiedHistogramQueryHits'); } public async getDocHeader() { @@ -573,7 +573,7 @@ export class DiscoverPageObject extends FtrService { } public async waitForChartLoadingComplete(renderCount: number) { - await this.elasticChart.waitForRenderingCount(renderCount, 'discoverChart'); + await this.elasticChart.waitForRenderingCount(renderCount, 'unifiedHistogramChart'); } public async waitForDocTableLoadingComplete() { diff --git a/tsconfig.base.json b/tsconfig.base.json index 3054a36f2bb86..271d6b1dc35df 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -169,6 +169,8 @@ "@kbn/ui-actions-plugin/*": ["src/plugins/ui_actions/*"], "@kbn/unified-field-list-plugin": ["src/plugins/unified_field_list"], "@kbn/unified-field-list-plugin/*": ["src/plugins/unified_field_list/*"], + "@kbn/unified-histogram-plugin": ["src/plugins/unified_histogram"], + "@kbn/unified-histogram-plugin/*": ["src/plugins/unified_histogram/*"], "@kbn/unified-search-plugin": ["src/plugins/unified_search"], "@kbn/unified-search-plugin/*": ["src/plugins/unified_search/*"], "@kbn/url-forwarding-plugin": ["src/plugins/url_forwarding"], diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index eba4c64dc818d..d4542ffa3c261 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1918,7 +1918,6 @@ "discover.advancedSettings.discover.showFieldStatisticsDescription": "Activez le {fieldStatisticsDocs} pour afficher des détails tels que les valeurs minimale et maximale d'un champ numérique ou une carte d'un champ géographique. Cette fonctionnalité est en version bêta et susceptible d'être modifiée.", "discover.advancedSettings.discover.showMultifieldsDescription": "Détermine si les {multiFields} doivent s'afficher dans la fenêtre de document étendue. Dans la plupart des cas, les champs multiples sont les mêmes que les champs d'origine. Cette option est uniquement disponible lorsque le paramètre ''searchFieldsFromSource'' est désactivé.", "discover.advancedSettings.enableSQLDescription": "{technicalPreviewLabel} Cette fonctionnalité en préversion est encore très expérimentale, ne pas s'y fier pour les recherches ni pour les tableaux de bord en production. Ce paramètre désactive SQL comme langage de requête à base de texte dans Discover. Si vous avez des commentaires sur cette expérience, contactez-nous via {link}", - "discover.bucketIntervalTooltip": "Cet intervalle crée {bucketsDescription} pour permettre l’affichage dans la plage temporelle sélectionnée, il a donc été redimensionné vers {bucketIntervalDescription}.", "discover.context.contextOfTitle": "Les documents relatifs à #{anchorId}", "discover.context.newerDocumentsWarning": "Seuls {docCount} documents plus récents que le document ancré ont été trouvés.", "discover.context.olderDocumentsWarning": "Seuls {docCount} documents plus anciens que le document ancré ont été trouvés.", @@ -1956,19 +1955,15 @@ "discover.grid.filterForAria": "Filtrer sur cette {value}", "discover.grid.filterOutAria": "Exclure cette {value}", "discover.gridSampleSize.description": "Vous voyez les {sampleSize} premiers échantillons de documents qui correspondent à votre recherche. Pour modifier cette valeur, accédez à {advancedSettingsLink}.", - "discover.histogramTimeRangeIntervalDescription": "(intervalle : {value})", - "discover.hitsPluralTitle": "{formattedHits} {hits, plural, one {résultat} other {résultats}}", "discover.howToSeeOtherMatchingDocumentsDescription": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner celle-ci pour en voir plus.", "discover.noMatchRoute.bannerText": "L'application Discover ne reconnaît pas cet itinéraire : {route}", "discover.noResults.tryRemovingOrDisablingFilters": "Essayez de supprimer ou de {disablingFiltersLink}.", "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", - "discover.partialHits": "≥ {formattedHits} {hits, plural, one {résultat} other {résultats}}", "discover.savedSearchAliasMatchRedirect.objectNoun": "Recherche {savedSearch}", "discover.savedSearchURLConflictCallout.objectNoun": "Recherche {savedSearch}", "discover.searchGenerationWithDescription": "Tableau généré par la recherche {searchTitle}", "discover.searchGenerationWithDescriptionGrid": "Tableau généré par la recherche {searchTitle} ({searchDescription})", "discover.selectedDocumentsNumber": "{nr} documents sélectionnés", - "discover.timeIntervalWithValue": "Intervalle de temps : {timeInterval}", "discover.topNav.optionsPopover.currentViewMode": "{viewModeLabel} : {currentViewMode}", "discover.utils.formatHit.moreFields": "et {count} {count, plural, one {autre champ} other {autres champs}}", "discover.valueIsNotConfiguredDataViewIDWarningTitle": "{stateVal} n'est pas un ID de vue de données configuré", @@ -2020,10 +2015,6 @@ "discover.backToTopLinkText": "Revenir en haut de la page.", "discover.badge.readOnly.text": "Lecture seule", "discover.badge.readOnly.tooltip": "Impossible d’enregistrer les recherches", - "discover.bucketIntervalTooltip.tooLargeBucketsText": "des compartiments trop volumineux", - "discover.bucketIntervalTooltip.tooManyBucketsText": "un trop grand nombre de compartiments", - "discover.chartOptions": "Options de graphique", - "discover.chartOptionsButton": "Options de graphique", "discover.clearSelection": "Effacer la sélection", "discover.context.breadcrumb": "Documents relatifs", "discover.context.failedToLoadAnchorDocumentDescription": "Échec de chargement du document ancré", @@ -2125,11 +2116,9 @@ "discover.dscTour.stepSort.description": "Utilisez le titre de colonne pour effectuer le tri sur un champ unique, ou la fenêtre contextuelle pour plusieurs champs.", "discover.dscTour.stepSort.imageAltText": "Cliquez sur un en-tête de colonne et sélectionnez l'ordre de tri souhaité. Ajustez un tri à champ multiple à l'aide de la fenêtre contextuelle des champs triés.", "discover.dscTour.stepSort.title": "Trier sur un ou plusieurs champs", - "discover.editVisualizationButton": "Modifier la visualisation", "discover.embeddable.inspectorRequestDataTitle": "Données", "discover.embeddable.inspectorRequestDescription": "Cette requête interroge Elasticsearch afin de récupérer les données pour la recherche.", "discover.embeddable.search.displayName": "rechercher", - "discover.errorLoadingChart": "Erreur lors du chargement du graphique", "discover.field.mappingConflict": "Ce champ est défini avec plusieurs types (chaîne, entier, etc.) dans les différents index qui correspondent à ce modèle. Vous pouvez toujours utiliser ce champ conflictuel, mais il sera indisponible pour les fonctions qui nécessitent que Kibana en connaisse le type. Pour corriger ce problème, vous devrez réindexer vos données.", "discover.field.mappingConflict.title": "Conflit de mapping", "discover.fieldChooser.addField.label": "Ajouter un champ", @@ -2233,10 +2222,6 @@ "discover.grid.viewDoc": "Afficher/Masquer les détails de la boîte de dialogue", "discover.gridSampleSize.advancedSettingsLinkLabel": "Paramètres avancés", "discover.helpMenu.appName": "Découverte", - "discover.hideChart": "Masquer le graphique", - "discover.histogramOfFoundDocumentsAriaLabel": "Histogramme des documents détectés", - "discover.histogramTimeRangeIntervalAuto": "Auto", - "discover.hitCountSpinnerAriaLabel": "Nombre final de résultats toujours en chargement", "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", "discover.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", @@ -2245,7 +2230,6 @@ "discover.inspectorRequestDescriptionTotalHits": "Cette requête interroge Elasticsearch afin de récupérer le nombre total de résultats.", "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", - "discover.loadingChartResults": "Chargement du graphique", "discover.loadingDocuments": "Chargement des documents", "discover.loadingJSON": "Chargement de JSON", "discover.loadingResults": "Chargement des résultats", @@ -2294,7 +2278,6 @@ "discover.searchingTitle": "Recherche", "discover.selectColumnHeader": "Sélectionner la colonne", "discover.showAllDocuments": "Afficher tous les documents", - "discover.showChart": "Afficher le graphique", "discover.showErrorMessageAgain": "Afficher le message d'erreur", "discover.showSelectedDocumentsOnly": "Afficher uniquement les documents sélectionnés", "discover.singleDocRoute.errorTitle": "Une erreur s'est produite", @@ -2302,8 +2285,6 @@ "discover.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", "discover.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", "discover.sourceViewer.refresh": "Actualiser", - "discover.timeIntervals": "Intervalles de temps", - "discover.timeIntervalWithValueWarning": "Avertissement", "discover.toggleSidebarAriaLabel": "Activer/Désactiver la barre latérale", "discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "Saviez-vous que Discover possède un nouvel Explorateur de documents avec un meilleur tri des données, des colonnes redimensionnables et une vue en plein écran ? Vous pouvez modifier le mode d'affichage dans les Paramètres avancés.", "discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "Vous pouvez revenir à l'affichage Discover classique dans les Paramètres avancés.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f5ef512297e2e..bbfd3a5f67903 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1916,7 +1916,6 @@ "discover.advancedSettings.discover.showFieldStatisticsDescription": "{fieldStatisticsDocs}を有効にすると、数値フィールドの最大/最小値やジオフィールドの地図といった詳細が表示されます。この機能はベータ段階で、変更される可能性があります。", "discover.advancedSettings.discover.showMultifieldsDescription": "拡張ドキュメントビューに{multiFields}が表示されるかどうかを制御します。ほとんどの場合、マルチフィールドは元のフィールドと同じです。「searchFieldsFromSource」がオフのときにのみこのオプションを使用できます。", "discover.advancedSettings.enableSQLDescription": "{technicalPreviewLabel} このパッチプレビュー機能は実験段階です。本番の保存された検索やダッシュボードでは、この機能を信頼しないでください。この設定により、Discoverでテキストベースのクエリ言語としてSQLを使用できます。このエクスペリエンスに関するフィードバックがございましたら、{link}からお問い合わせください", - "discover.bucketIntervalTooltip": "この間隔は選択された時間範囲に表示される{bucketsDescription}が作成されるため、{bucketIntervalDescription}にスケーリングされています。", "discover.context.contextOfTitle": "#{anchorId}の周りのドキュメント", "discover.context.newerDocumentsWarning": "アンカーよりも新しいドキュメントは{docCount}件しか見つかりませんでした。", "discover.context.olderDocumentsWarning": "アンカーよりも古いドキュメントは{docCount}件しか見つかりませんでした。", @@ -1952,19 +1951,15 @@ "discover.grid.filterForAria": "この{value}でフィルターを適用", "discover.grid.filterOutAria": "この{value}を除外", "discover.gridSampleSize.description": "検索と一致する最初の{sampleSize}ドキュメントを表示しています。この値を変更するには、{advancedSettingsLink}に移動してください。", - "discover.histogramTimeRangeIntervalDescription": "(間隔値: {value})", - "discover.hitsPluralTitle": "{formattedHits} {hits, plural, other {一致}}", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.noMatchRoute.bannerText": "Discoverアプリケーションはこのルート{route}を認識できません", "discover.noResults.tryRemovingOrDisablingFilters": "削除または{disablingFiltersLink}してください。", "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", - "discover.partialHits": "≥{formattedHits} {hits, plural, other {一致}}", "discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch}検索", "discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch}検索", "discover.searchGenerationWithDescription": "検索{searchTitle}で生成されたテーブル", "discover.searchGenerationWithDescriptionGrid": "検索{searchTitle}で生成されたテーブル({searchDescription})", "discover.selectedDocumentsNumber": "{nr}個のドキュメントが選択されました", - "discover.timeIntervalWithValue": "時間間隔:{timeInterval}", "discover.topNav.optionsPopover.currentViewMode": "{viewModeLabel}: {currentViewMode}", "discover.utils.formatHit.moreFields": "および{count} more {count, plural, other {個のフィールド}}", "discover.valueIsNotConfiguredDataViewIDWarningTitle": "{stateVal}は設定されたデータビューIDではありません", @@ -2016,10 +2011,6 @@ "discover.backToTopLinkText": "最上部へ戻る。", "discover.badge.readOnly.text": "読み取り専用", "discover.badge.readOnly.tooltip": "検索を保存できません", - "discover.bucketIntervalTooltip.tooLargeBucketsText": "大きすぎるバケット", - "discover.bucketIntervalTooltip.tooManyBucketsText": "バケットが多すぎます", - "discover.chartOptions": "グラフオプション", - "discover.chartOptionsButton": "グラフオプション", "discover.clearSelection": "選択した項目をクリア", "discover.context.breadcrumb": "周りのドキュメント", "discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました", @@ -2121,11 +2112,9 @@ "discover.dscTour.stepSort.description": "列見出しを使用して1つのフィールドを並べ替えるか、複数のフィールドに対してポップオーバーを使用します。", "discover.dscTour.stepSort.imageAltText": "列見出しをクリックして、任意の並べ順を選択します。フィールドが並べ替えられたポップオーバーを使用して、複数のフィールドの並べ替えを調整します。", "discover.dscTour.stepSort.title": "1つ以上のフィールドを並べ替えます", - "discover.editVisualizationButton": "ビジュアライゼーションを編集", "discover.embeddable.inspectorRequestDataTitle": "データ", "discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.embeddable.search.displayName": "検索", - "discover.errorLoadingChart": "グラフの読み込みエラー", "discover.field.mappingConflict": "このフィールドは、このパターンと一致するインデックス全体に対して複数の型(文字列、整数など)として定義されています。この競合フィールドを使用することはできますが、Kibana で型を認識する必要がある関数では使用できません。この問題を修正するにはデータのレンダリングが必要です。", "discover.field.mappingConflict.title": "マッピングの矛盾", "discover.fieldChooser.addField.label": "フィールドを追加", @@ -2229,10 +2218,6 @@ "discover.grid.viewDoc": "詳細ダイアログを切り替え", "discover.gridSampleSize.advancedSettingsLinkLabel": "高度な設定", "discover.helpMenu.appName": "Discover", - "discover.hideChart": "グラフを非表示", - "discover.histogramOfFoundDocumentsAriaLabel": "検出されたドキュメントのヒストグラム", - "discover.histogramTimeRangeIntervalAuto": "自動", - "discover.hitCountSpinnerAriaLabel": "読み込み中の最終一致件数", "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", "discover.inspectorRequestDataTitleTotalHits": "総ヒット数", @@ -2241,7 +2226,6 @@ "discover.inspectorRequestDescriptionTotalHits": "このリクエストはElasticsearchにクエリをかけ、合計一致数を取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", - "discover.loadingChartResults": "グラフを読み込み中", "discover.loadingDocuments": "ドキュメントを読み込み中", "discover.loadingJSON": "JSONを読み込んでいます", "discover.loadingResults": "結果を読み込み中", @@ -2290,7 +2274,6 @@ "discover.searchingTitle": "検索中", "discover.selectColumnHeader": "列を選択", "discover.showAllDocuments": "すべてのドキュメントを表示", - "discover.showChart": "グラフを表示", "discover.showErrorMessageAgain": "エラーメッセージを表示", "discover.showSelectedDocumentsOnly": "選択したドキュメントのみを表示", "discover.singleDocRoute.errorTitle": "エラーが発生しました", @@ -2298,8 +2281,6 @@ "discover.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", "discover.sourceViewer.errorMessageTitle": "エラーが発生しました", "discover.sourceViewer.refresh": "更新", - "discover.timeIntervals": "時間間隔", - "discover.timeIntervalWithValueWarning": "警告", "discover.toggleSidebarAriaLabel": "サイドバーを切り替える", "discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "Discoverの新しいドキュメントエクスプローラーでは、データの並べ替え、列のサイズ変更、全画面表示といった優れた機能をご利用いただけます。高度な設定で表示モードを変更できます。", "discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "高度な設定でクラシックDiscoverビューに戻すことができます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e4f966a0a485f..14c174124c67e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1918,7 +1918,6 @@ "discover.advancedSettings.discover.showFieldStatisticsDescription": "启用 {fieldStatisticsDocs} 以显示详细信息,如数字字段的最小和最大值,或地理字段的地图。此功能为公测版,可能会进行更改。", "discover.advancedSettings.discover.showMultifieldsDescription": "控制 {multiFields} 是否显示在展开的文档视图中。多数情况下,多字段与原始字段相同。此选项仅在 `searchFieldsFromSource` 关闭时可用。", "discover.advancedSettings.enableSQLDescription": "{technicalPreviewLabel} 此技术预览功能为高度实验性功能 -- 请勿在生产已保存搜索或仪表板中依赖此功能。此设置在 Discover 中将 SQL 用作基于文本的查询语言。\r\n如果具有与此体验有关的反馈,请通过 {link} 联系我们", - "discover.bucketIntervalTooltip": "此时间间隔创建的{bucketsDescription},无法在选定时间范围中显示,因此已调整为{bucketIntervalDescription}。", "discover.context.contextOfTitle": "#{anchorId} 周围的文档", "discover.context.newerDocumentsWarning": "仅可以找到 {docCount} 个比定位标记新的文档。", "discover.context.olderDocumentsWarning": "仅可以找到 {docCount} 个比定位标记旧的文档。", @@ -1956,19 +1955,15 @@ "discover.grid.filterForAria": "筛留此 {value}", "discover.grid.filterOutAria": "筛除此 {value}", "discover.gridSampleSize.description": "您正查看与您的搜索相匹配的前 {sampleSize} 个文档。要更改此值,请转到{advancedSettingsLink}。", - "discover.histogramTimeRangeIntervalDescription": "(时间间隔:{value})", - "discover.hitsPluralTitle": "{formattedHits} 个{hits, plural, other {命中}}", "discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.noMatchRoute.bannerText": "Discover 应用程序无法识别此路由:{route}", "discover.noResults.tryRemovingOrDisablingFilters": "尝试删除或{disablingFiltersLink}。", "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", - "discover.partialHits": "≥{formattedHits} 个{hits, plural, other {命中}}", "discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch} 搜索", "discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch} 搜索", "discover.searchGenerationWithDescription": "搜索 {searchTitle} 生成的表", "discover.searchGenerationWithDescriptionGrid": "搜索 {searchTitle} 生成的表({searchDescription})", "discover.selectedDocumentsNumber": "{nr} 个文档已选择", - "discover.timeIntervalWithValue": "时间间隔:{timeInterval}", "discover.topNav.optionsPopover.currentViewMode": "{viewModeLabel}:{currentViewMode}", "discover.utils.formatHit.moreFields": "及另外 {count} 个{count, plural, other {字段}}", "discover.valueIsNotConfiguredDataViewIDWarningTitle": "{stateVal} 不是配置的数据视图 ID", @@ -2020,10 +2015,6 @@ "discover.backToTopLinkText": "返回顶部。", "discover.badge.readOnly.text": "只读", "discover.badge.readOnly.tooltip": "无法保存搜索", - "discover.bucketIntervalTooltip.tooLargeBucketsText": "存储桶过大", - "discover.bucketIntervalTooltip.tooManyBucketsText": "存储桶过多", - "discover.chartOptions": "图表选项", - "discover.chartOptionsButton": "图表选项", "discover.clearSelection": "清除所选内容", "discover.context.breadcrumb": "周围文档", "discover.context.failedToLoadAnchorDocumentDescription": "无法加载定位点文档", @@ -2125,11 +2116,9 @@ "discover.dscTour.stepSort.description": "使用列标题对单一字段进行排序,或使用弹出框对多个字段进行排序。", "discover.dscTour.stepSort.imageAltText": "单击列标题并选择所需排序顺序。使用字段排序弹出框调整多字段排序。", "discover.dscTour.stepSort.title": "对一个或多个字段排序", - "discover.editVisualizationButton": "编辑可视化", "discover.embeddable.inspectorRequestDataTitle": "数据", "discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.embeddable.search.displayName": "搜索", - "discover.errorLoadingChart": "加载图表时出错", "discover.field.mappingConflict": "此字段在匹配此模式的各个索引中已定义为若干类型(字符串、整数等)。您可能仍可以使用此冲突字段,但它无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", "discover.field.mappingConflict.title": "映射冲突", "discover.fieldChooser.addField.label": "添加字段", @@ -2233,10 +2222,6 @@ "discover.grid.viewDoc": "切换具有详情的对话框", "discover.gridSampleSize.advancedSettingsLinkLabel": "高级设置", "discover.helpMenu.appName": "Discover", - "discover.hideChart": "隐藏图表", - "discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图", - "discover.histogramTimeRangeIntervalAuto": "自动", - "discover.hitCountSpinnerAriaLabel": "最终命中计数仍在加载", "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", "discover.inspectorRequestDataTitleTotalHits": "总命中数", @@ -2245,7 +2230,6 @@ "discover.inspectorRequestDescriptionTotalHits": "此请求将查询 Elasticsearch 以获取总命中数。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", - "discover.loadingChartResults": "正在加载图表", "discover.loadingDocuments": "正在加载文档", "discover.loadingJSON": "正在加载 JSON", "discover.loadingResults": "正在加载结果", @@ -2294,7 +2278,6 @@ "discover.searchingTitle": "正在搜索", "discover.selectColumnHeader": "选择列", "discover.showAllDocuments": "显示所有文档", - "discover.showChart": "显示图表", "discover.showErrorMessageAgain": "显示错误消息", "discover.showSelectedDocumentsOnly": "仅显示选定的文档", "discover.singleDocRoute.errorTitle": "发生错误", @@ -2302,8 +2285,6 @@ "discover.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", "discover.sourceViewer.errorMessageTitle": "发生错误", "discover.sourceViewer.refresh": "刷新", - "discover.timeIntervals": "时间间隔", - "discover.timeIntervalWithValueWarning": "警告", "discover.toggleSidebarAriaLabel": "切换侧边栏", "discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "您知道吗?Discover 有一种新的 Document Explorer,可提供更好的数据排序、可调整大小的列及全屏视图。您可以在高级设置中更改视图模式。", "discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "您可以在高级设置中切换回经典 Discover 视图。", diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index be1f223110503..453c3467c923a 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -95,7 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.discover.createAdHocDataView('logst', true); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click('discoverEditVisualization'); + await testSubjects.click('unifiedHistogramEditVisualization'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { diff --git a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts index e70e4b59dc31d..2ea778c2b8d7f 100644 --- a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts +++ b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts @@ -162,7 +162,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).getVisibleText(); expect(actualIndexPattern).to.be('*stash*'); - const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits'); + const actualDiscoverQueryHits = await testSubjects.getVisibleText( + 'unifiedHistogramQueryHits' + ); expect(actualDiscoverQueryHits).to.be('14,005'); const prevDataViewId = await PageObjects.discover.getCurrentDataViewId(); diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index ab9b08104a8e7..29f684c1ebf37 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('discoverChart'); + await testSubjects.existOrFail('unifiedHistogramChart'); // check the table columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['extension.raw', '@timestamp', 'bytes']); @@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const [lensWindowHandler, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('discoverChart'); + await testSubjects.existOrFail('unifiedHistogramChart'); expect(await queryBar.getQueryString()).be.eql(''); await browser.closeCurrentWindow(); await browser.switchToWindow(lensWindowHandler); @@ -103,7 +103,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const [lensWindowHandler, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('discoverChart'); + await testSubjects.existOrFail('unifiedHistogramChart'); // check the query expect(await queryBar.getQueryString()).be.eql( '( ( extension.raw: "png" ) OR ( extension.raw: "css" ) OR ( extension.raw: "jpg" ) )' @@ -139,7 +139,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const [lensWindowHandler, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('discoverChart'); + await testSubjects.existOrFail('unifiedHistogramChart'); // check the columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['extension.raw', '@timestamp', 'memory']); @@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const [lensWindowHandler, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('discoverChart'); + await testSubjects.existOrFail('unifiedHistogramChart'); // check the query expect(await queryBar.getQueryString()).be.eql( diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts index 92e9b6fcdb58e..a89b4c8727bc1 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('discoverChart'); + await testSubjects.existOrFail('unifiedHistogramChart'); // check the table columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['ip', '@timestamp', 'bytes']); diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts index d96ab079043a0..944e65b73f6e2 100644 --- a/x-pack/test/functional/services/transform/discover.ts +++ b/x-pack/test/functional/services/transform/discover.ts @@ -15,9 +15,11 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { return { async assertDiscoverQueryHits(expectedDiscoverQueryHits: string) { - await testSubjects.existOrFail('discoverQueryHits'); + await testSubjects.existOrFail('unifiedHistogramQueryHits'); - const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits'); + const actualDiscoverQueryHits = await testSubjects.getVisibleText( + 'unifiedHistogramQueryHits' + ); expect(actualDiscoverQueryHits).to.eql( expectedDiscoverQueryHits, @@ -59,7 +61,7 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { expect(actualApplyButtonText).to.be('Apply'); await applyButton.click(); - await testSubjects.existOrFail('discoverQueryHits'); + await testSubjects.existOrFail('unifiedHistogramQueryHits'); }, }; } From 35e8170a5a256c9885c73911174e6ca792b17555 Mon Sep 17 00:00:00 2001 From: Joseph Crail Date: Mon, 17 Oct 2022 14:24:14 -0700 Subject: [PATCH 20/74] [Profiling] Check caches before querying (#143089) * Add LRU cache for stackframes * Add LRU cache for executables * Remove splitting mgets into chunks * Move LRU cache for stacktraces before query * Summarize cache and query results Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../profiling/server/routes/stacktrace.ts | 227 ++++++++++++------ 1 file changed, 148 insertions(+), 79 deletions(-) diff --git a/x-pack/plugins/profiling/server/routes/stacktrace.ts b/x-pack/plugins/profiling/server/routes/stacktrace.ts index aee9b61694e21..0f513023fa829 100644 --- a/x-pack/plugins/profiling/server/routes/stacktrace.ts +++ b/x-pack/plugins/profiling/server/routes/stacktrace.ts @@ -6,7 +6,6 @@ */ import type { Logger } from '@kbn/core/server'; -import { chunk } from 'lodash'; import LRUCache from 'lru-cache'; import { INDEX_EXECUTABLES, INDEX_FRAMES, INDEX_TRACES } from '../../common'; import { @@ -32,8 +31,6 @@ import { withProfilingSpan } from '../utils/with_profiling_span'; import { DownsampledEventsIndex } from './downsampling'; import { ProjectTimeQuery } from './query'; -const traceLRU = new LRUCache({ max: 20000 }); - const BASE64_FRAME_ID_LENGTH = 32; export type EncodedStackTrace = DedotObject<{ @@ -236,80 +233,102 @@ export async function searchEventsGroupByStackTrace({ return { totalCount, stackTraceEvents }; } +function summarizeCacheAndQuery( + logger: Logger, + name: string, + cacheHits: number, + cacheTotal: number, + queryHits: number, + queryTotal: number +) { + logger.info(`found ${cacheHits} out of ${cacheTotal} ${name} in the cache`); + if (cacheHits === cacheTotal) { + return; + } + logger.info(`found ${queryHits} out of ${queryTotal} ${name}`); + if (queryHits < queryTotal) { + logger.info(`failed to find ${queryTotal - queryHits} ${name}`); + } +} + +const traceLRU = new LRUCache({ max: 20000 }); + export async function mgetStackTraces({ logger, client, events, - concurrency = 1, }: { logger: Logger; client: ProfilingESClient; events: Map; - concurrency?: number; }) { - const stackTraceIDs = [...events.keys()]; - const chunkSize = Math.floor(events.size / concurrency); - let chunks = chunk(stackTraceIDs, chunkSize); - - if (chunks.length !== concurrency) { - // The last array element contains the remainder, just drop it as irrelevant. - chunks = chunks.slice(0, concurrency); - } - - const stackResponses = await withProfilingSpan('mget_stacktraces', () => - Promise.all( - chunks.map((ids) => { - return client.mget< - PickFlattened< - ProfilingStackTrace, - ProfilingESField.StacktraceFrameIDs | ProfilingESField.StacktraceFrameTypes - > - >('mget_stacktraces_chunk', { - index: INDEX_TRACES, - ids, - realtime: true, - _source_includes: [ - ProfilingESField.StacktraceFrameIDs, - ProfilingESField.StacktraceFrameTypes, - ], - }); - }) - ) - ); + const stackTraceIDs = new Set([...events.keys()]); + const stackTraces = new Map(); + let cacheHits = 0; let totalFrames = 0; - const stackTraces = new Map(); const stackFrameDocIDs = new Set(); const executableDocIDs = new Set(); + for (const stackTraceID of stackTraceIDs) { + const stackTrace = traceLRU.get(stackTraceID); + if (stackTrace) { + cacheHits++; + stackTraceIDs.delete(stackTraceID); + stackTraces.set(stackTraceID, stackTrace); + + totalFrames += stackTrace.FrameIDs.length; + for (const frameID of stackTrace.FrameIDs) { + stackFrameDocIDs.add(frameID); + } + for (const fileID of stackTrace.FileIDs) { + executableDocIDs.add(fileID); + } + } + } + + if (stackTraceIDs.size === 0) { + summarizeCacheAndQuery(logger, 'stacktraces', cacheHits, events.size, 0, 0); + return { stackTraces, totalFrames, stackFrameDocIDs, executableDocIDs }; + } + + const stackResponses = await client.mget< + PickFlattened< + ProfilingStackTrace, + ProfilingESField.StacktraceFrameIDs | ProfilingESField.StacktraceFrameTypes + > + >('mget_stacktraces', { + index: INDEX_TRACES, + ids: [...stackTraceIDs], + realtime: true, + _source_includes: [ProfilingESField.StacktraceFrameIDs, ProfilingESField.StacktraceFrameTypes], + }); + + let queryHits = 0; const t0 = Date.now(); await withProfilingSpan('decode_stacktraces', async () => { - // flatMap() is significantly slower than an explicit for loop - for (const res of stackResponses) { - for (const trace of res.docs) { - if ('error' in trace) { - continue; + for (const trace of stackResponses.docs) { + if ('error' in trace) { + continue; + } + // Sometimes we don't find the trace. + // This is due to ES delays writing (data is not immediately seen after write). + // Also, ES doesn't know about transactions. + if (trace.found) { + queryHits++; + const traceid = trace._id as StackTraceID; + const stackTrace = decodeStackTrace(trace._source as EncodedStackTrace); + + stackTraces.set(traceid, stackTrace); + traceLRU.set(traceid, stackTrace); + + totalFrames += stackTrace.FrameIDs.length; + for (const frameID of stackTrace.FrameIDs) { + stackFrameDocIDs.add(frameID); } - // Sometimes we don't find the trace. - // This is due to ES delays writing (data is not immediately seen after write). - // Also, ES doesn't know about transactions. - if (trace.found) { - const traceid = trace._id as StackTraceID; - let stackTrace = traceLRU.get(traceid) as StackTrace; - if (!stackTrace) { - stackTrace = decodeStackTrace(trace._source as EncodedStackTrace); - traceLRU.set(traceid, stackTrace); - } - - totalFrames += stackTrace.FrameIDs.length; - stackTraces.set(traceid, stackTrace); - for (const frameID of stackTrace.FrameIDs) { - stackFrameDocIDs.add(frameID); - } - for (const fileID of stackTrace.FileIDs) { - executableDocIDs.add(fileID); - } + for (const fileID of stackTrace.FileIDs) { + executableDocIDs.add(fileID); } } } @@ -321,15 +340,20 @@ export async function mgetStackTraces({ logger.info('Average size of stacktrace: ' + totalFrames / stackTraces.size); } - if (stackTraces.size < events.size) { - logger.info( - 'failed to find ' + (events.size - stackTraces.size) + ' stacktraces (todo: find out why)' - ); - } + summarizeCacheAndQuery( + logger, + 'stacktraces', + cacheHits, + events.size, + queryHits, + stackTraceIDs.size + ); return { stackTraces, totalFrames, stackFrameDocIDs, executableDocIDs }; } +const frameLRU = new LRUCache({ max: 100000 }); + export async function mgetStackFrames({ logger, client, @@ -341,7 +365,20 @@ export async function mgetStackFrames({ }): Promise> { const stackFrames = new Map(); + let cacheHits = 0; + const cacheTotal = stackFrameIDs.size; + + for (const stackFrameID of stackFrameIDs) { + const stackFrame = frameLRU.get(stackFrameID); + if (stackFrame) { + cacheHits++; + stackFrames.set(stackFrameID, stackFrame); + stackFrameIDs.delete(stackFrameID); + } + } + if (stackFrameIDs.size === 0) { + summarizeCacheAndQuery(logger, 'frames', cacheHits, cacheTotal, 0, 0); return stackFrames; } @@ -352,7 +389,7 @@ export async function mgetStackFrames({ }); // Create a lookup map StackFrameID -> StackFrame. - let framesFound = 0; + let queryHits = 0; const t0 = Date.now(); const docs = resStackFrames.docs; for (const frame of docs) { @@ -360,25 +397,32 @@ export async function mgetStackFrames({ continue; } if (frame.found) { - stackFrames.set(frame._id, { + queryHits++; + const stackFrame = { FileName: frame._source!.Stackframe.file?.name, FunctionName: frame._source!.Stackframe.function?.name, FunctionOffset: frame._source!.Stackframe.function?.offset, LineNumber: frame._source!.Stackframe.line?.number, SourceType: frame._source!.Stackframe.source?.type, - }); - framesFound++; - } else { - stackFrames.set(frame._id, emptyStackFrame); + }; + stackFrames.set(frame._id, stackFrame); + frameLRU.set(frame._id, stackFrame); + continue; } + + stackFrames.set(frame._id, emptyStackFrame); + frameLRU.set(frame._id, emptyStackFrame); } + logger.info(`processing data took ${Date.now() - t0} ms`); - logger.info('found ' + framesFound + ' / ' + stackFrameIDs.size + ' frames'); + summarizeCacheAndQuery(logger, 'frames', cacheHits, cacheTotal, queryHits, stackFrameIDs.size); return stackFrames; } +const executableLRU = new LRUCache({ max: 100000 }); + export async function mgetExecutables({ logger, client, @@ -390,7 +434,20 @@ export async function mgetExecutables({ }): Promise> { const executables = new Map(); + let cacheHits = 0; + const cacheTotal = executableIDs.size; + + for (const fileID of executableIDs) { + const executable = executableLRU.get(fileID); + if (executable) { + cacheHits++; + executables.set(fileID, executable); + executableIDs.delete(fileID); + } + } + if (executableIDs.size === 0) { + summarizeCacheAndQuery(logger, 'frames', cacheHits, cacheTotal, 0, 0); return executables; } @@ -400,8 +457,8 @@ export async function mgetExecutables({ _source_includes: [ProfilingESField.ExecutableFileName], }); - // Create a lookup map StackFrameID -> StackFrame. - let exeFound = 0; + // Create a lookup map FileID -> Executable. + let queryHits = 0; const t0 = Date.now(); const docs = resExecutables.docs; for (const exe of docs) { @@ -409,17 +466,29 @@ export async function mgetExecutables({ continue; } if (exe.found) { - executables.set(exe._id, { + queryHits++; + const executable = { FileName: exe._source!.Executable.file.name, - }); - exeFound++; - } else { - executables.set(exe._id, emptyExecutable); + }; + executables.set(exe._id, executable); + executableLRU.set(exe._id, executable); + continue; } + + executables.set(exe._id, emptyExecutable); + executableLRU.set(exe._id, emptyExecutable); } + logger.info(`processing data took ${Date.now() - t0} ms`); - logger.info('found ' + exeFound + ' / ' + executableIDs.size + ' executables'); + summarizeCacheAndQuery( + logger, + 'executables', + cacheHits, + cacheTotal, + queryHits, + executableIDs.size + ); return executables; } From 8fba39c2dadd9b916e50a02efb4de39f7391c7b2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 Oct 2022 15:25:55 -0600 Subject: [PATCH 21/74] [Security Solution] Fixes a bug with timeline sourcerer state (#143237) --- .../sourcerer/use_pick_index_patterns.tsx | 6 +- .../containers/source/use_data_view.tsx | 10 +- .../containers/sourcerer/index.test.tsx | 195 +++++++++++++++++- .../common/containers/sourcerer/index.tsx | 51 +++-- .../public/common/store/sourcerer/actions.ts | 19 +- .../common/store/sourcerer/helpers.test.ts | 68 +++++- .../public/common/store/sourcerer/helpers.ts | 7 +- .../public/common/store/sourcerer/reducer.ts | 7 - 8 files changed, 307 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx index fd704f175a852..7be02be6abc37 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx @@ -195,11 +195,7 @@ export const usePickIndexPatterns = ({ // constantly getting destroy and re-init const pickedDataViewData = await getSourcererDataView(newSelectedDataViewId); if (isHookAlive.current) { - dispatch( - sourcererActions.updateSourcererDataViews({ - dataView: pickedDataViewData, - }) - ); + dispatch(sourcererActions.setDataView(pickedDataViewData)); setSelectedOptions( isOnlyDetectionAlerts ? alertsOptions diff --git a/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx b/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx index c259f8843263a..a5dd7f10edaab 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/use_data_view.tsx @@ -33,6 +33,7 @@ export type IndexFieldSearch = (param: { scopeId?: SourcererScopeName; needToBeInit?: boolean; cleanCache?: boolean; + skipScopeUpdate?: boolean; }) => Promise; type DangerCastForBrowserFieldsMutation = Record< @@ -102,6 +103,7 @@ export const useDataView = (): { scopeId = SourcererScopeName.default, needToBeInit = false, cleanCache = false, + skipScopeUpdate = false, }) => { const unsubscribe = () => { searchSubscription$.current[dataViewId]?.unsubscribe(); @@ -123,11 +125,7 @@ export const useDataView = (): { dataViewId, abortCtrl.current[dataViewId].signal ); - dispatch( - sourcererActions.updateSourcererDataViews({ - dataView: dataViewToUpdate, - }) - ); + dispatch(sourcererActions.setDataView(dataViewToUpdate)); } return new Promise((resolve) => { @@ -148,7 +146,7 @@ export const useDataView = (): { endTracking('success'); const patternString = response.indicesExist.sort().join(); - if (needToBeInit && scopeId) { + if (needToBeInit && scopeId && !skipScopeUpdate) { dispatch( sourcererActions.setSelectedDataView({ id: scopeId, diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 9be9c1266c1c9..634330ea10e91 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -34,6 +34,7 @@ import { import type { SelectedDataView } from '../../store/sourcerer/model'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { postSourcererDataView } from './api'; +import * as source from '../source/use_data_view'; import { sourcererActions } from '../../store/sourcerer'; import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string'; @@ -200,7 +201,7 @@ describe('Sourcerer Hooks', () => { }); }); - it('initilizes dataview with data from query string', async () => { + it('initializes dataview with data from query string', async () => { const selectedPatterns = ['testPattern-*']; const selectedDataViewId = 'security-solution-default'; (useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) => @@ -342,6 +343,198 @@ describe('Sourcerer Hooks', () => { }); }); }); + describe('initialization settings', () => { + const mockIndexFieldsSearch = jest.fn(); + beforeAll(() => { + // 👇️ not using dot-notation + the ignore clears up a ts error + // @ts-ignore + // eslint-disable-next-line dot-notation + source['useDataView'] = jest.fn(() => ({ + indexFieldsSearch: mockIndexFieldsSearch, + })); + }); + it('does not needToBeInit if scope is default and selectedPatterns/missingPatterns have values', async () => { + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenCalledWith({ + dataViewId: mockSourcererState.defaultDataView.id, + needToBeInit: false, + scopeId: SourcererScopeName.default, + }); + }); + }); + }); + + it('does needToBeInit if scope is default and selectedPatterns/missingPatterns are empty', async () => { + store = createStore( + { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + selectedPatterns: [], + missingPatterns: [], + }, + }, + }, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenCalledWith({ + dataViewId: mockSourcererState.defaultDataView.id, + needToBeInit: true, + scopeId: SourcererScopeName.default, + }); + }); + }); + }); + + it('does needToBeInit and skipScopeUpdate=false if scope is timeline and selectedPatterns/missingPatterns are empty', async () => { + store = createStore( + { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + ...mockGlobalState.sourcerer.kibanaDataViews, + { ...mockSourcererState.defaultDataView, id: 'something-weird', patternList: [] }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + selectedDataViewId: 'something-weird', + selectedPatterns: [], + missingPatterns: [], + }, + }, + }, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { + dataViewId: 'something-weird', + needToBeInit: true, + scopeId: SourcererScopeName.timeline, + skipScopeUpdate: false, + }); + }); + }); + }); + + it('does needToBeInit and skipScopeUpdate=true if scope is timeline and selectedPatterns have value', async () => { + store = createStore( + { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + ...mockGlobalState.sourcerer.kibanaDataViews, + { ...mockSourcererState.defaultDataView, id: 'something-weird', patternList: [] }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + selectedDataViewId: 'something-weird', + selectedPatterns: ['ohboy'], + missingPatterns: [], + }, + }, + }, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { + dataViewId: 'something-weird', + needToBeInit: true, + scopeId: SourcererScopeName.timeline, + skipScopeUpdate: true, + }); + }); + }); + }); + + it('does not needToBeInit if scope is timeline and data view has patternList', async () => { + store = createStore( + { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + ...mockGlobalState.sourcerer.kibanaDataViews, + { + ...mockSourcererState.defaultDataView, + id: 'something-weird', + patternList: ['ohboy'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + selectedDataViewId: 'something-weird', + selectedPatterns: [], + missingPatterns: [], + }, + }, + }, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { + dataViewId: 'something-weird', + needToBeInit: false, + scopeId: SourcererScopeName.timeline, + }); + }); + }); + }); + }); describe('useSourcererDataView', () => { it('Should put any excludes in the index pattern at the end of the pattern list, and sort both the includes and excludes', async () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 6db7392b596b7..07dafff7470d7 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -76,17 +76,21 @@ export const useInitSourcerer = ( const activeTimeline = useDeepEqualSelector((state) => getTimelineSelector(state, TimelineId.active) ); - const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []); + + const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); const { - selectedDataViewId: scopeDataViewId, - selectedPatterns, - missingPatterns, - } = useDeepEqualSelector((state) => scopeIdSelector(state, scopeId)); + sourcererScope: { selectedDataViewId: scopeDataViewId, selectedPatterns, missingPatterns }, + } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); + const { - selectedDataViewId: timelineDataViewId, - selectedPatterns: timelineSelectedPatterns, - missingPatterns: timelineMissingPatterns, - } = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline)); + selectedDataView: timelineSelectedDataView, + sourcererScope: { + selectedDataViewId: timelineDataViewId, + selectedPatterns: timelineSelectedPatterns, + missingPatterns: timelineMissingPatterns, + }, + } = useDeepEqualSelector((state) => sourcererScopeSelector(state, SourcererScopeName.timeline)); + const { indexFieldsSearch } = useDataView(); const onInitializeUrlParam = useCallback( @@ -137,19 +141,29 @@ export const useInitSourcerer = ( const searchedIds = useRef([]); useEffect(() => { const activeDataViewIds = [...new Set([scopeDataViewId, timelineDataViewId])]; - activeDataViewIds.forEach((id) => { + activeDataViewIds.forEach((id, i) => { if (id != null && id.length > 0 && !searchedIds.current.includes(id)) { searchedIds.current = [...searchedIds.current, id]; + + const currentScope = i === 0 ? SourcererScopeName.default : SourcererScopeName.timeline; + + const needToBeInit = + id === scopeDataViewId + ? selectedPatterns.length === 0 && missingPatterns.length === 0 + : timelineDataViewId === id + ? timelineMissingPatterns.length === 0 && + timelineSelectedDataView?.patternList.length === 0 + : false; + indexFieldsSearch({ dataViewId: id, - scopeId: - id === scopeDataViewId ? SourcererScopeName.default : SourcererScopeName.timeline, - needToBeInit: - id === scopeDataViewId - ? selectedPatterns.length === 0 && missingPatterns.length === 0 - : timelineDataViewId === id - ? timelineMissingPatterns.length === 0 && timelineSelectedPatterns.length === 0 - : false, + scopeId: currentScope, + needToBeInit, + ...(needToBeInit && currentScope === SourcererScopeName.timeline + ? { + skipScopeUpdate: timelineSelectedPatterns.length > 0, + } + : {}), }); } }); @@ -160,6 +174,7 @@ export const useInitSourcerer = ( selectedPatterns.length, timelineDataViewId, timelineMissingPatterns.length, + timelineSelectedDataView, timelineSelectedPatterns.length, ]); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts index f452d34cba310..db6dcedef8262 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts @@ -7,23 +7,12 @@ import actionCreatorFactory from 'typescript-fsa'; -import type { - KibanaDataView, - SelectedDataView, - SourcererDataView, - SourcererScopeName, -} from './model'; +import type { SelectedDataView, SourcererDataView, SourcererScopeName } from './model'; import type { SecurityDataView } from '../../containers/sourcerer/api'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer'); -export const setDataView = actionCreator<{ - browserFields: SourcererDataView['browserFields']; - id: SourcererDataView['id']; - indexFields: SourcererDataView['indexFields']; - loading: SourcererDataView['loading']; - runtimeMappings: SourcererDataView['runtimeMappings']; -}>('SET_DATA_VIEW'); +export const setDataView = actionCreator>('SET_DATA_VIEW'); export const setDataViewLoading = actionCreator<{ id: string; @@ -48,7 +37,3 @@ export interface SelectedDataViewPayload { shouldValidateSelectedPatterns?: boolean; } export const setSelectedDataView = actionCreator('SET_SELECTED_DATA_VIEW'); - -export const updateSourcererDataViews = actionCreator<{ - dataView: KibanaDataView; -}>('UPDATE_SOURCERER_DATA_VIEWS'); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts index a1b9586834cf6..aa193026465cf 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { mockGlobalState } from '../../mock'; +import { mockGlobalState, mockSourcererState } from '../../mock'; import { SourcererScopeName } from './model'; import { getScopePatternListSelection, validateSelectedPatterns } from './helpers'; import { sortWithExcludesAtEnd } from '../../../../common/utils/sourcerer'; @@ -210,5 +210,71 @@ describe('sourcerer store helpers', () => { }); }); }); + + it('does not attempt to validate when missing patterns', () => { + const state = { + ...mockGlobalState.sourcerer, + defaultDataView: { + ...mockSourcererState.defaultDataView, + patternList: [], + }, + kibanaDataViews: [ + { + ...mockSourcererState.defaultDataView, + patternList: [], + }, + ], + }; + const result = validateSelectedPatterns( + state, + { + ...payload, + id: SourcererScopeName.default, + selectedPatterns: ['auditbeat-*', 'yoohoo'], + }, + true + ); + expect(result).toEqual({ + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + missingPatterns: ['yoohoo'], + selectedPatterns: ['auditbeat-*', 'yoohoo'], + }, + }); + }); + + it('does not attempt to validate if non-default data view has not been initialized', () => { + const state = { + ...mockGlobalState.sourcerer, + defaultDataView: { + ...mockSourcererState.defaultDataView, + patternList: [], + }, + kibanaDataViews: [ + { + ...mockSourcererState.defaultDataView, + id: 'wow', + patternList: [], + }, + ], + }; + const result = validateSelectedPatterns( + state, + { + ...payload, + id: SourcererScopeName.default, + selectedDataViewId: 'wow', + selectedPatterns: ['auditbeat-*', 'yoohoo'], + }, + true + ); + expect(result).toEqual({ + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + selectedDataViewId: 'wow', + selectedPatterns: ['auditbeat-*', 'yoohoo'], + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts index 187e4923ed218..3d3b061194f79 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts @@ -58,7 +58,12 @@ export const validateSelectedPatterns = ( const selectedPatterns = // shouldValidateSelectedPatterns is false when upgrading from // legacy pre-8.0 timeline index patterns to data view. - shouldValidateSelectedPatterns && dataView != null && missingPatterns.length === 0 + shouldValidateSelectedPatterns && + dataView != null && + missingPatterns.length === 0 && + // don't validate when the data view has not been initialized (default is initialized already always) + dataView.id !== state.defaultDataView.id && + dataView.patternList.length > 0 ? dedupePatterns.filter( (pattern) => (dataView != null && dataView.patternList.includes(pattern)) || diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts index dc084956bcad8..5a253083d8b15 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts @@ -14,7 +14,6 @@ import { setSignalIndexName, setDataView, setDataViewLoading, - updateSourcererDataViews, } from './actions'; import type { SourcererModel } from './model'; import { initDataView, initialSourcererState, SourcererScopeName } from './model'; @@ -47,12 +46,6 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState) ...dataView, })), })) - .case(updateSourcererDataViews, (state, { dataView }) => ({ - ...state, - kibanaDataViews: state.kibanaDataViews.map((dv) => - dv.id === dataView.id ? { ...dv, ...dataView } : dv - ), - })) .case(setSourcererScopeLoading, (state, { id, loading }) => ({ ...state, sourcererScopes: { From 834a3c375ccf10b7d88bbbda6b7d3f030fd4ed00 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:28:49 -0700 Subject: [PATCH 22/74] [DOCS] Updates links in landing page (#143469) * [DOCS] Updates linkes * [DOCS] Adds link to release notes --- docs/index-extra-title-page.html | 46 +++++++++++++++++--------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html index b70e3a985f22b..ced2737984fa5 100644 --- a/docs/index-extra-title-page.html +++ b/docs/index-extra-title-page.html @@ -4,8 +4,10 @@ get the most of your data.

- How-to videos -

+ What's new +
Release notes + How-to videos +

@@ -17,27 +19,27 @@
  • - What is Kibana?
  • - Quick start
  • - Add data
  • - Create a dashboard
  • - Search your data
  • @@ -48,27 +50,27 @@
    • - Install Kibana
    • - Configure settings
    • - Kibana Query Language (KQL)
    • - Create a data view
    • - Create an alert
    • @@ -85,27 +87,27 @@
      • - Explore your data
      • - Create dashboards
      • - Create presentations
      • - Map geographic data
      • - Model, predict, and detect behavior
      • @@ -117,27 +119,27 @@
        • Secure access to Kibana
        • - Organize your data in spaces
        • - Categorize your saved objects
        • - Quickly find apps and objects
        • - Manage your data
        • From fabb3bc415a66deac1690fb8f3ab3b76dc0181f6 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 17 Oct 2022 16:07:10 -0700 Subject: [PATCH 23/74] [Elastic Defend onboarding][Fleet Integration] Unit tests to createDefaultPolicy and getPackagePolicyPostCreateCallback (#142690) --- .../fleet_integration.test.ts | 79 +++++++++- .../handlers/create_default_policy.test.ts | 142 ++++++++++++++++++ 2 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 664abac92db93..17d1d8f9a9295 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -19,6 +19,7 @@ import { import { buildManifestManagerMock } from '../endpoint/services/artifacts/manifest_manager/manifest_manager.mock'; import { getPackagePolicyCreateCallback, + getPackagePolicyPostCreateCallback, getPackagePolicyDeleteCallback, getPackagePolicyUpdateCallback, } from './fleet_integration'; @@ -40,11 +41,16 @@ import type { InternalArtifactCompleteSchema } from '../endpoint/schemas/artifac import { ManifestManager } from '../endpoint/services/artifacts/manifest_manager'; import { getMockArtifacts, toArtifactRecords } from '../endpoint/lib/artifacts/mocks'; import { Manifest } from '../endpoint/lib/artifacts'; -import type { NewPackagePolicy } from '@kbn/fleet-plugin/common/types/models'; +import type { NewPackagePolicy, PackagePolicy } from '@kbn/fleet-plugin/common/types/models'; import type { ManifestSchema } from '../../common/endpoint/schema/manifest'; import type { DeletePackagePoliciesResponse } from '@kbn/fleet-plugin/common'; import { createMockPolicyData } from '../endpoint/services/feature_usage/mocks'; import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../common/endpoint/service/artifacts/constants'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; + +jest.mock('uuid', () => ({ + v4: (): string => 'NEW_UUID', +})); describe('ingest_integration tests ', () => { let endpointAppContextMock: EndpointAppContextServiceStartContract; @@ -248,12 +254,75 @@ describe('ingest_integration tests ', () => { expect(manifestManager.pushArtifacts).not.toHaveBeenCalled(); expect(manifestManager.commit).not.toHaveBeenCalled(); }); - - it.todo('should override policy config with endpoint settings'); - it.todo('should override policy config with cloud settings'); }); + describe('package policy post create callback', () => { - it.todo('should create Event Filters given valid parameter on integration config'); + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyPostCreateCallback(logger, exceptionListClient); + const policyConfig = generator.generatePolicyPackagePolicy() as PackagePolicy; + + it('should create the Endpoint Event Filters List and add the correct Event Filters List Item attached to the policy given nonInteractiveSession parameter on integration config eventFilters', async () => { + const integrationConfig = { + type: 'cloud', + eventFilters: { + nonInteractiveSession: true, + }, + }; + + policyConfig.inputs[0]!.config!.integration_config = { + value: integrationConfig, + }; + const postCreatedPolicyConfig = await callback( + policyConfig, + requestContextMock.convertContext(ctx), + req + ); + + expect(await exceptionListClient.createExceptionList).toHaveBeenCalledWith( + expect.objectContaining({ listId: ENDPOINT_EVENT_FILTERS_LIST_ID }) + ); + + expect(await exceptionListClient.createExceptionListItem).toHaveBeenCalledWith( + expect.objectContaining({ + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + tags: [`policy:${postCreatedPolicyConfig.id}`], + osTypes: ['linux'], + entries: [ + { + field: 'process.entry_leader.interactive', + operator: 'included', + type: 'match', + value: 'false', + }, + ], + itemId: 'NEW_UUID', + namespaceType: 'agnostic', + }) + ); + }); + + it('should not call Event Filters List and Event Filters List Item if nonInteractiveSession parameter is not present on integration config eventFilters', async () => { + const integrationConfig = { + type: 'cloud', + }; + + policyConfig.inputs[0]!.config!.integration_config = { + value: integrationConfig, + }; + const postCreatedPolicyConfig = await callback( + policyConfig, + requestContextMock.convertContext(ctx), + req + ); + + expect(await exceptionListClient.createExceptionList).not.toHaveBeenCalled(); + + expect(await exceptionListClient.createExceptionListItem).not.toHaveBeenCalled(); + + expect(postCreatedPolicyConfig.inputs[0]!.config!.integration_config.value).toEqual( + integrationConfig + ); + }); }); describe('package policy update callback (when the license is below platinum)', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts new file mode 100644 index 0000000000000..73440a310b625 --- /dev/null +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { Subject } from 'rxjs'; +import type { ILicense } from '@kbn/licensing-plugin/common/types'; +import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; +import { LicenseService } from '../../../common/license'; +import { createDefaultPolicy } from './create_default_policy'; +import type { PolicyConfig } from '../../../common/endpoint/types'; +import { + policyFactory as policyConfigFactory, + policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures, +} from '../../../common/endpoint/models/policy_config'; +import type { + AnyPolicyCreateConfig, + PolicyCreateCloudConfig, + PolicyCreateEndpointConfig, +} from '../types'; + +describe('Create Default Policy tests ', () => { + const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); + const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); + let licenseEmitter: Subject; + let licenseService: LicenseService; + + const createDefaultPolicyCallback = (config: AnyPolicyCreateConfig | undefined): PolicyConfig => { + return createDefaultPolicy(licenseService, config); + }; + + beforeEach(() => { + licenseEmitter = new Subject(); + licenseService = new LicenseService(); + licenseService.start(licenseEmitter); + licenseEmitter.next(Platinum); // set license level to platinum + }); + describe('When no config is set', () => { + it('Should return the Default Policy Config when license is at least platinum', () => { + const policy = createDefaultPolicyCallback(undefined); + expect(policy).toEqual(policyConfigFactory()); + }); + it('Should return the Default Policy Config without paid features when license is below platinum', () => { + licenseEmitter.next(Gold); + const policy = createDefaultPolicyCallback(undefined); + expect(policy).toEqual(policyConfigFactoryWithoutPaidFeatures()); + }); + }); + describe('When endpoint config is set', () => { + const createEndpointConfig = ( + endpointConfig: PolicyCreateEndpointConfig['endpointConfig'] + ): PolicyCreateEndpointConfig => { + return { + type: 'endpoint', + endpointConfig, + }; + }; + + const defaultEventsDisabled = () => ({ + linux: { + process: false, + file: false, + network: false, + session_data: false, + tty_io: false, + }, + mac: { + process: false, + file: false, + network: false, + }, + windows: { + process: false, + file: false, + network: false, + dll_and_driver_load: false, + dns: false, + registry: false, + security: false, + }, + }); + const OSTypes = ['linux', 'mac', 'windows'] as const; + + it('Should return only process event enabled on policy when preset is NGAV', () => { + const config = createEndpointConfig({ preset: 'NGAV' }); + const policy = createDefaultPolicyCallback(config); + const events = defaultEventsDisabled(); + OSTypes.forEach((os) => { + expect(policy[os].events).toMatchObject({ + ...events[os], + process: true, + }); + }); + }); + it('Should return process, file and network events enabled when preset is EDR Essential', () => { + const config = createEndpointConfig({ preset: 'EDREssential' }); + const policy = createDefaultPolicyCallback(config); + const events = defaultEventsDisabled(); + const enabledEvents = { + process: true, + file: true, + network: true, + }; + OSTypes.forEach((os) => { + expect(policy[os].events).toMatchObject({ + ...events[os], + ...enabledEvents, + }); + }); + }); + it('Should return the default config when preset is EDR Complete', () => { + const config = createEndpointConfig({ preset: 'EDRComplete' }); + const policy = createDefaultPolicyCallback(config); + const policyFactory = policyConfigFactory(); + expect(policy).toMatchObject(policyFactory); + }); + }); + describe('When cloud config is set', () => { + const createCloudConfig = (): PolicyCreateCloudConfig => ({ + type: 'cloud', + }); + + it('Session data should be enabled for Linux', () => { + const config = createCloudConfig(); + const policy = createDefaultPolicyCallback(config); + expect(policy.linux.events.session_data).toBe(true); + }); + it('Protections should be disabled for all OSs', () => { + const config = createCloudConfig(); + const policy = createDefaultPolicyCallback(config); + const OSTypes = ['linux', 'mac', 'windows'] as const; + OSTypes.forEach((os) => { + expect(policy[os].malware.mode).toBe('off'); + expect(policy[os].memory_protection.mode).toBe('off'); + expect(policy[os].behavior_protection.mode).toBe('off'); + }); + // Ransomware is windows only + expect(policy.windows.ransomware.mode).toBe('off'); + }); + }); +}); From 4d301f7a5d729bba0ddaaae5bb2e4bbf703e2300 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 17 Oct 2022 19:14:08 -0500 Subject: [PATCH 24/74] [searchService] Dedupe shard error toasts (#131776) * dedupe shard error toasts Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../search/fetch/handle_warnings.test.ts | 34 +++++++++--- .../public/search/fetch/handle_warnings.tsx | 54 ++++++++++++++----- .../data/public/search/search_service.test.ts | 13 +++-- .../data/public/search/search_service.ts | 16 ++++-- test/examples/search/warnings.ts | 2 +- 5 files changed, 94 insertions(+), 25 deletions(-) diff --git a/src/plugins/data/public/search/fetch/handle_warnings.test.ts b/src/plugins/data/public/search/fetch/handle_warnings.test.ts index 8a1bb86b9f71c..0d644d8dc0811 100644 --- a/src/plugins/data/public/search/fetch/handle_warnings.test.ts +++ b/src/plugins/data/public/search/fetch/handle_warnings.test.ts @@ -45,13 +45,17 @@ const warnings: SearchResponseWarning[] = [ }, ]; +const sessionId = 'abcd'; + describe('Filtering and showing warnings', () => { const notifications = notificationServiceMock.createStartContract(); + jest.useFakeTimers('modern'); describe('handleWarnings', () => { const request = { body: {} }; beforeEach(() => { jest.resetAllMocks(); + jest.advanceTimersByTime(30000); setNotifications(notifications); (notifications.toasts.addWarning as jest.Mock).mockReset(); (extract.extractWarnings as jest.Mock).mockImplementation(() => warnings); @@ -60,10 +64,16 @@ describe('Filtering and showing warnings', () => { test('should notify if timed out', () => { (extract.extractWarnings as jest.Mock).mockImplementation(() => [warnings[0]]); const response = { rawResponse: { timed_out: true } } as unknown as estypes.SearchResponse; - handleWarnings(request, response, theme); + handleWarnings({ request, response, theme }); + // test debounce, addWarning should only be called once + handleWarnings({ request, response, theme }); expect(notifications.toasts.addWarning).toBeCalledTimes(1); expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Something timed out!' }); + + // test debounce, call addWarning again due to sessionId + handleWarnings({ request, response, theme, sessionId }); + expect(notifications.toasts.addWarning).toBeCalledTimes(2); }); test('should notify if shards failed for unknown type/reason', () => { @@ -71,10 +81,16 @@ describe('Filtering and showing warnings', () => { const response = { rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } }, } as unknown as estypes.SearchResponse; - handleWarnings(request, response, theme); + handleWarnings({ request, response, theme }); + // test debounce, addWarning should only be called once + handleWarnings({ request, response, theme }); expect(notifications.toasts.addWarning).toBeCalledTimes(1); expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Some shards failed!' }); + + // test debounce, call addWarning again due to sessionId + handleWarnings({ request, response, theme, sessionId }); + expect(notifications.toasts.addWarning).toBeCalledTimes(2); }); test('should add mount point for shard modal failure button if warning.text is provided', () => { @@ -82,13 +98,19 @@ describe('Filtering and showing warnings', () => { const response = { rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } }, } as unknown as estypes.SearchResponse; - handleWarnings(request, response, theme); + handleWarnings({ request, response, theme }); + // test debounce, addWarning should only be called once + handleWarnings({ request, response, theme }); expect(notifications.toasts.addWarning).toBeCalledTimes(1); expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Some shards failed!', text: expect.any(Function), }); + + // test debounce, call addWarning again due to sessionId + handleWarnings({ request, response, theme, sessionId }); + expect(notifications.toasts.addWarning).toBeCalledTimes(2); }); test('should notify once if the response contains multiple failures', () => { @@ -96,7 +118,7 @@ describe('Filtering and showing warnings', () => { const response = { rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } }, } as unknown as estypes.SearchResponse; - handleWarnings(request, response, theme); + handleWarnings({ request, response, theme }); expect(notifications.toasts.addWarning).toBeCalledTimes(1); expect(notifications.toasts.addWarning).toBeCalledWith({ @@ -111,7 +133,7 @@ describe('Filtering and showing warnings', () => { const response = { rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } }, } as unknown as estypes.SearchResponse; - handleWarnings(request, response, theme, callback); + handleWarnings({ request, response, theme, callback }); expect(notifications.toasts.addWarning).toBeCalledTimes(1); expect(notifications.toasts.addWarning).toBeCalledWith({ title: 'Some shards failed!' }); @@ -122,7 +144,7 @@ describe('Filtering and showing warnings', () => { const response = { rawResponse: { _shards: { failed: 1, total: 2, successful: 1, skipped: 1 } }, } as unknown as estypes.SearchResponse; - handleWarnings(request, response, theme, callback); + handleWarnings({ request, response, theme, callback }); expect(notifications.toasts.addWarning).toBeCalledTimes(0); }); diff --git a/src/plugins/data/public/search/fetch/handle_warnings.tsx b/src/plugins/data/public/search/fetch/handle_warnings.tsx index 0ffb6389f81c4..1720ea1f76c0c 100644 --- a/src/plugins/data/public/search/fetch/handle_warnings.tsx +++ b/src/plugins/data/public/search/fetch/handle_warnings.tsx @@ -7,10 +7,12 @@ */ import { estypes } from '@elastic/elasticsearch'; +import { debounce } from 'lodash'; import { EuiSpacer } from '@elastic/eui'; import { ThemeServiceStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import React from 'react'; +import type { MountPoint } from '@kbn/core/public'; import { SearchRequest } from '..'; import { getNotifications } from '../../services'; import { ShardFailureOpenModalButton, ShardFailureRequest } from '../../shard_failure_modal'; @@ -21,23 +23,53 @@ import { } from '../types'; import { extractWarnings } from './extract_warnings'; +const getDebouncedWarning = () => { + const addWarning = () => { + const { toasts } = getNotifications(); + return debounce(toasts.addWarning.bind(toasts), 30000, { + leading: true, + }); + }; + const memory: Record> = {}; + + return ( + debounceKey: string, + title: string, + text?: string | MountPoint | undefined + ) => { + memory[debounceKey] = memory[debounceKey] || addWarning(); + return memory[debounceKey]({ title, text }); + }; +}; + +const debouncedWarningWithoutReason = getDebouncedWarning(); +const debouncedTimeoutWarning = getDebouncedWarning(); +const debouncedWarning = getDebouncedWarning(); + /** * @internal * All warnings are expected to come from the same response. Therefore all "text" properties, which contain the * response, will be the same. */ -export function handleWarnings( - request: SearchRequest, - response: estypes.SearchResponse, - theme: ThemeServiceStart, - cb?: WarningHandlerCallback -) { +export function handleWarnings({ + request, + response, + theme, + callback, + sessionId = '', +}: { + request: SearchRequest; + response: estypes.SearchResponse; + theme: ThemeServiceStart; + callback?: WarningHandlerCallback; + sessionId?: string; +}) { const warnings = extractWarnings(response); if (warnings.length === 0) { return; } - const internal = cb ? filterWarnings(warnings, cb) : warnings; + const internal = callback ? filterWarnings(warnings, callback) : warnings; if (internal.length === 0) { return; } @@ -45,9 +77,7 @@ export function handleWarnings( // timeout notification const [timeout] = internal.filter((w) => w.type === 'timed_out'); if (timeout) { - getNotifications().toasts.addWarning({ - title: timeout.message, - }); + debouncedTimeoutWarning(sessionId + timeout.message, timeout.message); } // shard warning failure notification @@ -75,12 +105,12 @@ export function handleWarnings( { theme$: theme.theme$ } ); - getNotifications().toasts.addWarning({ title, text }); + debouncedWarning(sessionId + warning.text, title, text); return; } // timeout warning, or shard warning with no failure reason - getNotifications().toasts.addWarning({ title }); + debouncedWarningWithoutReason(sessionId + title, title); } /** diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 432549698eed2..c1c8fc37a0e72 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -27,6 +27,7 @@ describe('Search service', () => { let mockCoreSetup: MockedKeys; let mockCoreStart: MockedKeys; const initializerContext = coreMock.createPluginInitializerContext(); + jest.useFakeTimers('modern'); initializerContext.config.get = jest.fn().mockReturnValue({ search: { aggs: { shardDelay: { enabled: false } }, sessions: { enabled: true } }, }); @@ -35,6 +36,7 @@ describe('Search service', () => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); searchService = new SearchService(initializerContext); + jest.advanceTimersByTime(30000); }); describe('setup()', () => { @@ -217,7 +219,13 @@ describe('Search service', () => { const responder1 = inspector.adapter.start('request1'); const responder2 = inspector.adapter.start('request2'); responder1.ok(getMockResponseWithShards(shards)); - responder2.ok(getMockResponseWithShards(shards)); + responder2.ok({ + json: { + rawResponse: { + timed_out: true, + }, + }, + }); data.showWarnings(inspector.adapter, callback); @@ -227,8 +235,7 @@ describe('Search service', () => { text: expect.any(Function), }); expect(notifications.toasts.addWarning).nthCalledWith(2, { - title: '2 of 4 shards failed', - text: expect.any(Function), + title: 'Data might be incomplete because your request timed out', }); }); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index c88201405d7f0..e7b753838edff 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -240,7 +240,12 @@ export class SearchService implements Plugin { onResponse: (request, response, options) => { if (!options.disableShardFailureWarning) { const { rawResponse } = response; - handleWarnings(request.body, rawResponse, theme); + handleWarnings({ + request: request.body, + response: rawResponse, + theme, + sessionId: options.sessionId, + }); } return response; }, @@ -271,7 +276,7 @@ export class SearchService implements Plugin { showError: (e) => { this.searchInterceptor.showError(e); }, - showWarnings: (adapter, cb) => { + showWarnings: (adapter, callback) => { adapter?.getRequests().forEach((request) => { const rawResponse = ( request.response?.json as { rawResponse: estypes.SearchResponse | undefined } @@ -281,7 +286,12 @@ export class SearchService implements Plugin { return; } - handleWarnings(request.json as SearchRequest, rawResponse, theme, cb); + handleWarnings({ + request: request.json as SearchRequest, + response: rawResponse, + theme, + callback, + }); }); }, session: this.sessionService, diff --git a/test/examples/search/warnings.ts b/test/examples/search/warnings.ts index fc1949549d66e..2cc674fb01024 100644 --- a/test/examples/search/warnings.ts +++ b/test/examples/search/warnings.ts @@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { toasts = await find.allByCssSelector(toastsSelector); expect(toasts.length).to.be(2); }); - const expects = ['2 of 4 shards failed', 'Query result']; + const expects = ['Query result', '2 of 4 shards failed']; await asyncForEach(toasts, async (t, index) => { expect(await t.getVisibleText()).to.eql(expects[index]); }); From a9461556006831111ce3be3b27ad75520db8bf97 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 17 Oct 2022 22:46:22 -0600 Subject: [PATCH 25/74] [api-docs] Daily api_docs build (#143491) --- api_docs/actions.devdocs.json | 14 + api_docs/actions.mdx | 4 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.devdocs.json | 14 + api_docs/alerting.mdx | 4 +- api_docs/apm.devdocs.json | 2 +- api_docs/apm.mdx | 2 +- api_docs/banners.mdx | 2 +- api_docs/bfetch.mdx | 2 +- api_docs/canvas.mdx | 2 +- api_docs/cases.mdx | 2 +- api_docs/charts.mdx | 2 +- api_docs/cloud.devdocs.json | 150 ++- api_docs/cloud.mdx | 4 +- api_docs/cloud_chat.mdx | 2 +- api_docs/cloud_experiments.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/controls.mdx | 2 +- api_docs/core.mdx | 2 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.devdocs.json | 14 + api_docs/dashboard.mdx | 4 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.mdx | 2 +- api_docs/data_query.mdx | 2 +- api_docs/data_search.mdx | 2 +- api_docs/data_view_editor.mdx | 2 +- api_docs/data_view_field_editor.mdx | 2 +- api_docs/data_view_management.mdx | 2 +- api_docs/data_views.mdx | 2 +- api_docs/data_visualizer.mdx | 2 +- api_docs/deprecations_by_api.mdx | 2 +- api_docs/deprecations_by_plugin.mdx | 6 +- api_docs/deprecations_by_team.mdx | 6 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- api_docs/embeddable.devdocs.json | 4 +- api_docs/embeddable.mdx | 2 +- api_docs/embeddable_enhanced.mdx | 2 +- api_docs/encrypted_saved_objects.mdx | 2 +- api_docs/enterprise_search.mdx | 2 +- api_docs/es_ui_shared.mdx | 2 +- api_docs/event_annotation.mdx | 2 +- api_docs/event_log.mdx | 2 +- api_docs/expression_error.mdx | 2 +- api_docs/expression_gauge.mdx | 2 +- api_docs/expression_heatmap.devdocs.json | 13 +- api_docs/expression_heatmap.mdx | 4 +- api_docs/expression_image.mdx | 2 +- api_docs/expression_legacy_metric_vis.mdx | 2 +- api_docs/expression_metric.mdx | 2 +- api_docs/expression_metric_vis.mdx | 2 +- api_docs/expression_partition_vis.mdx | 2 +- api_docs/expression_repeat_image.mdx | 2 +- api_docs/expression_reveal_image.mdx | 2 +- api_docs/expression_shape.mdx | 2 +- api_docs/expression_tagcloud.mdx | 2 +- api_docs/expression_x_y.devdocs.json | 11 + api_docs/expression_x_y.mdx | 4 +- api_docs/expressions.devdocs.json | 132 +- api_docs/expressions.mdx | 4 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/files.mdx | 2 +- api_docs/fleet.mdx | 2 +- api_docs/global_search.mdx | 2 +- api_docs/guided_onboarding.mdx | 2 +- api_docs/home.mdx | 2 +- api_docs/index_lifecycle_management.mdx | 2 +- api_docs/index_management.mdx | 2 +- api_docs/infra.mdx | 2 +- api_docs/inspector.mdx | 2 +- api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_ace.mdx | 2 +- api_docs/kbn_aiops_components.mdx | 2 +- api_docs/kbn_aiops_utils.mdx | 2 +- api_docs/kbn_alerts.mdx | 2 +- api_docs/kbn_analytics.mdx | 2 +- api_docs/kbn_analytics_client.mdx | 2 +- ..._analytics_shippers_elastic_v3_browser.mdx | 2 +- ...n_analytics_shippers_elastic_v3_common.mdx | 2 +- ...n_analytics_shippers_elastic_v3_server.mdx | 2 +- api_docs/kbn_analytics_shippers_fullstory.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_synthtrace.mdx | 2 +- api_docs/kbn_apm_utils.mdx | 2 +- api_docs/kbn_axe_config.mdx | 2 +- api_docs/kbn_cases_components.mdx | 2 +- api_docs/kbn_chart_icons.mdx | 2 +- api_docs/kbn_ci_stats_core.mdx | 2 +- api_docs/kbn_ci_stats_performance_metrics.mdx | 2 +- api_docs/kbn_ci_stats_reporter.mdx | 2 +- api_docs/kbn_cli_dev_mode.mdx | 2 +- api_docs/kbn_coloring.mdx | 2 +- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_config_mocks.mdx | 2 +- api_docs/kbn_config_schema.mdx | 2 +- .../kbn_content_management_table_list.mdx | 2 +- api_docs/kbn_core_analytics_browser.mdx | 2 +- .../kbn_core_analytics_browser_internal.mdx | 2 +- api_docs/kbn_core_analytics_browser_mocks.mdx | 2 +- api_docs/kbn_core_analytics_server.mdx | 2 +- .../kbn_core_analytics_server_internal.mdx | 2 +- api_docs/kbn_core_analytics_server_mocks.mdx | 2 +- api_docs/kbn_core_application_browser.mdx | 2 +- .../kbn_core_application_browser_internal.mdx | 2 +- .../kbn_core_application_browser_mocks.mdx | 2 +- api_docs/kbn_core_application_common.mdx | 2 +- api_docs/kbn_core_apps_browser_internal.mdx | 2 +- api_docs/kbn_core_apps_browser_mocks.mdx | 2 +- api_docs/kbn_core_base_browser_mocks.mdx | 2 +- api_docs/kbn_core_base_common.mdx | 2 +- api_docs/kbn_core_base_server_internal.mdx | 2 +- api_docs/kbn_core_base_server_mocks.mdx | 2 +- .../kbn_core_capabilities_browser_mocks.mdx | 2 +- api_docs/kbn_core_capabilities_common.mdx | 2 +- api_docs/kbn_core_capabilities_server.mdx | 2 +- .../kbn_core_capabilities_server_mocks.mdx | 2 +- api_docs/kbn_core_chrome_browser.mdx | 2 +- api_docs/kbn_core_chrome_browser_mocks.mdx | 2 +- api_docs/kbn_core_config_server_internal.mdx | 2 +- api_docs/kbn_core_deprecations_browser.mdx | 2 +- ...kbn_core_deprecations_browser_internal.mdx | 2 +- .../kbn_core_deprecations_browser_mocks.mdx | 2 +- api_docs/kbn_core_deprecations_common.mdx | 2 +- api_docs/kbn_core_deprecations_server.mdx | 2 +- .../kbn_core_deprecations_server_internal.mdx | 2 +- .../kbn_core_deprecations_server_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_browser.mdx | 2 +- api_docs/kbn_core_doc_links_browser_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_server.mdx | 2 +- api_docs/kbn_core_doc_links_server_mocks.mdx | 2 +- ...e_elasticsearch_client_server_internal.mdx | 2 +- ...core_elasticsearch_client_server_mocks.mdx | 2 +- api_docs/kbn_core_elasticsearch_server.mdx | 2 +- ...kbn_core_elasticsearch_server_internal.mdx | 2 +- .../kbn_core_elasticsearch_server_mocks.mdx | 2 +- .../kbn_core_environment_server_internal.mdx | 2 +- .../kbn_core_environment_server_mocks.mdx | 2 +- .../kbn_core_execution_context_browser.mdx | 2 +- ...ore_execution_context_browser_internal.mdx | 2 +- ...n_core_execution_context_browser_mocks.mdx | 2 +- .../kbn_core_execution_context_common.mdx | 2 +- .../kbn_core_execution_context_server.mdx | 2 +- ...core_execution_context_server_internal.mdx | 2 +- ...bn_core_execution_context_server_mocks.mdx | 2 +- api_docs/kbn_core_fatal_errors_browser.mdx | 2 +- .../kbn_core_fatal_errors_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_browser.mdx | 2 +- api_docs/kbn_core_http_browser_internal.mdx | 2 +- api_docs/kbn_core_http_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_common.mdx | 2 +- .../kbn_core_http_context_server_mocks.mdx | 2 +- ...re_http_request_handler_context_server.mdx | 2 +- api_docs/kbn_core_http_resources_server.mdx | 2 +- ...bn_core_http_resources_server_internal.mdx | 2 +- .../kbn_core_http_resources_server_mocks.mdx | 2 +- .../kbn_core_http_router_server_internal.mdx | 2 +- .../kbn_core_http_router_server_mocks.mdx | 2 +- api_docs/kbn_core_http_server.mdx | 2 +- api_docs/kbn_core_http_server_internal.mdx | 2 +- api_docs/kbn_core_http_server_mocks.mdx | 2 +- api_docs/kbn_core_i18n_browser.mdx | 2 +- api_docs/kbn_core_i18n_browser_mocks.mdx | 2 +- api_docs/kbn_core_i18n_server.mdx | 2 +- api_docs/kbn_core_i18n_server_internal.mdx | 2 +- api_docs/kbn_core_i18n_server_mocks.mdx | 2 +- .../kbn_core_injected_metadata_browser.mdx | 2 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- ...kbn_core_integrations_browser_internal.mdx | 2 +- .../kbn_core_integrations_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_browser.mdx | 2 +- api_docs/kbn_core_lifecycle_browser_mocks.mdx | 2 +- api_docs/kbn_core_logging_server.mdx | 2 +- api_docs/kbn_core_logging_server_internal.mdx | 2 +- api_docs/kbn_core_logging_server_mocks.mdx | 2 +- ...ore_metrics_collectors_server_internal.mdx | 2 +- ...n_core_metrics_collectors_server_mocks.mdx | 2 +- api_docs/kbn_core_metrics_server.mdx | 2 +- api_docs/kbn_core_metrics_server_internal.mdx | 2 +- api_docs/kbn_core_metrics_server_mocks.mdx | 2 +- api_docs/kbn_core_mount_utils_browser.mdx | 2 +- api_docs/kbn_core_node_server.mdx | 2 +- api_docs/kbn_core_node_server_internal.mdx | 2 +- api_docs/kbn_core_node_server_mocks.mdx | 2 +- api_docs/kbn_core_notifications_browser.mdx | 2 +- ...bn_core_notifications_browser_internal.mdx | 2 +- .../kbn_core_notifications_browser_mocks.mdx | 2 +- api_docs/kbn_core_overlays_browser.mdx | 2 +- .../kbn_core_overlays_browser_internal.mdx | 2 +- api_docs/kbn_core_overlays_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_browser.mdx | 2 +- api_docs/kbn_core_plugins_browser_mocks.mdx | 2 +- api_docs/kbn_core_preboot_server.mdx | 2 +- api_docs/kbn_core_preboot_server_mocks.mdx | 2 +- api_docs/kbn_core_rendering_browser_mocks.mdx | 2 +- .../kbn_core_rendering_server_internal.mdx | 2 +- api_docs/kbn_core_rendering_server_mocks.mdx | 2 +- .../kbn_core_saved_objects_api_browser.mdx | 2 +- .../kbn_core_saved_objects_api_server.mdx | 2 +- ...core_saved_objects_api_server_internal.mdx | 2 +- ...bn_core_saved_objects_api_server_mocks.mdx | 2 +- ...ore_saved_objects_base_server_internal.mdx | 2 +- ...n_core_saved_objects_base_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_browser.mdx | 2 +- ...bn_core_saved_objects_browser_internal.mdx | 2 +- .../kbn_core_saved_objects_browser_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_common.mdx | 2 +- ..._objects_import_export_server_internal.mdx | 2 +- ...ved_objects_import_export_server_mocks.mdx | 2 +- ...aved_objects_migration_server_internal.mdx | 2 +- ...e_saved_objects_migration_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_server.mdx | 2 +- ...kbn_core_saved_objects_server_internal.mdx | 2 +- .../kbn_core_saved_objects_server_mocks.mdx | 2 +- .../kbn_core_saved_objects_utils_server.mdx | 2 +- api_docs/kbn_core_status_common.mdx | 2 +- api_docs/kbn_core_status_common_internal.mdx | 2 +- api_docs/kbn_core_status_server.mdx | 2 +- api_docs/kbn_core_status_server_internal.mdx | 2 +- api_docs/kbn_core_status_server_mocks.mdx | 2 +- ...core_test_helpers_deprecations_getters.mdx | 2 +- ...n_core_test_helpers_http_setup_browser.mdx | 2 +- ...n_core_test_helpers_so_type_serializer.mdx | 2 +- api_docs/kbn_core_theme_browser.mdx | 2 +- api_docs/kbn_core_theme_browser_internal.mdx | 2 +- api_docs/kbn_core_theme_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_browser.mdx | 2 +- .../kbn_core_ui_settings_browser_internal.mdx | 2 +- .../kbn_core_ui_settings_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_common.mdx | 2 +- api_docs/kbn_core_ui_settings_server.mdx | 2 +- .../kbn_core_ui_settings_server_internal.mdx | 2 +- .../kbn_core_ui_settings_server_mocks.mdx | 2 +- api_docs/kbn_core_usage_data_server.mdx | 2 +- .../kbn_core_usage_data_server_internal.mdx | 2 +- api_docs/kbn_core_usage_data_server_mocks.mdx | 2 +- api_docs/kbn_crypto.mdx | 2 +- api_docs/kbn_crypto_browser.mdx | 2 +- api_docs/kbn_datemath.mdx | 2 +- api_docs/kbn_dev_cli_errors.mdx | 2 +- api_docs/kbn_dev_cli_runner.mdx | 2 +- api_docs/kbn_dev_proc_runner.mdx | 2 +- api_docs/kbn_dev_utils.mdx | 2 +- api_docs/kbn_doc_links.mdx | 2 +- api_docs/kbn_docs_utils.mdx | 2 +- api_docs/kbn_ebt_tools.mdx | 2 +- api_docs/kbn_es_archiver.mdx | 2 +- api_docs/kbn_es_errors.mdx | 2 +- api_docs/kbn_es_query.mdx | 2 +- api_docs/kbn_es_types.mdx | 2 +- api_docs/kbn_eslint_plugin_imports.mdx | 2 +- api_docs/kbn_field_types.mdx | 2 +- api_docs/kbn_find_used_node_modules.mdx | 2 +- .../kbn_ftr_common_functional_services.mdx | 2 +- api_docs/kbn_generate.mdx | 2 +- api_docs/kbn_get_repo_files.mdx | 2 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_hapi_mocks.mdx | 2 +- api_docs/kbn_home_sample_data_card.mdx | 2 +- api_docs/kbn_home_sample_data_tab.mdx | 2 +- api_docs/kbn_i18n.mdx | 2 +- api_docs/kbn_import_resolver.mdx | 2 +- api_docs/kbn_interpreter.mdx | 2 +- api_docs/kbn_io_ts_utils.mdx | 2 +- api_docs/kbn_jest_serializers.mdx | 2 +- api_docs/kbn_journeys.mdx | 2 +- api_docs/kbn_kibana_manifest_schema.mdx | 2 +- api_docs/kbn_logging.mdx | 2 +- api_docs/kbn_logging_mocks.mdx | 2 +- api_docs/kbn_managed_vscode_config.mdx | 2 +- api_docs/kbn_mapbox_gl.mdx | 2 +- api_docs/kbn_ml_agg_utils.mdx | 2 +- api_docs/kbn_ml_is_populated_object.mdx | 2 +- api_docs/kbn_ml_string_hash.mdx | 2 +- api_docs/kbn_monaco.mdx | 2 +- api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_optimizer_webpack_helpers.mdx | 2 +- api_docs/kbn_osquery_io_ts_types.mdx | 2 +- ..._performance_testing_dataset_extractor.mdx | 2 +- api_docs/kbn_plugin_generator.mdx | 2 +- api_docs/kbn_plugin_helpers.mdx | 2 +- api_docs/kbn_react_field.mdx | 2 +- api_docs/kbn_repo_source_classifier.mdx | 2 +- api_docs/kbn_rule_data_utils.mdx | 2 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_es_utils.mdx | 2 +- ...ritysolution_exception_list_components.mdx | 2 +- api_docs/kbn_securitysolution_hook_utils.mdx | 2 +- ..._securitysolution_io_ts_alerting_types.mdx | 2 +- .../kbn_securitysolution_io_ts_list_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_utils.mdx | 2 +- api_docs/kbn_securitysolution_list_api.mdx | 2 +- .../kbn_securitysolution_list_constants.mdx | 2 +- api_docs/kbn_securitysolution_list_hooks.mdx | 2 +- api_docs/kbn_securitysolution_list_utils.mdx | 2 +- api_docs/kbn_securitysolution_rules.mdx | 2 +- api_docs/kbn_securitysolution_t_grid.mdx | 2 +- api_docs/kbn_securitysolution_utils.mdx | 2 +- api_docs/kbn_server_http_tools.mdx | 2 +- api_docs/kbn_server_route_repository.mdx | 2 +- api_docs/kbn_shared_svg.mdx | 2 +- ...ared_ux_avatar_user_profile_components.mdx | 2 +- ...hared_ux_button_exit_full_screen_mocks.mdx | 2 +- api_docs/kbn_shared_ux_button_toolbar.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_link_redirect_app_mocks.mdx | 2 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- ...shared_ux_page_analytics_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- ...bn_shared_ux_page_kibana_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_template.mdx | 2 +- ...n_shared_ux_page_kibana_template_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data.mdx | 2 +- .../kbn_shared_ux_page_no_data_config.mdx | 2 +- ...bn_shared_ux_page_no_data_config_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_solution_nav.mdx | 2 +- .../kbn_shared_ux_prompt_no_data_views.mdx | 2 +- ...n_shared_ux_prompt_no_data_views_mocks.mdx | 2 +- api_docs/kbn_shared_ux_router.mdx | 2 +- api_docs/kbn_shared_ux_router_mocks.mdx | 2 +- api_docs/kbn_shared_ux_storybook_config.mdx | 2 +- api_docs/kbn_shared_ux_storybook_mock.mdx | 2 +- api_docs/kbn_shared_ux_utility.mdx | 2 +- api_docs/kbn_some_dev_log.mdx | 2 +- api_docs/kbn_sort_package_json.mdx | 2 +- api_docs/kbn_std.mdx | 2 +- api_docs/kbn_stdio_dev_helpers.mdx | 2 +- api_docs/kbn_storybook.mdx | 2 +- api_docs/kbn_telemetry_tools.mdx | 2 +- api_docs/kbn_test.mdx | 2 +- api_docs/kbn_test_jest_helpers.mdx | 2 +- api_docs/kbn_test_subj_selector.mdx | 2 +- api_docs/kbn_tooling_log.mdx | 2 +- api_docs/kbn_type_summarizer.mdx | 2 +- api_docs/kbn_type_summarizer_core.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_theme.mdx | 2 +- api_docs/kbn_user_profile_components.mdx | 2 +- api_docs/kbn_utility_types.mdx | 2 +- api_docs/kbn_utility_types_jest.mdx | 2 +- api_docs/kbn_utils.mdx | 2 +- api_docs/kbn_yarn_lock_validator.mdx | 2 +- api_docs/kibana_overview.mdx | 2 +- api_docs/kibana_react.mdx | 2 +- api_docs/kibana_utils.mdx | 2 +- api_docs/kubernetes_security.mdx | 2 +- api_docs/lens.devdocs.json | 11 + api_docs/lens.mdx | 4 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- api_docs/licensing.devdocs.json | 16 + api_docs/licensing.mdx | 2 +- api_docs/lists.mdx | 2 +- api_docs/management.mdx | 2 +- api_docs/maps.mdx | 2 +- api_docs/maps_ems.mdx | 2 +- api_docs/ml.mdx | 2 +- api_docs/monitoring.mdx | 2 +- api_docs/monitoring_collection.mdx | 2 +- api_docs/navigation.mdx | 2 +- api_docs/newsfeed.mdx | 2 +- api_docs/observability.devdocs.json | 2 +- api_docs/observability.mdx | 2 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 25 +- api_docs/presentation_util.mdx | 2 +- api_docs/profiling.mdx | 2 +- api_docs/remote_clusters.mdx | 2 +- api_docs/reporting.mdx | 2 +- api_docs/rollup.mdx | 2 +- api_docs/rule_registry.mdx | 2 +- api_docs/runtime_fields.mdx | 2 +- api_docs/saved_objects.mdx | 2 +- api_docs/saved_objects_finder.mdx | 2 +- api_docs/saved_objects_management.mdx | 2 +- api_docs/saved_objects_tagging.mdx | 2 +- api_docs/saved_objects_tagging_oss.mdx | 2 +- api_docs/saved_search.mdx | 2 +- api_docs/screenshot_mode.mdx | 2 +- api_docs/screenshotting.mdx | 2 +- api_docs/security.mdx | 2 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.devdocs.json | 14 + api_docs/share.mdx | 4 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.mdx | 2 +- api_docs/stack_alerts.mdx | 2 +- api_docs/stack_connectors.mdx | 2 +- api_docs/task_manager.mdx | 2 +- api_docs/telemetry.mdx | 2 +- api_docs/telemetry_collection_manager.mdx | 2 +- api_docs/telemetry_collection_xpack.mdx | 2 +- api_docs/telemetry_management_section.mdx | 2 +- api_docs/threat_intelligence.mdx | 2 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.mdx | 2 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_field_list.mdx | 2 +- api_docs/unified_histogram.devdocs.json | 1085 +++++++++++++++++ api_docs/unified_histogram.mdx | 36 + api_docs/unified_search.mdx | 2 +- api_docs/unified_search_autocomplete.mdx | 2 +- api_docs/url_forwarding.mdx | 2 +- api_docs/usage_collection.mdx | 2 +- api_docs/ux.mdx | 2 +- api_docs/vis_default_editor.mdx | 2 +- api_docs/vis_type_gauge.mdx | 2 +- api_docs/vis_type_heatmap.mdx | 2 +- api_docs/vis_type_pie.mdx | 2 +- api_docs/vis_type_table.mdx | 2 +- api_docs/vis_type_timelion.mdx | 2 +- api_docs/vis_type_timeseries.mdx | 2 +- api_docs/vis_type_vega.mdx | 2 +- api_docs/vis_type_vislib.mdx | 2 +- api_docs/vis_type_xy.mdx | 2 +- api_docs/visualizations.mdx | 2 +- 427 files changed, 1929 insertions(+), 462 deletions(-) create mode 100644 api_docs/unified_histogram.devdocs.json create mode 100644 api_docs/unified_histogram.mdx diff --git a/api_docs/actions.devdocs.json b/api_docs/actions.devdocs.json index c20f6e15fb966..0f63f83fedf12 100644 --- a/api_docs/actions.devdocs.json +++ b/api_docs/actions.devdocs.json @@ -1389,6 +1389,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "actions", + "id": "def-server.ActionTypeExecutorOptions.logger", + "type": "Object", + "tags": [], + "label": "logger", + "description": [], + "signature": [ + "Logger" + ], + "path": "x-pack/plugins/actions/server/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "actions", "id": "def-server.ActionTypeExecutorOptions.isEphemeral", diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 44a8da932ebdf..c3eb06615e8f8 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 213 | 0 | 208 | 23 | +| 214 | 0 | 209 | 23 | ## Client diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index c9f06a7fd2960..3f764836650f8 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index e8173cc57239e..cfabe6b38aaa1 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index 4c8345c2bc602..2e701198049b0 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -1902,6 +1902,20 @@ "path": "x-pack/plugins/alerting/server/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-server.RuleExecutorOptions.logger", + "type": "Object", + "tags": [], + "label": "logger", + "description": [], + "signature": [ + "Logger" + ], + "path": "x-pack/plugins/alerting/server/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index e66c835cde417..556859658e63b 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 381 | 0 | 372 | 24 | +| 382 | 0 | 373 | 24 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index 017ae6a510700..7fd8519951fc6 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -3703,7 +3703,7 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { samples: { traceId: string; transactionId: string; }[]; }, ", + ", { traceSamples: { traceId: string; transactionId: string; }[]; }, ", "APMRouteCreateOptions", ">; \"GET /internal/apm/transactions/{transactionId}\": ", "ServerRoute", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index c55852de2cb5a..5ceacebf9e42c 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 0104cb6bda448..2a1de0f673861 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 1062c61b4eb77..d99310156b987 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index dd47790528e13..0fc9007e20b60 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index a170a098ef509..2fc8e1a6c41d0 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index ec60bb2bf7937..a87e142890f08 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.devdocs.json b/api_docs/cloud.devdocs.json index 89134783e726f..e86310156f1c1 100644 --- a/api_docs/cloud.devdocs.json +++ b/api_docs/cloud.devdocs.json @@ -101,13 +101,27 @@ }, { "parentPluginId": "cloud", - "id": "def-public.CloudConfigType.full_story", - "type": "Object", + "id": "def-public.CloudConfigType.trial_end_date", + "type": "string", + "tags": [], + "label": "trial_end_date", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/cloud/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cloud", + "id": "def-public.CloudConfigType.is_elastic_staff_owned", + "type": "CompoundType", "tags": [], - "label": "full_story", + "label": "is_elastic_staff_owned", "description": [], "signature": [ - "{ enabled: boolean; org_id?: string | undefined; eventTypesAllowlist?: string[] | undefined; }" + "boolean | undefined" ], "path": "x-pack/plugins/cloud/public/plugin.tsx", "deprecated": false, @@ -275,7 +289,9 @@ "type": "string", "tags": [], "label": "cloudId", - "description": [], + "description": [ + "\nCloud ID. Undefined if not running on Cloud." + ], "signature": [ "string | undefined" ], @@ -289,7 +305,9 @@ "type": "string", "tags": [], "label": "cname", - "description": [], + "description": [ + "\nThis value is the same as `baseUrl` on ESS but can be customized on ECE." + ], "signature": [ "string | undefined" ], @@ -303,7 +321,9 @@ "type": "string", "tags": [], "label": "baseUrl", - "description": [], + "description": [ + "\nThis is the URL of the Cloud interface." + ], "signature": [ "string | undefined" ], @@ -317,7 +337,9 @@ "type": "string", "tags": [], "label": "deploymentUrl", - "description": [], + "description": [ + "\nThe full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud." + ], "signature": [ "string | undefined" ], @@ -331,7 +353,9 @@ "type": "string", "tags": [], "label": "profileUrl", - "description": [], + "description": [ + "\nThe full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud." + ], "signature": [ "string | undefined" ], @@ -345,7 +369,9 @@ "type": "string", "tags": [], "label": "organizationUrl", - "description": [], + "description": [ + "\nThe full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud." + ], "signature": [ "string | undefined" ], @@ -359,7 +385,9 @@ "type": "string", "tags": [], "label": "snapshotsUrl", - "description": [], + "description": [ + "\nThis is the path to the Snapshots page for the deployment to which the Kibana instance belongs. The value is already prepended with `deploymentUrl`." + ], "signature": [ "string | undefined" ], @@ -373,7 +401,41 @@ "type": "boolean", "tags": [], "label": "isCloudEnabled", - "description": [], + "description": [ + "\n`true` when Kibana is running on Elastic Cloud." + ], + "path": "x-pack/plugins/cloud/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cloud", + "id": "def-public.CloudSetup.trialEndDate", + "type": "Object", + "tags": [], + "label": "trialEndDate", + "description": [ + "\nWhen the Cloud Trial ends/ended for the organization that owns this deployment. Only available when running on Elastic Cloud." + ], + "signature": [ + "Date | undefined" + ], + "path": "x-pack/plugins/cloud/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cloud", + "id": "def-public.CloudSetup.isElasticStaffOwned", + "type": "CompoundType", + "tags": [], + "label": "isElasticStaffOwned", + "description": [ + "\n`true` if the Elastic Cloud organization that owns this deployment is owned by an Elastician. Only available when running on Elastic Cloud." + ], + "signature": [ + "boolean | undefined" + ], "path": "x-pack/plugins/cloud/public/plugin.tsx", "deprecated": false, "trackAdoption": false @@ -384,7 +446,9 @@ "type": "Function", "tags": [], "label": "registerCloudService", - "description": [], + "description": [ + "\nRegisters CloudServiceProviders so start's `CloudContextProvider` hooks them." + ], "signature": [ "(contextProvider: React.FC<{}>) => void" ], @@ -398,7 +462,9 @@ "type": "Function", "tags": [], "label": "contextProvider", - "description": [], + "description": [ + "The React component from the Service Provider." + ], "signature": [ "React.FC<{}>" ], @@ -428,7 +494,9 @@ "type": "Interface", "tags": [], "label": "CloudSetup", - "description": [], + "description": [ + "\nSetup contract" + ], "path": "x-pack/plugins/cloud/server/plugin.ts", "deprecated": false, "trackAdoption": false, @@ -439,7 +507,9 @@ "type": "string", "tags": [], "label": "cloudId", - "description": [], + "description": [ + "\nThe deployment's Cloud ID. Only available when running on Elastic Cloud." + ], "signature": [ "string | undefined" ], @@ -453,7 +523,9 @@ "type": "string", "tags": [], "label": "deploymentId", - "description": [], + "description": [ + "\nThe deployment's ID. Only available when running on Elastic Cloud." + ], "signature": [ "string | undefined" ], @@ -467,7 +539,9 @@ "type": "boolean", "tags": [], "label": "isCloudEnabled", - "description": [], + "description": [ + "\n`true` when running on Elastic Cloud." + ], "path": "x-pack/plugins/cloud/server/plugin.ts", "deprecated": false, "trackAdoption": false @@ -478,7 +552,9 @@ "type": "number", "tags": [], "label": "instanceSizeMb", - "description": [], + "description": [ + "\nThe size of the instance in which Kibana is running. Only available when running on Elastic Cloud." + ], "signature": [ "number | undefined" ], @@ -486,13 +562,47 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "cloud", + "id": "def-server.CloudSetup.trialEndDate", + "type": "Object", + "tags": [], + "label": "trialEndDate", + "description": [ + "\nWhen the Cloud Trial ends/ended for the organization that owns this deployment. Only available when running on Elastic Cloud." + ], + "signature": [ + "Date | undefined" + ], + "path": "x-pack/plugins/cloud/server/plugin.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "cloud", + "id": "def-server.CloudSetup.isElasticStaffOwned", + "type": "CompoundType", + "tags": [], + "label": "isElasticStaffOwned", + "description": [ + "\n`true` if the Elastic Cloud organization that owns this deployment is owned by an Elastician. Only available when running on Elastic Cloud." + ], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/cloud/server/plugin.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "cloud", "id": "def-server.CloudSetup.apm", "type": "Object", "tags": [], "label": "apm", - "description": [], + "description": [ + "\nAPM configuration keys." + ], "signature": [ "{ url?: string | undefined; secretToken?: string | undefined; }" ], diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index ac208319e2e02..d857e34b3b9e2 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 34 | 0 | 26 | 0 | +| 39 | 0 | 11 | 0 | ## Client diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 9dada4c4dd04e..dbcb36118fde6 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 3d656220231ff..4be455a506123 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 7c4195e373d96..0a9b7d2375dab 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index eef60631d7706..b51c199d10cd5 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index f475769e816dc..601df2f5255ab 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/core.mdx b/api_docs/core.mdx index a2435d5368b9b..2ec3f0a7262ec 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github description: API docs for the core plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] --- import coreObj from './core.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 141e578ac4fc7..b0c5dab3f98fa 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.devdocs.json b/api_docs/dashboard.devdocs.json index c185507fd9101..a4309f565cf9f 100644 --- a/api_docs/dashboard.devdocs.json +++ b/api_docs/dashboard.devdocs.json @@ -288,6 +288,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardContainerInput.syncCursor", + "type": "CompoundType", + "tags": [], + "label": "syncCursor", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/dashboard/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "dashboard", "id": "def-public.DashboardContainerInput.viewMode", diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 4298c36f86195..8b7de334deda8 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 120 | 0 | 113 | 3 | +| 121 | 0 | 114 | 3 | ## Client diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index b92690c335ea4..369c9dc006fdb 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 6bc8d66273ed2..d49e7e4e2c3d8 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 13aa66ad4c1bb..23441aa926576 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index afe45496b9cc4..925a4fbb81958 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 9b182a62c9bba..7917daf25e25b 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 7618ea5a3857d..d34da2a7332b8 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 88419f85fef70..e94f7afe6585c 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index ea1a50769ae6f..df78232d00397 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 589d097fd6ec3..9a764354b95ee 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 294b8baa0a135..5b3d01cb4eae2 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 77d7f9b7f82e7..f12a4c2d6b469 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -751,8 +751,8 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts#:~:text=options) | - | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 18 more | - | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title), [validators.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx#:~:text=title)+ 4 more | - | -| | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [list.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts#:~:text=mode), [response_actions.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts#:~:text=mode)+ 3 more | 8.8.0 | -| | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [list.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts#:~:text=mode), [response_actions.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts#:~:text=mode)+ 3 more | 8.8.0 | +| | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 5 more | 8.8.0 | +| | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 5 more | 8.8.0 | | | [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts#:~:text=license%24) | 8.8.0 | | | [request_context_factory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [request_context_factory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [create_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts#:~:text=authc), [delete_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts#:~:text=authc), [finalize_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts#:~:text=authc), [open_close_signals_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts#:~:text=authc), [preview_rules_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts#:~:text=authc), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts#:~:text=authc) | - | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/index.tsx#:~:text=onAppLeave), [plugin.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/plugin.tsx#:~:text=onAppLeave) | 8.8.0 | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 051dd9119b418..4aeef7cf6dc85 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -163,8 +163,8 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| -| securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [list.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts#:~:text=mode), [response_actions.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts#:~:text=mode)+ 3 more | 8.8.0 | -| securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [list.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.test.ts#:~:text=mode), [response_actions.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts#:~:text=mode)+ 3 more | 8.8.0 | +| securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 5 more | 8.8.0 | +| securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 5 more | 8.8.0 | | securitySolution | | [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts#:~:text=license%24) | 8.8.0 | | securitySolution | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/index.tsx#:~:text=onAppLeave), [plugin.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/plugin.tsx#:~:text=onAppLeave) | 8.8.0 | | securitySolution | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx#:~:text=AppLeaveHandler), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx#:~:text=AppLeaveHandler), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/types.ts#:~:text=AppLeaveHandler), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/types.ts#:~:text=AppLeaveHandler), [routes.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/routes.tsx#:~:text=AppLeaveHandler), [routes.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/routes.tsx#:~:text=AppLeaveHandler), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=AppLeaveHandler), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=AppLeaveHandler), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=AppLeaveHandler), [use_timeline_save_prompt.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/hooks/timeline/use_timeline_save_prompt.ts#:~:text=AppLeaveHandler)+ 1 more | 8.8.0 | diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index d475502a928d2..ef1cdd9b4d903 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 0987eb3d36c0d..0010b677e5e12 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index b9258ccd6cff9..f117d9d140edb 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.devdocs.json b/api_docs/embeddable.devdocs.json index 20bcffaab5464..21a6b55795244 100644 --- a/api_docs/embeddable.devdocs.json +++ b/api_docs/embeddable.devdocs.json @@ -9750,7 +9750,7 @@ }, " | undefined; title?: string | undefined; id: string; lastReloadRequestTime?: number | undefined; hidePanelTitles?: boolean | undefined; enhancements?: ", "SerializableRecord", - " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; syncTooltips?: boolean | undefined; executionContext?: ", + " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; syncCursor?: boolean | undefined; syncTooltips?: boolean | undefined; executionContext?: ", "KibanaExecutionContext", " | undefined; }" ], @@ -11328,7 +11328,7 @@ }, " | undefined; title?: string | undefined; id: string; lastReloadRequestTime?: number | undefined; hidePanelTitles?: boolean | undefined; enhancements?: ", "SerializableRecord", - " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; syncTooltips?: boolean | undefined; executionContext?: ", + " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; syncCursor?: boolean | undefined; syncTooltips?: boolean | undefined; executionContext?: ", "KibanaExecutionContext", " | undefined; }" ], diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index d87fd71d4813b..1c24dc1085f49 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index d0efc7d058262..f194611543bbe 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index fb272c39b3337..dc36561519d20 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 6911b0f2b73c9..9a499860128c3 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 7c24943a52254..25830dc3d3aad 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 46c72c5979803..bcdc6a18969af 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 56f2a42cafcd9..4a8ab4c9d8582 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index bd779d168e848..e94fab0683d54 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index fb3222340a00b..66ff63b20ef9c 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.devdocs.json b/api_docs/expression_heatmap.devdocs.json index 4e53b8fde2123..101973f84ca4d 100644 --- a/api_docs/expression_heatmap.devdocs.json +++ b/api_docs/expression_heatmap.devdocs.json @@ -447,6 +447,17 @@ "path": "src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "expressionHeatmap", + "id": "def-common.HeatmapExpressionProps.syncCursor", + "type": "boolean", + "tags": [], + "label": "syncCursor", + "description": [], + "path": "src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -620,7 +631,7 @@ "section": "def-public.PersistedState", "text": "PersistedState" }, - "; interactive: boolean; syncTooltips: boolean; renderComplete: () => void; }" + "; interactive: boolean; syncTooltips: boolean; syncCursor: boolean; renderComplete: () => void; }" ], "path": "src/plugins/chart_expressions/expression_heatmap/common/types/expression_renderers.ts", "deprecated": false, diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 17065861981cd..4204207ac8d9b 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 106 | 0 | 102 | 3 | +| 107 | 0 | 103 | 3 | ## Common diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 9933dd2032597..979751224d794 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index 9de9393a41450..061799006b8bd 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 75bd75b868b0e..7b900f28b3f2f 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 961ffc0eecfd3..24d8613e4804b 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 4aca4e034dfdb..ad2cff13b95ca 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index c447ec5bc621e..e9bff50c4d86a 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 85ef888dd171d..2c1e776d19ead 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 3a310b469bf9a..910cb27bdb971 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 4d815ed16bb29..b5e97db3ffe59 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.devdocs.json b/api_docs/expression_x_y.devdocs.json index 9378c2a614083..1ed51d6360c13 100644 --- a/api_docs/expression_x_y.devdocs.json +++ b/api_docs/expression_x_y.devdocs.json @@ -1803,6 +1803,17 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "expressionXY", + "id": "def-common.XYChartProps.syncCursor", + "type": "boolean", + "tags": [], + "label": "syncCursor", + "description": [], + "path": "src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "expressionXY", "id": "def-common.XYChartProps.syncColors", diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index dec05d77bd487..705f353388259 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 158 | 0 | 148 | 9 | +| 159 | 0 | 149 | 9 | ## Client diff --git a/api_docs/expressions.devdocs.json b/api_docs/expressions.devdocs.json index dfc68e39dd9c9..651bca99eca7c 100644 --- a/api_docs/expressions.devdocs.json +++ b/api_docs/expressions.devdocs.json @@ -3716,7 +3716,7 @@ "id": "def-public.ExpressionRenderHandler.Unnamed.$2", "type": "Object", "tags": [], - "label": "{\n onRenderError,\n renderMode,\n syncColors,\n syncTooltips,\n interactive,\n hasCompatibleActions = async () => false,\n executionContext,\n }", + "label": "{\n onRenderError,\n renderMode,\n syncColors,\n syncTooltips,\n syncCursor,\n interactive,\n hasCompatibleActions = async () => false,\n executionContext,\n }", "description": [], "signature": [ "ExpressionRenderHandlerParams" @@ -7030,6 +7030,24 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "expressions", + "id": "def-public.ExecutionContext.isSyncCursorEnabled", + "type": "Function", + "tags": [], + "label": "isSyncCursorEnabled", + "description": [ + "\nReturns the state (true|false) of the sync cursor across panels switch." + ], + "signature": [ + "(() => boolean) | undefined" + ], + "path": "src/plugins/expressions/common/execution/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "expressions", "id": "def-public.ExecutionContext.isSyncTooltipsEnabled", @@ -10548,6 +10566,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "expressions", + "id": "def-public.IExpressionLoaderParams.syncCursor", + "type": "CompoundType", + "tags": [], + "label": "syncCursor", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/expressions/public/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "expressions", "id": "def-public.IExpressionLoaderParams.syncTooltips", @@ -10909,6 +10941,22 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "expressions", + "id": "def-public.IInterpreterRenderHandlers.isSyncCursorEnabled", + "type": "Function", + "tags": [], + "label": "isSyncCursorEnabled", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/expressions/common/expression_renderers/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "expressions", "id": "def-public.IInterpreterRenderHandlers.isSyncTooltipsEnabled", @@ -18009,6 +18057,24 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "expressions", + "id": "def-server.ExecutionContext.isSyncCursorEnabled", + "type": "Function", + "tags": [], + "label": "isSyncCursorEnabled", + "description": [ + "\nReturns the state (true|false) of the sync cursor across panels switch." + ], + "signature": [ + "(() => boolean) | undefined" + ], + "path": "src/plugins/expressions/common/execution/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "expressions", "id": "def-server.ExecutionContext.isSyncTooltipsEnabled", @@ -20583,6 +20649,22 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "expressions", + "id": "def-server.IInterpreterRenderHandlers.isSyncCursorEnabled", + "type": "Function", + "tags": [], + "label": "isSyncCursorEnabled", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/expressions/common/expression_renderers/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "expressions", "id": "def-server.IInterpreterRenderHandlers.isSyncTooltipsEnabled", @@ -29658,6 +29740,24 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "expressions", + "id": "def-common.ExecutionContext.isSyncCursorEnabled", + "type": "Function", + "tags": [], + "label": "isSyncCursorEnabled", + "description": [ + "\nReturns the state (true|false) of the sync cursor across panels switch." + ], + "signature": [ + "(() => boolean) | undefined" + ], + "path": "src/plugins/expressions/common/execution/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "expressions", "id": "def-common.ExecutionContext.isSyncTooltipsEnabled", @@ -31278,6 +31378,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "expressions", + "id": "def-common.ExpressionExecutionParams.syncCursor", + "type": "CompoundType", + "tags": [], + "label": "syncCursor", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/expressions/common/service/expressions_services.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "expressions", "id": "def-common.ExpressionExecutionParams.syncTooltips", @@ -34365,6 +34479,22 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "expressions", + "id": "def-common.IInterpreterRenderHandlers.isSyncCursorEnabled", + "type": "Function", + "tags": [], + "label": "isSyncCursorEnabled", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/expressions/common/expression_renderers/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "expressions", "id": "def-common.IInterpreterRenderHandlers.isSyncTooltipsEnabled", diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index d1960b4de9b68..76c55a4b58037 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2183 | 17 | 1729 | 5 | +| 2191 | 17 | 1734 | 5 | ## Client diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 1133028869eba..da91f227af390 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 9c94a44ffadec..4523ead38b58b 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 9a9be7582ce9d..c0294e50ed582 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 76efd6cfb977f..d693b45a531aa 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index e58a20c5ee983..91ef5d2772ce2 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index c4c9a924f15bf..b989d9bad1e3b 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index e8b5280d0e2ea..8418d55c8ff85 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 246f650f020c1..4577b0f4e88dc 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 91cf3a7836f2c..6ecaeb0006d0d 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 7ee6cf44735d6..977ecfc0766f0 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 63e5474c9b3d2..c0dd3767e353d 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 75f7a54eb7483..49689351b17a9 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 4858834afb756..00352cab42857 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 6d7f73bfc8ad2..bdc594ed8725d 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index f03a1a6fa8165..908564cfc641c 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 0926dc4e419a0..1da5becf768bc 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 8ddcbeec02f9d..dd409bf5386d3 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index c491faf53cb8a..e0b8f3b9ff23f 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 59632c1d3a87d..bcc3a708b31e4 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index c9d7993a4df3d..1d8ae3a0f5c67 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index e5ca859ca6591..03683ffc81f4e 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index a49dcd7d53de9..db2e6330164d6 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index a112cef1f9ca5..f90ef47a7726b 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 14890706f4c7f..ec38943ba6b25 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 3d722227a9569..4a82106315395 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index d748be38bbb6c..f8b1e8255cbf6 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index 2775628c8168e..5caf2591aac24 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 2af523b0462f1..227a50bcba7e0 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index f43771e589f42..4050ce85968e8 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 2589aa244318f..f0d4176e905e2 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index f3aa9c51bc4b6..1a43a15587640 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index 939b2b88a6dc7..0a3d0a062b9ce 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 22bac5e77de6c..2e6937258bb59 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 8605fb4c27b84..a8a928b930d9f 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 7a386f010b42a..0b31a57545615 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index fb0e631dd6eeb..a6d305f92217c 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 9ed72653f94f9..7422e5e1610ad 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index c9f1bd5d5b1d1..636fc1a5bebae 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 39ca75ebdd662..406277b8006bd 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 65cbd40888647..5fbb43e9dae4e 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index a3833a17de3a6..a0db33ee20281 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 7979c5768713c..0dc8b453b2a6c 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index f1a06f92e296b..0bf53e2e5bae6 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index a28e66b152326..ff84d9fdc0503 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index 3a2f44bf1236c..44c8aef5c9217 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 54dc5fd943387..17e0aa8eab477 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index 4da9fffc75e8d..80b08fb8aba32 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 9aba2e6f2cfc8..cee02c9f7f6a3 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 7e5763b66db1e..c7710532f6c47 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 9090ea9b3dae4..238ce502f0c73 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 64ab097717304..92725a0d92687 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index a5f5aafb2ebc8..5582bcad413a2 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 4ad262feb7719..761e2dfe0b72b 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 606a1cb2b6a53..36c69cac69a0b 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 286c992389983..3c67cb607a218 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index cd2e9cc26d9d4..5fe6f62ef2929 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index 0d172a3829c94..76f0c66049174 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index 0435ca4ce5d88..eb3b83aac9b93 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index 5b604e8925887..3c64a388200ad 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index a94a9afd70dd9..d54cea6fe899a 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 8be5b7c8766b1..4d11c278bce63 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index d2829bd75d504..ced77f8debe79 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index 71770dfa6107a..063255c448d40 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index c242260d3ae9b..a163964a8177b 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 510745d909593..bbe5c1a7fb764 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 104b009e8e132..d0436a80628cb 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index 88179af913545..74daa6b5a6aa2 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index e9f05e11af078..7134379ff167b 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index a8c8ffd2b89d8..805eb5e20cd76 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 3e7261b11c4f9..3089353641990 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index cf478de4599fd..c080da192d657 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index 7171de6bc8257..5d760dc722338 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 1bbcbab54b3e5..0b033c3bf5a66 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 74c16cef96d37..a27ad888e8174 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index ee57062a1d770..076d1af298978 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index ca954ee656c72..13038f04466b3 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index 686820b846c17..c3d8002cfde02 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index b371dc2ce6af3..9cdf49d635f1e 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index 37e63190c4e6f..f8c2c116a9848 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index 74ec3c2068ff3..c2f9f2219c782 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index 312fa33dea490..756f1ec2a5301 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index 09069175efcf2..c8e2693b97d0c 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 58af58b774bce..7a6b67f42ad3c 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index cd1c895ca79b4..82843d60e8a53 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index e6ac9fe853ede..b2c5d33b0964d 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 3b53f4ec9ab89..c79087990b7b0 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 05ab2714fde30..f8f2013e5beca 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 72033231812ec..36a0e5247908b 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index 6e2fca747fbd4..889942cb35d08 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index 3237999e37792..073d42a71ed8b 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index 8e60ae8d13efc..75127c646b101 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 1557cc41e93cb..a868e5f82a6b1 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index a732c7b902574..76ac625a59b11 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index 6d8dc337edd5c..3edf68eafe99d 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index 21895582deb70..f707ec89e17b6 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index 16426662fd83e..dc669daf91f77 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index cff15361430a5..3230a8e2bbaf1 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 3eca728a9128b..c56673e06cef2 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 7a0c2c59d2491..93bf87b306494 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index ac410e083924a..639d71c2d4594 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index 97b824680cc3f..e1c52253dc8d1 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index d9e44f8020314..8aaa5fa4ba07b 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index aa5c36f61d8fd..2f876d13c3e17 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 392a6d628e97d..75217cafa2f59 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index b9dbfbc447c03..497a726e140a0 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index 2d7375a3c7220..f534e0f28892b 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index fe3d8caa47c0f..d9383302da10c 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index 118ae484900d1..61e50ad61f5c4 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] --- import kbnCoreInjectedMetadataBrowserObj from './kbn_core_injected_metadata_browser.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 7bb0bc7a55924..ee66f0bfb4093 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 9058034fe6ed4..4a0c223679ec8 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index 4129772acbce5..f9163546e7a0c 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index e1bc41b0b1fd0..93e16f5b7d558 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index a351162b92535..e2c122cf80066 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 80c67cbefb41d..0157435c53146 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index 81c79f199e296..7a1162cc2155c 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index 6cea0b3edb58b..4397611b84ba8 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 9528c7f087d4f..22bdcae2b8cea 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index 1fc1b5edfd4a9..6bdc621187c53 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index 95ffc3ff48c8a..4c77c0c54e9e9 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index 596dfbfa9324f..0dd7400ad6ad5 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 0f49932a294e6..58ad84604a65f 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 8685b3dd8ffae..eeff94632ae1a 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index 1b3a50a65001a..d4c79e0e6c215 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index a44aed5fb0c40..e82bbd8389273 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index 5401b2238c84e..9db8ef993ee7c 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index 8234e1c7699de..7245bd212d9f8 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index a254a39f50976..9bfa0261e6bf6 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index 70fa1bdac381c..65ae87a11014b 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index 8efcbe04147f2..c1d3dd3c41d4a 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index ae13deb6ab4a7..63d290d66b778 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 329a751558828..db4dbc041d411 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index b453f6bb2b57d..3b2067d95d659 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 426df918fb1c2..1ee60ba77c41f 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 267d37656f2d3..7b577e691950b 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index bdeef2478073c..ca3435f2f4d52 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index c8a131ca1c925..ed940e5483a3e 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 7052188237271..fe735e6c229f0 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index e29ec5c5f28cc..f54ad738ed7c1 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index bd9522fb6c16a..964a4762c74a3 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 166b281a1f819..21e6e4f1d3397 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index 62a295a989748..312b838c9d7e1 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index b9b12f139a291..c608f300ad8e8 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index b9e17b56c7215..cb1d574e0a58c 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index c2d75d32460f5..62baf4d2d88d2 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index eeeab2680fc91..a691eb782e811 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index 3b53b418fde0e..c10f9d65a6f71 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 7cd60fefcbed7..d574624b37f94 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 9bd5d2601c628..9525ea5d597d4 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index cb6aaa1892628..ff270c9571cce 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 997cbbbeee0ce..95ab433b8afb8 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index e8a96d95e0b52..0f3f1085f8c8a 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index ebb8890436531..a60490350a3ed 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 4e2e211290a68..e3fe63104244e 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 67ee330bf497e..0085619324578 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 12a6ae1b41c50..cab88970ed077 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index 087f9bc2d6a84..76088f8aeed0a 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index b3ecdb125a1d8..fc5aed267b9f5 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 8920b923837be..2ec7dcbe4956b 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index 4f8ee81478644..2484910018514 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index 335fed85de068..b8c9e6c5557f1 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index 9d39db8a256a8..4d6a9f502357d 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index 8c860cc0ed201..0427e329d1011 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index ddae9cdc8613e..5cde88b3919e5 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 2590679c346ab..a5ea5d26f044f 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index e3e829c62749f..14efca2695a96 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index 3aa81b6989235..5d8b628b0a600 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 93324103d7a72..5b768628e4650 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index dda5842f987e7..ed7469f8e97c8 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index 2c23e50f95bce..290f19d278b55 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 6241b69a38c3d..340a6e14cf577 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index 083c99ce18e9a..87365ac46488a 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 625e58764a98b..6bae1f5ae6d5f 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 5f38b9ffbe2f8..b112d2b576340 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index 1a69a645a027b..6b8100a9b589f 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index e919b4aba9c51..78cec0e593b9c 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index e8913afa81c88..bfd62a7a6772c 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index c17dff7ee239c..2045467a8672e 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 6b9b7c47f66e3..7597dae0e37a3 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index dcbdcc38dfdb9..640ae9607d3b3 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 6fd8bc19e7e34..ac7d9d3c614be 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index bfc5286c8483b..ad7e34dec65b2 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index 61702adbf85ec..59ba10b04be8b 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 5f4151126ff61..9dad230ca2bca 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index ed341b7fd0d3d..64208ee04c44c 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index dd99724236b57..e05772209caab 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 3ab75edf78427..a63c738302b0c 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index b8335686424f3..c7da7cfd52a0e 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 8c5befe03b359..72f9610185bf1 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 605bd1db95430..e3cf0863d2510 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 77f67be571740..6113655f9c767 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 0e63700233d85..7509afb9b1ae7 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index b2b1fa910efbc..a3bdf0e9100c6 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 9eb7c2fa89f3d..ee220a2445b3b 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 7a3a034993af1..c7cbab7cefb71 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 7461e140b1517..4e48fd41674ad 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index 0d2aac1746d29..d19d29035f88e 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_get_repo_files.mdx b/api_docs/kbn_get_repo_files.mdx index 232fe900144ad..6e19b9402bd68 100644 --- a/api_docs/kbn_get_repo_files.mdx +++ b/api_docs/kbn_get_repo_files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-get-repo-files title: "@kbn/get-repo-files" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/get-repo-files plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/get-repo-files'] --- import kbnGetRepoFilesObj from './kbn_get_repo_files.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 703cc640f9c98..927a9e35f8957 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index ab550d01cd4aa..9a908f0df623b 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index f1a4c6333160c..25b904c350aa1 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index 36bdaf020341d..0abeb8ab57c4a 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 9087b92760bf5..9fd9e68ba6614 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 62ce7e6a00a7a..68998444485e1 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index efc7e6717383e..785e8a58f6c04 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 6dde0c81a8b68..cba1aba4b8260 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 2b4e5883950e5..a7e679d18370c 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 4f66f7786e798..aa05bd60ae18a 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index ebb98ebacd44e..9449b5bac59f1 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 3d98ca7e00e92..f31a8eb027cc1 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index b1ca1bbb247b0..46ca6b388ba41 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 5d97e59fb4e81..840054b21257a 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index cc57923957615..3fe4d045d41dc 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 6a3a6ac4ef228..1a74dd508a738 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index aa2a7822c83c8..a868d5020cc5b 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index 6131d1fcfce58..46f75db960ad2 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index b9a9606fa03d4..44687276f41fa 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 38886f7dae121..8d823a70a689c 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index 07979c2656cfc..9acce86ac28e4 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index d369e769b38d7..19564c3aa8fa9 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 8e3e91f1da134..4ac66fcf327e1 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index 237f0ef3a9024..49df6b1ff2c4f 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 8a0d54de39844..76f6d207b5225 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index d59e901750fc4..40bc459ae63ca 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index e990105e75989..9c20eddc8a232 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 1fb1dc646b831..babc0d4c61f6b 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 978b07b6bee0e..abfec62f04384 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index fbc0cca8a3a8e..75628505a8ea4 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 04d20b1704bd8..57e3ae6b223c9 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 6fd18e8011f86..0124e1b38a152 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index a8d86cfca2cfb..b8f39e86bdcba 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 584949a0b23a4..576e5be73b87e 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 88637f8423878..72684cd28d9e1 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index d38b83e89fcde..d5dc67c8edc67 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index e90e1e8c97e4a..f87f523d0b56a 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index fa5fe444a0276..f631ac324d1d0 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index 41afd4e5fb4a0..9b0ae5c234b6a 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index a2837efbd4207..da5e941152a47 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index d4a8fc67beabd..62cfab54a06f5 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index 87addb64d93e3..987efffb0f8ae 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 056b09bbb85cc..539d5ae582431 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index 7ce61f34b4ab0..08c4ac78e3adb 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index cd60083336f7c..42c785435aef8 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index 7d3de8e112faa..44e738d077c1f 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index c45e31b93fdb3..b1bbaa1796285 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index ffe79ed31b8f7..d9f38a49f975e 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 5ff8fb8885e7a..1e3bee65e9308 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index 05d762a44ace5..7f4b52c6d311e 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 642072da18d1a..c4fb97f8e4fa5 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 91cb4417726a8..c317c380e6cd1 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index b915b9a086d46..b1f886993474c 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index f8ebff6ff0f35..40b4f6a029d7f 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 1f0ccc59a50e5..33ceaf49ecf75 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index a5b61d014aca9..a8c4b1e864210 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 3eeb2b8a1b4b7..b304f30777f3f 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index b78439042d011..eeaf3ec3f430c 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 05e8f0fda3186..8eb1a4ff07c84 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index 3c628af3d29db..fad9139f25596 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 180b30d640a55..c3099a7a51e10 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index 67f2dcbf87d17..e7851ad78bed4 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index 28fe4be1e9315..4433d4a820ef5 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index cd2bfe2727e29..6ea91ef49a072 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index b50866740f17e..c62dbdd1e83dc 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index d7e73919195c7..5185e64ae96c3 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index c4b979f0bf020..26d969e8de8e4 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 2fffdca031d68..406b564c37741 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index 4f7505aa18f19..c42da58b607db 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index ef10794ec07c7..1e2b1a10306d2 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index 4e445695ce043..c6cc3b55f1c9e 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_sort_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index 619317adad6ab..60ce680b77644 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-package-json plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] --- import kbnSortPackageJsonObj from './kbn_sort_package_json.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index 7c82fe28b9ef8..e1d98bcd2fb53 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index af25ac211f92c..621e5f1b4c3e5 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 02b67f55c2d11..04c1cde8832ca 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index a7e94ddd6bf9f..6b19c47dc1d47 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 520a84d725f50..cc85d61d46df9 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index e57378a0bdd1b..e8d9308bf5997 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index 5848474c98abb..436e8bcce963b 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 34593f5e4de31..6c2a1c773c283 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index 28ff056daa4d8..901c92404826c 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] --- import kbnTypeSummarizerObj from './kbn_type_summarizer.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer_core.mdx b/api_docs/kbn_type_summarizer_core.mdx index c89a6bfd01217..d687ab10cb7a7 100644 --- a/api_docs/kbn_type_summarizer_core.mdx +++ b/api_docs/kbn_type_summarizer_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer-core title: "@kbn/type-summarizer-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer-core plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer-core'] --- import kbnTypeSummarizerCoreObj from './kbn_type_summarizer_core.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index bb4d7a58c835d..98b6e73944f7a 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 836008a78b41d..725211e81d8a2 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 808ad6744bf6a..00ad67e879354 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 0953b950f3934..4467d33f0b409 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index b30a7490bcf3c..1e80c86ce5a31 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index 7df2fce140900..ae0a544b7695f 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 2be9c46589362..7c8551ee4fc49 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 62bce0880f9f2..a2615f303483e 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 811a1eeae8e88..f5c420e0b3613 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 695c59da8b2dd..6b5285b485392 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index 3108f06e328c4..5750c3e7538c2 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.devdocs.json b/api_docs/lens.devdocs.json index 285035a4d158d..d6d05446175c1 100644 --- a/api_docs/lens.devdocs.json +++ b/api_docs/lens.devdocs.json @@ -7219,6 +7219,17 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "lens", + "id": "def-public.XYChartProps.syncCursor", + "type": "boolean", + "tags": [], + "label": "syncCursor", + "description": [], + "path": "src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "lens", "id": "def-public.XYChartProps.syncColors", diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 705e405628d63..b6dbf8edb6768 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 658 | 0 | 567 | 45 | +| 659 | 0 | 568 | 45 | ## Client diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 46fdabc8e4ce4..b4dd33b05d466 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index f03ac62ef69d4..af33991449df5 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.devdocs.json b/api_docs/licensing.devdocs.json index 335c7891795d3..6b2d792de9c75 100644 --- a/api_docs/licensing.devdocs.json +++ b/api_docs/licensing.devdocs.json @@ -574,6 +574,14 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts" @@ -1839,6 +1847,14 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts" diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 853896a42382a..a7707f7a483a7 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index ba4d095fe0776..c39127de230b9 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 32fbb57687eba..ff717f629fb87 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index 379267cafc4e9..6843462849616 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 10e21a471b952..d99496f67b331 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index 11aa6c1abad09..c3aa02065f24c 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index fc7fd5cb50030..e74ff2665332c 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 5713e1d3651b5..985a05b1bd793 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index e06ca6ebfca3d..d1a12bb52a8c4 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 641b1c39a2230..42652b4f681ec 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index 4a4216ec894ca..c088a28c032a2 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -8054,7 +8054,7 @@ "label": "ObservabilityConfig", "description": [], "signature": [ - "{ readonly unsafe: Readonly<{} & { slo: Readonly<{} & { enabled: boolean; }>; alertDetails: Readonly<{} & { enabled: boolean; }>; }>; readonly annotations: Readonly<{} & { enabled: boolean; index: string; }>; }" + "{ readonly unsafe: Readonly<{} & { slo: Readonly<{} & { enabled: boolean; }>; alertDetails: Readonly<{} & { apm: Readonly<{} & { enabled: boolean; }>; metrics: Readonly<{} & { enabled: boolean; }>; logs: Readonly<{} & { enabled: boolean; }>; uptime: Readonly<{} & { enabled: boolean; }>; }>; }>; readonly annotations: Readonly<{} & { enabled: boolean; index: string; }>; }" ], "path": "x-pack/plugins/observability/server/index.ts", "deprecated": false, diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 11f000f7d7608..aca18e6aa58ce 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 9fb829dcd0133..dab4536bed192 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 6e41a12b3f107..11a1a79ea8ab8 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,29 +15,29 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
          public API | Number of teams | |--------------|----------|------------------------| -| 488 | 405 | 38 | +| 489 | 406 | 38 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 32298 | 179 | 21765 | 1023 | +| 32374 | 179 | 21791 | 1023 | ## Plugin Directory | Plugin name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
          comments | Missing
          exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 213 | 0 | 208 | 23 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 214 | 0 | 209 | 23 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 1 | 32 | 2 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 9 | 0 | 0 | 2 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 381 | 0 | 372 | 24 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 382 | 0 | 373 | 24 | | | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 38 | 0 | 38 | 52 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 81 | 1 | 72 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Canvas application to Kibana | 9 | 0 | 8 | 3 | | | [ResponseOps](https://github.com/orgs/elastic/teams/response-ops) | The Case management system in Kibana | 87 | 0 | 70 | 28 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 264 | 2 | 249 | 9 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 34 | 0 | 26 | 0 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 39 | 0 | 11 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | Chat available on Elastic Cloud deployments for quicker assistance. | 1 | 0 | 0 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/@elastic/kibana-core) | Provides the necessary APIs to implement A/B testing scenarios, fetching the variations in configuration and reporting back metrics to track conversion rates of the experiments. | 12 | 0 | 0 | 0 | | cloudFullStory | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | When Kibana runs on Elastic Cloud, this plugin registers FullStory as a shipper for telemetry. | 0 | 0 | 0 | 0 | @@ -48,7 +48,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2691 | 0 | 23 | 0 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 107 | 0 | 88 | 1 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 120 | 0 | 113 | 3 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 121 | 0 | 114 | 3 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 52 | 0 | 51 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3228 | 33 | 2516 | 24 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | This plugin provides the ability to create data views via a modal flyout inside Kibana apps | 16 | 0 | 7 | 0 | @@ -68,7 +68,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 106 | 0 | 106 | 10 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'error' renderer to expressions | 17 | 0 | 15 | 2 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Gauge plugin adds a `gauge` renderer and function to the expression plugin. The renderer will display the `gauge` chart. | 58 | 0 | 58 | 2 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart. | 106 | 0 | 102 | 3 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart. | 107 | 0 | 103 | 3 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'image' function and renderer to expressions | 26 | 0 | 26 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Adds a `metric` renderer and function to the expression plugin. The renderer will display the `legacy metric` chart. | 49 | 0 | 49 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'metric' function and renderer to expressions | 32 | 0 | 27 | 0 | @@ -78,8 +78,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'revealImage' function and renderer to expressions | 14 | 0 | 14 | 3 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'shape' function and renderer to expressions | 148 | 0 | 146 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression Tagcloud plugin adds a `tagcloud` renderer and function to the expression plugin. The renderer will display the `Wordcloud` chart. | 7 | 0 | 7 | 0 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart. | 158 | 0 | 148 | 9 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds expression runtime to Kibana | 2183 | 17 | 1729 | 5 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart. | 159 | 0 | 149 | 9 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds expression runtime to Kibana | 2191 | 17 | 1734 | 5 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 222 | 0 | 95 | 2 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Index pattern fields and ambiguous values formatters | 288 | 5 | 249 | 3 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 62 | 0 | 62 | 2 | @@ -104,7 +104,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | kibanaUsageCollection | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 615 | 3 | 418 | 9 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 3 | 0 | 3 | 1 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 658 | 0 | 567 | 45 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 659 | 0 | 568 | 45 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 3 | 0 | 3 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | @@ -140,7 +140,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 249 | 0 | 90 | 0 | | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 55 | 0 | 54 | 23 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 7 | 0 | 7 | 1 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 114 | 0 | 55 | 10 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 115 | 0 | 56 | 10 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 22 | 1 | 22 | 1 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories. | 260 | 0 | 64 | 0 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 4 | 0 | 4 | 0 | @@ -159,6 +159,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 133 | 0 | 92 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 206 | 0 | 142 | 9 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 122 | 0 | 117 | 2 | +| | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. | 56 | 0 | 29 | 0 | | | [Unified Search](https://github.com/orgs/elastic/teams/kibana-app-services) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 131 | 2 | 104 | 17 | | upgradeAssistant | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | urlDrilldown | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds drilldown implementations to Kibana | 0 | 0 | 0 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index 2021d52d5b724..329f51e250d65 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index bd58024c8b8cf..0d9a7fc571f27 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index b8dfe036b8982..ae21bc28c6bf8 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index 9d4b798faf065..37e12a3b07a9e 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index f378fa5d64b7b..4719a3c46b623 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index bd7cfb20966d9..bf157129468a4 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 92337b2109f81..35353d132c955 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 8a73ba883619c..e00e0f7d9a6fd 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index 73ab7a4c9b93c..4ba0cc0ec3648 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index c1f7ee5147a7a..36af0dc877c5e 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 68f25f69466be..39ee9f7685854 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 9306b6f7fb5f0..ecd0f9604f7f1 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index eba3c945abc73..2769ab8cf6979 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index dc468637b642e..ec3131cb16134 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 3eb523a6b9ae3..e1961cfb6925f 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 7ca02cce627c9..a064cfbe7809c 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 24c4b236600d9..fbc1a6e18a359 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 4df85c4373c03..cd86553b2fdd2 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.devdocs.json b/api_docs/share.devdocs.json index 12c7ffc463d84..67996465a603c 100644 --- a/api_docs/share.devdocs.json +++ b/api_docs/share.devdocs.json @@ -1194,6 +1194,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "share", + "id": "def-public.ShowShareMenuOptions.snapshotShareWarning", + "type": "string", + "tags": [], + "label": "snapshotShareWarning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/share/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "share", "id": "def-public.ShowShareMenuOptions.onClose", diff --git a/api_docs/share.mdx b/api_docs/share.mdx index c45c800a370a1..c30d5ac7213a7 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 114 | 0 | 55 | 10 | +| 115 | 0 | 56 | 10 | ## Client diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 0bedbd3ce8b4d..846371e932f14 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 32294e6a8ac26..1e28ebf3f6c31 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index cfdb1f16ae452..3d7d66d817877 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index 38beb100ce9c7..36eb243de2cb7 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 6b05776dfe8d8..477dfacb56064 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index c7cbb53358e47..080140243e825 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index d4449150f82bc..44e1153ac923f 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 8ea2af4967e32..c76c8cd5fb369 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index bf761c3ba0bdd..7bc04081f6e9d 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index c9e7be99354d8..34fe439836dea 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 96864aac0ddf5..a0b552640a010 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index 25c741ee70bf4..e38e33c757aaa 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 43b75995f580f..30c7e4602c598 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 82cf55203c9c8..d1908f8d512fd 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 1713f83d8f967..59ba7eadc46a1 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index efd048ac0e36e..d30517b185e1b 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_histogram.devdocs.json b/api_docs/unified_histogram.devdocs.json new file mode 100644 index 0000000000000..ae17302e9ef1e --- /dev/null +++ b/api_docs/unified_histogram.devdocs.json @@ -0,0 +1,1085 @@ +{ + "id": "unifiedHistogram", + "client": { + "classes": [], + "functions": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.buildChartData", + "type": "Function", + "tags": [], + "label": "buildChartData", + "description": [ + "\nConvert the response from the chart request into a format that can be used\nby the unified histogram chart. The returned object should be used to update\n{@link UnifiedHistogramChartContext.bucketInterval} and {@link UnifiedHistogramChartContext.data}." + ], + "signature": [ + "({ data, dataView, timeInterval, response, }: { data: ", + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + }, + "; dataView: ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + }, + "; timeInterval?: string | undefined; response?: ", + "SearchResponse", + "> | undefined; }) => { bucketInterval?: undefined; chartData?: undefined; } | { bucketInterval: ", + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramBucketInterval", + "text": "UnifiedHistogramBucketInterval" + }, + " | undefined; chartData: ", + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramChartData", + "text": "UnifiedHistogramChartData" + }, + "; }" + ], + "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.buildChartData.$1", + "type": "Object", + "tags": [], + "label": "{\n data,\n dataView,\n timeInterval,\n response,\n}", + "description": [], + "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.buildChartData.$1.data", + "type": "Object", + "tags": [], + "label": "data", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + } + ], + "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.buildChartData.$1.dataView", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.buildChartData.$1.timeInterval", + "type": "string", + "tags": [], + "label": "timeInterval", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.buildChartData.$1.response", + "type": "Object", + "tags": [], + "label": "response", + "description": [], + "signature": [ + "SearchResponse", + "> | undefined" + ], + "path": "src/plugins/unified_histogram/public/chart/build_chart_data.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.getChartAggConfigs", + "type": "Function", + "tags": [], + "label": "getChartAggConfigs", + "description": [ + "\nHelper function to get the agg configs required for the unified histogram chart request" + ], + "signature": [ + "({\n dataView,\n timeInterval,\n data,\n}: { dataView: ", + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + }, + "; timeInterval: string; data: ", + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + }, + "; }) => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.AggConfigs", + "text": "AggConfigs" + } + ], + "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.getChartAggConfigs.$1", + "type": "Object", + "tags": [], + "label": "{\n dataView,\n timeInterval,\n data,\n}", + "description": [], + "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.getChartAggConfigs.$1.dataView", + "type": "Object", + "tags": [], + "label": "dataView", + "description": [], + "signature": [ + { + "pluginId": "dataViews", + "scope": "common", + "docId": "kibDataViewsPluginApi", + "section": "def-common.DataView", + "text": "DataView" + } + ], + "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.getChartAggConfigs.$1.timeInterval", + "type": "string", + "tags": [], + "label": "timeInterval", + "description": [], + "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.getChartAggConfigs.$1.data", + "type": "Object", + "tags": [], + "label": "data", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + } + ], + "path": "src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayout", + "type": "Function", + "tags": [], + "label": "UnifiedHistogramLayout", + "description": [ + "\nA resizable layout component with two panels that renders a histogram with a hits\ncounter in the top panel, and a main display (data table, etc.) in the bottom panel.\nIf all context props are left undefined, the layout will render in a single panel\nmode including only the main display." + ], + "signature": [ + "React.ForwardRefExoticComponent<", + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramLayoutProps", + "text": "UnifiedHistogramLayoutProps" + }, + " & React.RefAttributes<{}>>" + ], + "path": "src/plugins/unified_histogram/public/layout/index.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayout.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramBucketInterval", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramBucketInterval", + "description": [ + "\nThe bucketInterval object returned by {@link buildChartData} that\nshould be used to set {@link UnifiedHistogramChartContext.bucketInterval}" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramBucketInterval.scaled", + "type": "CompoundType", + "tags": [], + "label": "scaled", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramBucketInterval.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramBucketInterval.scale", + "type": "number", + "tags": [], + "label": "scale", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramChartContext", + "description": [ + "\nContext object for the chart" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext.status", + "type": "CompoundType", + "tags": [], + "label": "status", + "description": [ + "\nThe fetch status of the chart request" + ], + "signature": [ + "\"loading\" | \"error\" | \"complete\" | \"partial\" | \"uninitialized\"" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext.hidden", + "type": "CompoundType", + "tags": [], + "label": "hidden", + "description": [ + "\nControls whether or not the chart is hidden" + ], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext.timeInterval", + "type": "string", + "tags": [], + "label": "timeInterval", + "description": [ + "\nControls the time interval of the chart" + ], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext.bucketInterval", + "type": "Object", + "tags": [], + "label": "bucketInterval", + "description": [ + "\nThe bucketInterval object returned by {@link buildChartData}" + ], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramBucketInterval", + "text": "UnifiedHistogramBucketInterval" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext.data", + "type": "Object", + "tags": [], + "label": "data", + "description": [ + "\nThe chartData object returned by {@link buildChartData}" + ], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramChartData", + "text": "UnifiedHistogramChartData" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartContext.error", + "type": "Object", + "tags": [], + "label": "error", + "description": [ + "\nError from failed chart request" + ], + "signature": [ + "Error | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramChartData", + "description": [ + "\nThe chartData object returned by {@link buildChartData} that\nshould be used to set {@link UnifiedHistogramChartContext.data}" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.values", + "type": "Array", + "tags": [], + "label": "values", + "description": [], + "signature": [ + "{ x: number; y: number; }[]" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.xAxisOrderedValues", + "type": "Array", + "tags": [], + "label": "xAxisOrderedValues", + "description": [], + "signature": [ + "number[]" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.xAxisFormat", + "type": "Object", + "tags": [], + "label": "xAxisFormat", + "description": [], + "signature": [ + "{ id?: string | undefined; params?: ", + { + "pluginId": "fieldFormats", + "scope": "common", + "docId": "kibFieldFormatsPluginApi", + "section": "def-common.FieldFormatParams", + "text": "FieldFormatParams" + }, + "<{ pattern: string; }> | undefined; }" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.yAxisFormat", + "type": "Object", + "tags": [], + "label": "yAxisFormat", + "description": [], + "signature": [ + "{ id?: string | undefined; params?: ", + { + "pluginId": "fieldFormats", + "scope": "common", + "docId": "kibFieldFormatsPluginApi", + "section": "def-common.FieldFormatParams", + "text": "FieldFormatParams" + }, + "<{ pattern: string; }> | undefined; }" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.xAxisLabel", + "type": "string", + "tags": [], + "label": "xAxisLabel", + "description": [], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.yAxisLabel", + "type": "string", + "tags": [], + "label": "yAxisLabel", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramChartData.ordered", + "type": "Object", + "tags": [], + "label": "ordered", + "description": [], + "signature": [ + "Ordered" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramHitsContext", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramHitsContext", + "description": [ + "\nContext object for the hits count" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramHitsContext.status", + "type": "CompoundType", + "tags": [], + "label": "status", + "description": [ + "\nThe fetch status of the hits count request" + ], + "signature": [ + "\"loading\" | \"error\" | \"complete\" | \"partial\" | \"uninitialized\"" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramHitsContext.total", + "type": "number", + "tags": [], + "label": "total", + "description": [ + "\nThe total number of hits" + ], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramLayoutProps", + "description": [], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramLayoutProps", + "text": "UnifiedHistogramLayoutProps" + }, + " extends { children?: React.ReactNode; }" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.className", + "type": "string", + "tags": [], + "label": "className", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.services", + "type": "Object", + "tags": [], + "label": "services", + "description": [], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramServices", + "text": "UnifiedHistogramServices" + } + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.hits", + "type": "Object", + "tags": [], + "label": "hits", + "description": [ + "\nContext object for the hits count -- leave undefined to hide the hits count" + ], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramHitsContext", + "text": "UnifiedHistogramHitsContext" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.chart", + "type": "Object", + "tags": [], + "label": "chart", + "description": [ + "\nContext object for the chart -- leave undefined to hide the chart" + ], + "signature": [ + { + "pluginId": "unifiedHistogram", + "scope": "public", + "docId": "kibUnifiedHistogramPluginApi", + "section": "def-public.UnifiedHistogramChartContext", + "text": "UnifiedHistogramChartContext" + }, + " | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.resizeRef", + "type": "Object", + "tags": [], + "label": "resizeRef", + "description": [ + "\nRef to the element wrapping the layout which will be used for resize calculations" + ], + "signature": [ + "React.RefObject" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.topPanelHeight", + "type": "number", + "tags": [], + "label": "topPanelHeight", + "description": [ + "\nCurrent top panel height -- leave undefined to use the default" + ], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.appendHitsCounter", + "type": "Object", + "tags": [], + "label": "appendHitsCounter", + "description": [ + "\nAppend a custom element to the right of the hits count" + ], + "signature": [ + "React.ReactElement> | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTopPanelHeightChange", + "type": "Function", + "tags": [], + "label": "onTopPanelHeightChange", + "description": [ + "\nCallback to update the topPanelHeight prop when a resize is triggered" + ], + "signature": [ + "((topPanelHeight: number | undefined) => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTopPanelHeightChange.$1", + "type": "number", + "tags": [], + "label": "topPanelHeight", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onEditVisualization", + "type": "Function", + "tags": [], + "label": "onEditVisualization", + "description": [ + "\nCallback to invoke when the user clicks the edit visualization button -- leave undefined to hide the button" + ], + "signature": [ + "(() => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onChartHiddenChange", + "type": "Function", + "tags": [], + "label": "onChartHiddenChange", + "description": [ + "\nCallback to hide or show the chart -- should set {@link UnifiedHistogramChartContext.hidden} to chartHidden" + ], + "signature": [ + "((chartHidden: boolean) => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onChartHiddenChange.$1", + "type": "boolean", + "tags": [], + "label": "chartHidden", + "description": [], + "signature": [ + "boolean" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTimeIntervalChange", + "type": "Function", + "tags": [], + "label": "onTimeIntervalChange", + "description": [ + "\nCallback to update the time interval -- should set {@link UnifiedHistogramChartContext.timeInterval} to timeInterval" + ], + "signature": [ + "((timeInterval: string) => void) | undefined" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramLayoutProps.onTimeIntervalChange.$1", + "type": "string", + "tags": [], + "label": "timeInterval", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/unified_histogram/public/layout/layout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramServices", + "type": "Interface", + "tags": [], + "label": "UnifiedHistogramServices", + "description": [ + "\nThe services required by the unified histogram components" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramServices.data", + "type": "Object", + "tags": [], + "label": "data", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "public", + "docId": "kibDataPluginApi", + "section": "def-public.DataPublicPluginStart", + "text": "DataPublicPluginStart" + } + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramServices.theme", + "type": "Object", + "tags": [], + "label": "theme", + "description": [], + "signature": [ + "{ readonly chartsDefaultTheme: ", + "RecursivePartial", + "<", + "Theme", + ">; readonly chartsDefaultBaseTheme: ", + "Theme", + "; chartsTheme$: ", + "Observable", + "<", + "RecursivePartial", + "<", + "Theme", + ">>; chartsBaseTheme$: ", + "Observable", + "<", + "Theme", + ">; readonly darkModeEnabled$: ", + "Observable", + "; useDarkMode: () => boolean; useChartsTheme: () => ", + "RecursivePartial", + "<", + "Theme", + ">; useChartsBaseTheme: () => ", + "Theme", + "; }" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramServices.uiSettings", + "type": "Object", + "tags": [], + "label": "uiSettings", + "description": [], + "signature": [ + "IUiSettingsClient" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramServices.fieldFormats", + "type": "CompoundType", + "tags": [], + "label": "fieldFormats", + "description": [], + "signature": [ + "Omit<", + { + "pluginId": "fieldFormats", + "scope": "common", + "docId": "kibFieldFormatsPluginApi", + "section": "def-common.FieldFormatsRegistry", + "text": "FieldFormatsRegistry" + }, + ", \"init\" | \"register\"> & { deserialize: ", + { + "pluginId": "fieldFormats", + "scope": "common", + "docId": "kibFieldFormatsPluginApi", + "section": "def-common.FormatFactory", + "text": "FormatFactory" + }, + "; }" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "unifiedHistogram", + "id": "def-public.UnifiedHistogramFetchStatus", + "type": "Type", + "tags": [], + "label": "UnifiedHistogramFetchStatus", + "description": [ + "\nThe fetch status of a unified histogram request" + ], + "signature": [ + "\"loading\" | \"error\" | \"complete\" | \"partial\" | \"uninitialized\"" + ], + "path": "src/plugins/unified_histogram/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx new file mode 100644 index 0000000000000..a1d173ce94c7c --- /dev/null +++ b/api_docs/unified_histogram.mdx @@ -0,0 +1,36 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibUnifiedHistogramPluginApi +slug: /kibana-dev-docs/api/unifiedHistogram +title: "unifiedHistogram" +image: https://source.unsplash.com/400x175/?github +description: API docs for the unifiedHistogram plugin +date: 2022-10-18 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] +--- +import unifiedHistogramObj from './unified_histogram.devdocs.json'; + +The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. + +Contact [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 56 | 0 | 29 | 0 | + +## Client + +### Functions + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index fadfe11b38fb7..76459a60c6d21 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index d799d4fd80c0e..a2e78fce5f760 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index 0a52c11a03854..c39752fcb63b6 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index d82a3c180cad2..99ba0bda60c1d 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index 59254e5f865e2..8ed7aa98ba363 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 0182afa67b39e..dbb08de964fc4 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 021eb298c7981..9bed30004b6cf 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 43257d5ca1b94..233daecf7a8b8 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index a78557b09236d..00b96cf306e06 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index dc90e0c203f09..e260c971f5579 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 9e617bff19342..a40a021d230e1 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index a65d89a8f19da..4b4d58a5ca996 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index ca2ffc9d904d8..7a757e85a52ad 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 2faa5d6c961d6..dd6fe5c32cdad 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index cd92c771a8660..b4661b0fa4f6e 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 5e76e91dfb780..e622d31cf22b1 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2022-10-17 +date: 2022-10-18 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; From 4549c4e2f9bb566df4dcdca8c5fa430c228e7449 Mon Sep 17 00:00:00 2001 From: Mat Schaffer Date: Tue, 18 Oct 2022 16:21:17 +0900 Subject: [PATCH 26/74] A tiny note on adding cpu quotas for logstash (#143494) --- x-pack/plugins/monitoring/dev_docs/how_to/local_setup.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/plugins/monitoring/dev_docs/how_to/local_setup.md b/x-pack/plugins/monitoring/dev_docs/how_to/local_setup.md index 2abc96a5d4957..4e1b67ddd612e 100644 --- a/x-pack/plugins/monitoring/dev_docs/how_to/local_setup.md +++ b/x-pack/plugins/monitoring/dev_docs/how_to/local_setup.md @@ -85,6 +85,13 @@ docker run --name logstash \ docker.elastic.co/logstash/logstash:master-SNAPSHOT ``` +Note that you can add these arguments to populate cgroup/cfs data for logstash as well. This will require a cgroup v1 docker host until [logstash#14534](https://github.com/elastic/logstash/issues/14534) is resolved: + +``` + --cpu-period=100000 \ + --cpu-quota=150000 \ +``` + # Complete docker setup We also maintain an internal docker-compose setup for running a full stack with monitoring enabled for all components. From b7d3809b8c9aefb462a5a084cea0df35e39d856c Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Tue, 18 Oct 2022 08:33:57 +0100 Subject: [PATCH 27/74] fix layout in transactions (#143453) --- .../waterfall_container/waterfall_legends.tsx | 2 +- .../apm/public/components/shared/charts/latency_chart/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx index 30d4b0049d1bd..5aeae556f33ec 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends.tsx @@ -37,7 +37,7 @@ const LEGEND_LABELS = { }; export function WaterfallLegends({ legends, type }: Props) { return ( - + {LEGEND_LABELS[type]} diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index b47695ebed74c..76bc9ffb9f440 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -84,7 +84,7 @@ export function LatencyChart({ height, kuery }: Props) { - +

          From b288c2fcb70be0cc0701da4ef32b069cd1132301 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 18 Oct 2022 10:47:11 +0300 Subject: [PATCH 28/74] [Lens] Fixes chart scroll when the legend is long (#143340) * [Lens] Fixes chart scroll when the legend is long * Remove unnecessary css prop * Apply PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../workspace_panel_wrapper.scss | 6 +++++ .../workspace_panel_wrapper.tsx | 26 ++++++++----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 4d07441287cbd..e24149cf53e30 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -9,6 +9,12 @@ overflow: visible; height: 100%; + .lnsWorkspacePanelWrapper__content { + width: 100%; + height: 100%; + position: absolute; + } + .lnsWorkspacePanelWrapper__pageContentBody { @include euiBottomShadowMedium; @include euiScrollBar; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 865dbb4e859e4..62703e4daefc8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -8,13 +8,7 @@ import './workspace_panel_wrapper.scss'; import React, { useCallback } from 'react'; -import { - EuiPageSection, - EuiPageTemplate, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; +import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types'; @@ -118,9 +112,9 @@ export function WorkspacePanelWrapper({ warningMessages.push(...requestWarnings); } return ( - + {!(isFullscreen && (autoApplyEnabled || warningMessages?.length)) && ( - + - + )} - {children} - + ); } From 411a306216e9b2dade96e008cee3b1d3dca8f6b3 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 18 Oct 2022 11:24:17 +0300 Subject: [PATCH 29/74] [Unified search] Cleanups the showQueryBar flag (#143270) * [Unified search] Cleanups the showQueryBar flag that is not used anymore * Fix add filter story error --- .../public/application/top_nav/dashboard_top_nav.tsx | 1 - .../public/application/context/context_app.test.tsx | 1 - .../public/application/context/context_app.tsx | 1 - .../navigation/public/top_nav_menu/top_nav_menu.tsx | 2 -- .../public/__stories__/search_bar.stories.tsx | 10 ++++++++-- .../public/search_bar/create_search_bar.tsx | 1 - .../public/search_bar/search_bar.test.tsx | 1 - .../unified_search/public/search_bar/search_bar.tsx | 2 -- src/plugins/vis_type_markdown/public/markdown_vis.ts | 1 - .../vis_types/heatmap/public/sample_vis.test.mocks.ts | 1 - .../vis_types/pie/public/sample_vis.test.mocks.ts | 1 - .../vis_types/timelion/public/timelion_vis_type.tsx | 1 - .../vis_types/timeseries/public/metrics_type.ts | 1 - src/plugins/vis_types/vega/public/vega_type.ts | 1 - .../options/point_series/point_series.mocks.ts | 1 - .../vis_types/xy/public/sample_vis.test.mocks.ts | 1 - .../visualizations/public/vis_types/base_vis_type.ts | 1 - src/plugins/visualizations/public/vis_types/types.ts | 1 - .../visualize_app/components/visualize_top_nav.tsx | 3 +-- .../pages/findings/layout/findings_search_bar.tsx | 1 - .../infra/public/pages/metrics/hosts/hosts_content.tsx | 1 - x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx | 1 - .../profiling_search_bar.tsx | 1 - .../public/common/components/filter_bar/index.tsx | 1 - .../public/common/components/query_bar/index.test.tsx | 1 - .../public/common/components/query_bar/index.tsx | 1 - .../public/common/components/search_bar/index.tsx | 1 - .../endpoint_hosts/view/components/search_bar.tsx | 1 - .../expression/search_source_expression_form.tsx | 1 - 29 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 31896f6fc2adf..3e9697d56d657 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -517,7 +517,6 @@ export function DashboardTopNav({ return { badges, screenTitle, - showQueryBar, showSearchBar, showFilterBar, showSaveQuery, diff --git a/src/plugins/discover/public/application/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx index 987147403cc82..5b5fa21f12483 100644 --- a/src/plugins/discover/public/application/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/context/context_app.test.tsx @@ -74,7 +74,6 @@ describe('ContextApp test', () => { const topNavProps = { appName: 'context', showSearchBar: true, - showQueryBar: true, showQueryInput: false, showFilterBar: true, showSaveQuery: false, diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index b7e3f834781cc..3146e634c4575 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -145,7 +145,6 @@ export const ContextApp = ({ dataView, anchorId }: ContextAppProps) => { return { appName: 'context', showSearchBar: true, - showQueryBar: true, showQueryInput: false, showFilterBar: true, showSaveQuery: false, diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 2f07824d884a0..652ff58a5ab4a 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,7 +24,6 @@ export type TopNavMenuProps = config?: TopNavMenuData[]; badges?: Array; showSearchBar?: boolean; - showQueryBar?: boolean; showQueryInput?: boolean; showDatePicker?: boolean; showFilterBar?: boolean; @@ -142,7 +141,6 @@ export function TopNavMenu( TopNavMenu.defaultProps = { showSearchBar: false, - showQueryBar: true, showQueryInput: true, showDatePicker: true, showFilterBar: true, diff --git a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx index 40d3abfb7fae0..60e0659a6112a 100644 --- a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx +++ b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx @@ -22,7 +22,7 @@ const mockIndexPatterns = [ title: 'logstash-*', fields: [ { - name: 'response', + name: 'bytes', type: 'number', esTypes: ['integer'], aggregatable: true, @@ -30,6 +30,7 @@ const mockIndexPatterns = [ searchable: true, }, ], + getName: () => 'logstash-*', }, { id: '1235', @@ -44,6 +45,7 @@ const mockIndexPatterns = [ searchable: true, }, ], + getName: () => 'test-*', }, ] as DataView[]; @@ -162,6 +164,11 @@ const services = { ], }, }, + dataViewEditor: { + userPermissions: { + editDataView: action('editDataView'), + }, + }, }; setIndexPatterns({ @@ -173,7 +180,6 @@ function wrapSearchBarInContext(testProps: SearchBarProps) { appName: 'test', timeHistory: mockTimeHistory, intl: null as any, - showQueryBar: true, showFilterBar: true, showDatePicker: true, showAutoRefreshOnly: false, 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 5c0dfda5d9b2c..35555137f12b2 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 @@ -190,7 +190,6 @@ export function createSearchBar({ showAutoRefreshOnly={props.showAutoRefreshOnly} showDatePicker={props.showDatePicker} showFilterBar={props.showFilterBar} - showQueryBar={props.showQueryBar} showQueryInput={props.showQueryInput} showSaveQuery={props.showSaveQuery} showSubmitButton={props.showSubmitButton} diff --git a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx index 518f9d1f16e16..0d0ff97ae4b05 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx @@ -211,7 +211,6 @@ describe('SearchBar', () => { screenTitle: 'test screen', onQuerySubmit: noop, query: kqlQuery, - showQueryBar: false, showQueryInput: false, }) ); diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index 76746d8a86979..ebaa3a317c270 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -48,7 +48,6 @@ export interface SearchBarOwnProps { screenTitle?: string; dataTestSubj?: string; // Togglers - showQueryBar?: boolean; showQueryInput?: boolean; showFilterBar?: boolean; showDatePicker?: boolean; @@ -122,7 +121,6 @@ class SearchBarUI extends C State > { public static defaultProps = { - showQueryBar: true, showFilterBar: true, showDatePicker: true, showSubmitButton: true, diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index a4b4010064e78..33acfa21cd0b0 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -58,7 +58,6 @@ export const markdownVisDefinition: VisTypeDefinition = { options: { showTimePicker: false, showFilterBar: false, - showQueryBar: true, showQueryInput: false, }, inspectorAdapters: {}, diff --git a/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts index 55a8aaa218837..6a33feb853221 100644 --- a/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts @@ -14,7 +14,6 @@ export const sampleAreaVis = { stage: 'production', options: { showTimePicker: true, - showQueryBar: true, showFilterBar: true, showIndexSelection: true, hierarchicalData: false, diff --git a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts index 035432de9ad23..58a512d95bd64 100644 --- a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts @@ -20,7 +20,6 @@ export const samplePieVis = { stage: 'production', options: { showTimePicker: true, - showQueryBar: true, showFilterBar: true, showIndexSelection: true, hierarchicalData: false, diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx index f8d7415f6aefe..011f6f6aafeee 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx @@ -66,7 +66,6 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) }, options: { showIndexSelection: false, - showQueryBar: true, showFilterBar: false, showQueryInput: false, }, diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 2e18e9c0af48e..3bf9f9b90bf1a 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -157,7 +157,6 @@ export const metricsVisDefinition: VisTypeDefinition< editor: TSVB_EDITOR_NAME, }, options: { - showQueryBar: true, showFilterBar: true, showIndexSelection: false, }, diff --git a/src/plugins/vis_types/vega/public/vega_type.ts b/src/plugins/vis_types/vega/public/vega_type.ts index 798930db1f975..3299c86dc0a80 100644 --- a/src/plugins/vis_types/vega/public/vega_type.ts +++ b/src/plugins/vis_types/vega/public/vega_type.ts @@ -51,7 +51,6 @@ export const createVegaTypeDefinition = (): VisTypeDefinition => { toExpressionAst, options: { showIndexSelection: false, - showQueryBar: true, showFilterBar: true, }, getSupportedTriggers: () => { diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts index 0f6f403d16ac8..a60e6577d412a 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -346,7 +346,6 @@ export const getVis = (bucketType: string) => { titleInWizard: '', options: { showTimePicker: true, - showQueryBar: true, showFilterBar: true, showIndexSelection: true, hierarchicalData: false, diff --git a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts index b2660b7c66551..da78864bc08d0 100644 --- a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts @@ -19,7 +19,6 @@ export const sampleAreaVis = { stage: 'production', options: { showTimePicker: true, - showQueryBar: true, showFilterBar: true, showIndexSelection: true, hierarchicalData: false, diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 2d351f18a3b47..4253b134cb748 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -15,7 +15,6 @@ import { Schemas } from './schemas'; const defaultOptions: VisTypeOptions = { showTimePicker: true, - showQueryBar: true, showFilterBar: true, showIndexSelection: true, showQueryInput: true, diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 9689729f187fd..5d581a52130a5 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -22,7 +22,6 @@ import { NavigateToLensContext } from '../../common'; export interface VisTypeOptions { showTimePicker: boolean; - showQueryBar: boolean; showFilterBar: boolean; showIndexSelection: boolean; showQueryInput: boolean; diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx index 055b326b8f19f..e7512c6dd6473 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx @@ -170,8 +170,7 @@ const TopNav = ({ return vis.type.options.showTimePicker && hasTimeField; }; const showFilterBar = vis.type.options.showFilterBar; - const showQueryInput = - vis.type.requiresSearch && vis.type.options.showQueryBar && vis.type.options.showQueryInput; + const showQueryInput = vis.type.requiresSearch && vis.type.options.showQueryInput; useEffect(() => { return () => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_search_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_search_bar.tsx index bc7e524ce7bf3..b2272b47c543b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_search_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_search_bar.tsx @@ -43,7 +43,6 @@ export const FindingsSearchBar = ({ appName={PLUGIN_NAME} dataTestSubj={TEST_SUBJECTS.FINDINGS_SEARCH_BAR} showFilterBar={true} - showQueryBar={true} showQueryInput={true} showDatePicker={false} showSaveQuery={false} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx index 7bf087db39eb5..2fc841d651e21 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx @@ -60,7 +60,6 @@ export const HostsContent: React.FunctionComponent = () => { {metricsDataView ? ( <> ( showAutoRefreshOnly={false} showFilterBar={true} showDatePicker={false} - showQueryBar={false} showQueryInput={false} showSaveQuery={false} dataTestSubj={dataTestSubj} diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index b9e95a2ee837e..118c78e290759 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -202,7 +202,6 @@ describe('QueryBar ', () => { showAutoRefreshOnly: false, showDatePicker: false, showFilterBar: true, - showQueryBar: true, showQueryInput: true, showSaveQuery: true, showSubmitButton: false, diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 82080ba1ac602..d86f3de10b549 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -125,7 +125,6 @@ export const QueryBar = memo( showAutoRefreshOnly={false} showFilterBar={!hideSavedQuery} showDatePicker={false} - showQueryBar={true} showQueryInput={true} showSaveQuery={true} timeHistory={timeHistory} diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index 188c7a1359743..ba7efafb6b0dd 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -311,7 +311,6 @@ export const SearchBarComponent = memo( savedQuery={savedQuery} showFilterBar={!hideFilterBar} showDatePicker={true} - showQueryBar={true} showQueryInput={!hideQueryInput} showSaveQuery={true} dataTestSubj={dataTestSubj} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx index c2fef59ebd460..8b7f668df439d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -63,7 +63,6 @@ export const AdminSearchBar = memo(() => { iconType="search" showFilterBar={false} showDatePicker={false} - showQueryBar={true} showQueryInput={true} />

      diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx index 67526a53a6b8e..6274a4dcdba95 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx @@ -259,7 +259,6 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp onSavedQueryUpdated={onSavedQuery} onSaved={onSavedQuery} showSaveQuery - showQueryBar showQueryInput showFilterBar showDatePicker={false} From 74595dee9bec1f0f1ff8c026c3f4b50393ece025 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 18 Oct 2022 07:22:35 -0500 Subject: [PATCH 30/74] [data view editor] Form state refactor - create service, replace useEffect with observables (#142421) * replace useEffect with observables as much as possible * cleanup, fix timestamp field / allow hidden bug --- .../data_view_editor_flyout_content.tsx | 258 +++++++----------- .../components/data_view_editor_lazy.tsx | 6 +- .../data_view_flyout_content_container.tsx | 34 ++- .../components/form_fields/name_field.tsx | 13 +- .../form_fields/timestamp_field.tsx | 41 ++- .../components/form_fields/title_field.tsx | 8 +- .../indices_list/indices_list.tsx | 33 ++- .../preview_panel/preview_panel.tsx | 8 +- .../public/data_view_editor_service.ts | 215 +++++++++++++++ .../data_view_editor/public/open_editor.tsx | 4 +- .../data_view_editor/public/plugin.test.tsx | 4 +- src/plugins/data_view_editor/public/types.ts | 6 +- .../_index_pattern_create_delete.ts | 14 + 13 files changed, 425 insertions(+), 219 deletions(-) create mode 100644 src/plugins/data_view_editor/public/data_view_editor_service.ts diff --git a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx index 98f5f906113b6..9c86170795961 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useEffect, useCallback, useRef, useContext } from 'react'; import { EuiTitle, EuiFlexGroup, @@ -15,14 +15,12 @@ import { EuiLoadingSpinner, EuiLink, } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import memoizeOne from 'memoize-one'; -import { - DataViewField, - DataViewsPublicPluginStart, - INDEX_PATTERN_TYPE, - MatchedItem, -} from '@kbn/data-views-plugin/public'; +import { BehaviorSubject } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; +import { INDEX_PATTERN_TYPE, MatchedItem } from '@kbn/data-views-plugin/public'; import { DataView, @@ -31,11 +29,10 @@ import { useForm, useFormData, useKibana, - GetFieldsOptions, UseField, } from '../shared_imports'; -import { ensureMinimumTime, extractTimeFields, getMatchedIndices } from '../lib'; +import { ensureMinimumTime, getMatchedIndices } from '../lib'; import { FlyoutPanels } from './flyout_panels'; import { removeSpaces } from '../lib'; @@ -46,7 +43,6 @@ import { IndexPatternConfig, MatchedIndicesSet, FormInternal, - TimestampOption, } from '../types'; import { @@ -61,6 +57,8 @@ import { RollupBetaWarning, } from '.'; import { editDataViewModal } from './confirm_modals/edit_data_view_changed_modal'; +import { DataViewEditorServiceContext } from './data_view_flyout_content_container'; +import { DataViewEditorService } from '../data_view_editor_service'; export interface Props { /** @@ -78,6 +76,13 @@ export interface Props { allowAdHoc: boolean; } +export const matchedIndiciesDefault = { + allIndices: [], + exactMatchedIndices: [], + partialMatchedIndices: [], + visibleIndices: [], +}; + const editorTitle = i18n.translate('indexPatternEditor.title', { defaultMessage: 'Create data view', }); @@ -96,9 +101,11 @@ const IndexPatternEditorFlyoutContentComponent = ({ showManagementLink, }: Props) => { const { - services: { application, http, dataViews, uiSettings, overlays }, + services: { application, dataViews, uiSettings, overlays }, } = useKibana(); + const { dataViewEditorService } = useContext(DataViewEditorServiceContext); + const canSave = dataViews.getCanSaveSync(); const { form } = useForm({ @@ -125,12 +132,15 @@ const IndexPatternEditorFlyoutContentComponent = ({ return; } + const rollupIndicesCapabilities = dataViewEditorService.rollupIndicesCapabilities$.getValue(); + const indexPatternStub: DataViewSpec = { title: removeSpaces(formData.title), timeFieldName: formData.timestampField?.value, id: formData.id, name: formData.name, }; + const rollupIndex = rollupIndex$.current.getValue(); if (type === INDEX_PATTERN_TYPE.ROLLUP && rollupIndex) { indexPatternStub.type = INDEX_PATTERN_TYPE.ROLLUP; @@ -156,8 +166,6 @@ const IndexPatternEditorFlyoutContentComponent = ({ }, }); - const { getFields } = form; - // `useFormData` initially returns `undefined`, // we override `undefined` with real default values from `schema` // to get a stable reference to avoid hooks re-run and reduce number of excessive requests @@ -168,148 +176,92 @@ const IndexPatternEditorFlyoutContentComponent = ({ type = schema.type.defaultValue, }, ] = useFormData({ form }); - const [isLoadingSources, setIsLoadingSources] = useState(true); - const [timestampFieldOptions, setTimestampFieldOptions] = useState([]); - const [isLoadingTimestampFields, setIsLoadingTimestampFields] = useState(false); - const currentLoadingTimestampFieldsRef = useRef(0); - const [isLoadingMatchedIndices, setIsLoadingMatchedIndices] = useState(false); const currentLoadingMatchedIndicesRef = useRef(0); - const [allSources, setAllSources] = useState([]); - const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); - const [existingIndexPatterns, setExistingIndexPatterns] = useState([]); - const [rollupIndex, setRollupIndex] = useState(); - const [rollupIndicesCapabilities, setRollupIndicesCapabilities] = - useState({}); - const [matchedIndices, setMatchedIndices] = useState({ - allIndices: [], - exactMatchedIndices: [], - partialMatchedIndices: [], - visibleIndices: [], - }); - // load all data sources and set initial matchedIndices - const loadSources = useCallback(() => { - dataViews - .getIndices({ - isRollupIndex: () => false, - pattern: '*', - showAllIndices: allowHidden, - }) - .then((dataSources) => { - setAllSources(dataSources); - const matchedSet = getMatchedIndices(dataSources, [], [], allowHidden); - setMatchedIndices(matchedSet); - setIsLoadingSources(false); - }); - }, [allowHidden, dataViews]); + const isLoadingSources = useObservable(dataViewEditorService.isLoadingSources$, true); - // loading list of index patterns - useEffect(() => { - loadSources(); - const getTitles = async () => { - const dataViewListItems = await dataViews.getIdsWithTitle(editData ? true : false); - const indexPatternNames = dataViewListItems.map((item) => item.name || item.title); + const loadingMatchedIndices$ = useRef(new BehaviorSubject(false)); - setExistingIndexPatterns( - editData ? indexPatternNames.filter((v) => v !== editData.name) : indexPatternNames - ); - setIsLoadingIndexPatterns(false); - }; - getTitles(); - }, [http, dataViews, editData, loadSources]); + const isLoadingDataViewNames$ = useRef(new BehaviorSubject(true)); + const existingDataViewNames$ = useRef(new BehaviorSubject([])); + const isLoadingDataViewNames = useObservable(isLoadingDataViewNames$.current, true); + + const rollupIndicesCapabilities = useObservable( + dataViewEditorService.rollupIndicesCapabilities$, + {} + ); + + const rollupIndex$ = useRef(new BehaviorSubject(undefined)); - // loading rollup info + // initial loading of indicies and data view names useEffect(() => { - const getRollups = async () => { - try { - const response = await http.get('/api/rollup/indices'); - if (response) { - setRollupIndicesCapabilities(response); - } - } catch (e) { - // Silently swallow failure responses such as expired trials - } + let isCancelled = false; + const matchedIndicesSub = dataViewEditorService.matchedIndices$.subscribe((matchedIndices) => { + const timeFieldQuery = editData ? editData.title : title; + dataViewEditorService.loadTimestampFields( + removeSpaces(timeFieldQuery), + type, + requireTimestampField, + rollupIndex$.current.getValue() + ); + }); + + dataViewEditorService.loadIndices(title, allowHidden).then((matchedIndices) => { + if (isCancelled) return; + dataViewEditorService.matchedIndices$.next(matchedIndices); + }); + + dataViewEditorService.loadDataViewNames(title).then((names) => { + if (isCancelled) return; + const filteredNames = editData ? names.filter((name) => name !== editData?.name) : names; + existingDataViewNames$.current.next(filteredNames); + isLoadingDataViewNames$.current.next(false); + }); + + return () => { + isCancelled = true; + matchedIndicesSub.unsubscribe(); }; - - getRollups(); - }, [http, type]); + }, [editData, type, title, allowHidden, requireTimestampField, dataViewEditorService]); const getRollupIndices = (rollupCaps: RollupIndicesCapsResponse) => Object.keys(rollupCaps); - const loadTimestampFieldOptions = useCallback( - async (query: string) => { - const currentLoadingTimestampFieldsIdx = ++currentLoadingTimestampFieldsRef.current; - let timestampOptions: TimestampOption[] = []; - const isValidResult = - matchedIndices.exactMatchedIndices.length > 0 && !isLoadingMatchedIndices; - if (isValidResult) { - setIsLoadingTimestampFields(true); - const getFieldsOptions: GetFieldsOptions = { - pattern: query, - }; - if (type === INDEX_PATTERN_TYPE.ROLLUP) { - getFieldsOptions.type = INDEX_PATTERN_TYPE.ROLLUP; - getFieldsOptions.rollupIndex = rollupIndex; - } - - const fields = await ensureMinimumTime(dataViews.getFieldsForWildcard(getFieldsOptions)); - timestampOptions = extractTimeFields(fields as DataViewField[], requireTimestampField); - } - if (currentLoadingTimestampFieldsIdx === currentLoadingTimestampFieldsRef.current) { - setIsLoadingTimestampFields(false); - setTimestampFieldOptions(timestampOptions); - } - return timestampOptions; - }, - [ - dataViews, - requireTimestampField, - rollupIndex, - type, - matchedIndices.exactMatchedIndices, - isLoadingMatchedIndices, - ] - ); - + // used in title field validation const reloadMatchedIndices = useCallback( async (newTitle: string) => { - const isRollupIndex = (indexName: string) => - getRollupIndices(rollupIndicesCapabilities).includes(indexName); let newRollupIndexName: string | undefined; const fetchIndices = async (query: string = '') => { const currentLoadingMatchedIndicesIdx = ++currentLoadingMatchedIndicesRef.current; - setIsLoadingMatchedIndices(true); + loadingMatchedIndices$.current.next(true); + + const allSrcs = await dataViewEditorService.getIndicesCached({ + pattern: '*', + showAllIndices: allowHidden, + }); const { matchedIndicesResult, exactMatched } = !isLoadingSources - ? await loadMatchedIndices(query, allowHidden, allSources, { - isRollupIndex, - dataViews, - }) + ? await loadMatchedIndices(query, allowHidden, allSrcs, dataViewEditorService) : { - matchedIndicesResult: { - exactMatchedIndices: [], - allIndices: [], - partialMatchedIndices: [], - visibleIndices: [], - }, + matchedIndicesResult: matchedIndiciesDefault, exactMatched: [], }; if (currentLoadingMatchedIndicesIdx === currentLoadingMatchedIndicesRef.current) { // we are still interested in this result if (type === INDEX_PATTERN_TYPE.ROLLUP) { + const isRollupIndex = await dataViewEditorService.getIsRollupIndex(); const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name)); newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : undefined; - setRollupIndex(newRollupIndexName); + rollupIndex$.current.next(newRollupIndexName); } else { - setRollupIndex(undefined); + rollupIndex$.current.next(undefined); } - setMatchedIndices(matchedIndicesResult); - setIsLoadingMatchedIndices(false); + dataViewEditorService.matchedIndices$.next(matchedIndicesResult); + loadingMatchedIndices$.current.next(false); } return { matchedIndicesResult, newRollupIndexName }; @@ -317,27 +269,16 @@ const IndexPatternEditorFlyoutContentComponent = ({ return fetchIndices(newTitle); }, - [dataViews, allowHidden, allSources, type, rollupIndicesCapabilities, isLoadingSources] + [ + allowHidden, + type, + dataViewEditorService, + rollupIndex$, + isLoadingSources, + loadingMatchedIndices$, + ] ); - // If editData exists, loadSources so that MatchedIndices can be loaded for the Timestampfields - useEffect(() => { - if (editData) { - loadSources(); - reloadMatchedIndices(removeSpaces(editData.getIndexPattern())); - } - // We use the below eslint-disable as adding 'loadSources' and 'reloadMatchedIndices' as a dependency creates an infinite loop - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editData]); - - useEffect(() => { - const timeFieldQuery = editData ? editData.title : title; - loadTimestampFieldOptions(removeSpaces(timeFieldQuery)); - if (!editData) getFields().timestampField?.setValue(''); - // We use the below eslint-disable as adding editData as a dependency create an infinite loop - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadTimestampFieldOptions, title, getFields]); - const onTypeChange = useCallback( (newType) => { form.setFieldValue('title', ''); @@ -350,7 +291,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ [form] ); - if (isLoadingSources || isLoadingIndexPatterns) { + if (isLoadingSources || isLoadingDataViewNames) { return ; } @@ -404,7 +345,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ - + @@ -413,7 +354,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ @@ -422,10 +363,10 @@ const IndexPatternEditorFlyoutContentComponent = ({ @@ -460,7 +401,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ type={type} allowHidden={allowHidden} title={title} - matched={matchedIndices} + matchedIndices$={dataViewEditorService.matchedIndices$} /> )} @@ -479,13 +420,7 @@ const loadMatchedIndices = memoizeOne( query: string, allowHidden: boolean, allSources: MatchedItem[], - { - isRollupIndex, - dataViews, - }: { - isRollupIndex: (index: string) => boolean; - dataViews: DataViewsPublicPluginStart; - } + dataViewEditorService: DataViewEditorService ): Promise<{ matchedIndicesResult: MatchedIndicesSet; exactMatched: MatchedItem[]; @@ -494,8 +429,7 @@ const loadMatchedIndices = memoizeOne( const indexRequests = []; if (query?.endsWith('*')) { - const exactMatchedQuery = dataViews.getIndices({ - isRollupIndex, + const exactMatchedQuery = dataViewEditorService.getIndicesCached({ pattern: query, showAllIndices: allowHidden, }); @@ -503,13 +437,11 @@ const loadMatchedIndices = memoizeOne( // provide default value when not making a request for the partialMatchQuery indexRequests.push(Promise.resolve([])); } else { - const exactMatchQuery = dataViews.getIndices({ - isRollupIndex, + const exactMatchQuery = dataViewEditorService.getIndicesCached({ pattern: query, showAllIndices: allowHidden, }); - const partialMatchQuery = dataViews.getIndices({ - isRollupIndex, + const partialMatchQuery = dataViewEditorService.getIndicesCached({ pattern: `${query}*`, showAllIndices: allowHidden, }); diff --git a/src/plugins/data_view_editor/public/components/data_view_editor_lazy.tsx b/src/plugins/data_view_editor/public/components/data_view_editor_lazy.tsx index 0abf5e3821f8e..ae3c98d512213 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor_lazy.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor_lazy.tsx @@ -11,12 +11,10 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { DataViewEditorProps } from '../types'; -const IndexPatternFlyoutContentContainer = lazy( - () => import('./data_view_flyout_content_container') -); +const DataViewFlyoutContentContainer = lazy(() => import('./data_view_flyout_content_container')); export const DataViewEditorLazy = (props: DataViewEditorProps) => ( }> - + ); diff --git a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx index 3aaf56e6daaad..16e58d52624c3 100644 --- a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -12,8 +12,14 @@ import { i18n } from '@kbn/i18n'; import { DataViewSpec, useKibana } from '../shared_imports'; import { IndexPatternEditorFlyoutContent } from './data_view_editor_flyout_content'; import { DataViewEditorContext, DataViewEditorProps } from '../types'; +import { DataViewEditorService } from '../data_view_editor_service'; -const IndexPatternFlyoutContentContainer = ({ +// @ts-ignore +export const DataViewEditorServiceContext = React.createContext<{ + dataViewEditorService: DataViewEditorService; +}>(); + +const DataViewFlyoutContentContainer = ({ onSave, onCancel = () => {}, defaultTypeIsRollup, @@ -23,7 +29,7 @@ const IndexPatternFlyoutContentContainer = ({ showManagementLink, }: DataViewEditorProps) => { const { - services: { dataViews, notifications }, + services: { dataViews, notifications, http }, } = useKibana(); const onSaveClick = async (dataViewSpec: DataViewSpec, persist: boolean = true) => { @@ -63,17 +69,21 @@ const IndexPatternFlyoutContentContainer = ({ }; return ( - + + + ); }; /* eslint-disable import/no-default-export */ -export default IndexPatternFlyoutContentContainer; +export default DataViewFlyoutContentContainer; diff --git a/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx index f2236abad5ec7..425178d452c5a 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/name_field.tsx @@ -9,8 +9,9 @@ import React, { ChangeEvent, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { BehaviorSubject } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; import { - DataView, UseField, ValidationConfig, FieldConfig, @@ -20,8 +21,7 @@ import { IndexPatternConfig } from '../../types'; import { schema } from '../form_schema'; interface NameFieldProps { - editData?: DataView; - existingDataViewNames: string[]; + existingDataViewNames$: BehaviorSubject; } interface GetNameConfigArgs { @@ -53,13 +53,14 @@ const getNameConfig = ({ namesNotAllowed }: GetNameConfigArgs): FieldConfig { +export const NameField = ({ existingDataViewNames$ }: NameFieldProps) => { + const namesNotAllowed = useObservable(existingDataViewNames$, []); const config = useMemo( () => getNameConfig({ - namesNotAllowed: existingDataViewNames, + namesNotAllowed, }), - [existingDataViewNames] + [namesNotAllowed] ); return ( diff --git a/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx index ce36d1e1fdc99..846a9db09ee80 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/timestamp_field.tsx @@ -8,8 +8,10 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; - +import useObservable from 'react-use/lib/useObservable'; +import { BehaviorSubject, Subject } from 'rxjs'; import { EuiFormRow, EuiComboBox, EuiFormHelpText, EuiComboBoxOptionOption } from '@elastic/eui'; +import { matchedIndiciesDefault } from '../data_view_editor_flyout_content'; import { UseField, @@ -18,17 +20,17 @@ import { getFieldValidityAndErrorMessage, } from '../../shared_imports'; -import { TimestampOption } from '../../types'; +import { TimestampOption, MatchedIndicesSet } from '../../types'; import { schema } from '../form_schema'; interface Props { - options: TimestampOption[]; - isLoadingOptions: boolean; - isLoadingMatchedIndices: boolean; - hasMatchedIndices: boolean; + options$: Subject; + isLoadingOptions$: BehaviorSubject; + isLoadingMatchedIndices$: BehaviorSubject; + matchedIndices$: Subject; } -const requireTimestampOptionValidator = (options: Props['options']): ValidationConfig => ({ +const requireTimestampOptionValidator = (options: TimestampOption[]): ValidationConfig => ({ validator: async ({ value }) => { const isValueRequired = !!options.length; if (isValueRequired && !value) { @@ -45,7 +47,7 @@ const requireTimestampOptionValidator = (options: Props['options']): ValidationC }); const getTimestampConfig = ( - options: Props['options'] + options: TimestampOption[] ): FieldConfig> => { const timestampFieldConfig = schema.timestampField; @@ -70,15 +72,22 @@ const timestampFieldHelp = i18n.translate('indexPatternEditor.editor.form.timeFi }); export const TimestampField = ({ - options = [], - isLoadingOptions = false, - isLoadingMatchedIndices, - hasMatchedIndices, + options$, + isLoadingOptions$, + isLoadingMatchedIndices$, + matchedIndices$, }: Props) => { + const options = useObservable(options$, []); + const isLoadingOptions = useObservable(isLoadingOptions$, false); + const isLoadingMatchedIndices = useObservable(isLoadingMatchedIndices$, false); + const hasMatchedIndices = !!useObservable(matchedIndices$, matchedIndiciesDefault) + .exactMatchedIndices.length; + const optionsAsComboBoxOptions = options.map(({ display, fieldName }) => ({ label: display, value: fieldName, })); + const timestampConfig = useMemo(() => getTimestampConfig(options), [options]); const selectTimestampHelp = options.length ? timestampFieldHelp : ''; @@ -98,8 +107,12 @@ export const TimestampField = ({ const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const isDisabled = !optionsAsComboBoxOptions.length; + // if the value isn't in the list then don't use it. + const valueInList = !!optionsAsComboBoxOptions.find( + (option) => option.value === value.value + ); - if (!value && !isDisabled) { + if ((!value || !valueInList) && !isDisabled) { const val = optionsAsComboBoxOptions.filter((el) => el.value === '@timestamp'); if (val.length) { setValue(val[0]); @@ -124,7 +137,7 @@ export const TimestampField = ({ )} singleSelection={{ asPlainText: true }} options={optionsAsComboBoxOptions} - selectedOptions={value ? [value] : undefined} + selectedOptions={value && valueInList ? [value] : undefined} onChange={(newValue) => { if (newValue.length === 0) { // Don't allow clearing the type. One must always be selected diff --git a/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx index 9a4c209a56ebf..6f443871dd66e 100644 --- a/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx +++ b/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx @@ -9,6 +9,8 @@ import React, { ChangeEvent, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { Subject } from 'rxjs'; +import useObservable from 'react-use/lib/useObservable'; import { MatchedItem } from '@kbn/data-views-plugin/public'; import { UseField, @@ -19,6 +21,7 @@ import { import { canAppendWildcard, removeSpaces } from '../../lib'; import { schema } from '../form_schema'; import { RollupIndicesCapsResponse, IndexPatternConfig, MatchedIndicesSet } from '../../types'; +import { matchedIndiciesDefault } from '../data_view_editor_flyout_content'; interface RefreshMatchedIndicesResult { matchedIndicesResult: MatchedIndicesSet; @@ -27,7 +30,7 @@ interface RefreshMatchedIndicesResult { interface TitleFieldProps { isRollup: boolean; - matchedIndices: MatchedItem[]; + matchedIndices$: Subject; rollupIndicesCapabilities: RollupIndicesCapsResponse; refreshMatchedIndices: (title: string) => Promise; } @@ -134,11 +137,12 @@ const getTitleConfig = ({ export const TitleField = ({ isRollup, - matchedIndices, + matchedIndices$, rollupIndicesCapabilities, refreshMatchedIndices, }: TitleFieldProps) => { const [appendedWildcard, setAppendedWildcard] = useState(false); + const matchedIndices = useObservable(matchedIndices$, matchedIndiciesDefault).exactMatchedIndices; const fieldConfig = useMemo( () => diff --git a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx index 51405efc58790..f307bbfc43889 100644 --- a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx +++ b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx @@ -144,18 +144,35 @@ export class IndicesList extends React.Component q.trim()); + let queryIdx = -1; + let queryWithoutWildcard = ''; + for (let i = 0; i < queryAsArray.length; i++) { + const queryComponent = queryAsArray[i]; + queryWithoutWildcard = queryComponent.endsWith('*') + ? queryComponent.substring(0, queryComponent.length - 1) + : queryComponent; + queryIdx = indexName.indexOf(queryWithoutWildcard); + + if (queryIdx !== -1) { + break; + } + } + if (queryIdx === -1) { + return indexName; + } + + const preStr = indexName.substring(0, queryIdx); + const postStr = indexName.substr(queryIdx + queryWithoutWildcard.length); return ( {preStr} - {query} + {queryWithoutWildcard} {postStr} ); @@ -164,15 +181,11 @@ export class IndicesList extends React.Component { return ( - - {this.highlightIndexName(index.name, queryWithoutWildcard)} - + {this.highlightIndexName(index.name, query)} {index.tags.map((tag: Tag) => { return ( diff --git a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx index 26f6b2b1c2a70..28163384ca0f8 100644 --- a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx +++ b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx @@ -8,9 +8,12 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; +import { Subject } from 'rxjs'; import { INDEX_PATTERN_TYPE } from '@kbn/data-views-plugin/public'; import { StatusMessage } from './status_message'; import { IndicesList } from './indices_list'; +import { matchedIndiciesDefault } from '../data_view_editor_flyout_content'; import { MatchedIndicesSet } from '../../types'; @@ -18,10 +21,11 @@ interface Props { type: INDEX_PATTERN_TYPE; allowHidden: boolean; title: string; - matched: MatchedIndicesSet; + matchedIndices$: Subject; } -export const PreviewPanel = ({ type, allowHidden, title = '', matched }: Props) => { +export const PreviewPanel = ({ type, allowHidden, title = '', matchedIndices$ }: Props) => { + const matched = useObservable(matchedIndices$, matchedIndiciesDefault); const indicesListContent = matched.visibleIndices.length || matched.allIndices.length ? ( <> diff --git a/src/plugins/data_view_editor/public/data_view_editor_service.ts b/src/plugins/data_view_editor/public/data_view_editor_service.ts new file mode 100644 index 0000000000000..57963830149db --- /dev/null +++ b/src/plugins/data_view_editor/public/data_view_editor_service.ts @@ -0,0 +1,215 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import { BehaviorSubject, Subject } from 'rxjs'; + +import { + DataViewsServicePublic, + MatchedItem, + INDEX_PATTERN_TYPE, + DataViewField, +} from '@kbn/data-views-plugin/public'; + +import { RollupIndicesCapsResponse, MatchedIndicesSet, TimestampOption } from './types'; +import { getMatchedIndices, ensureMinimumTime, extractTimeFields } from './lib'; +import { GetFieldsOptions } from './shared_imports'; + +export class DataViewEditorService { + constructor(private http: HttpSetup, private dataViews: DataViewsServicePublic) { + this.rollupCapsResponse = this.getRollupIndexCaps(); + } + + rollupIndicesCapabilities$ = new BehaviorSubject({}); + isLoadingSources$ = new BehaviorSubject(false); + + loadingTimestampFields$ = new BehaviorSubject(false); + timestampFieldOptions$ = new Subject(); + + matchedIndices$ = new BehaviorSubject({ + allIndices: [], + exactMatchedIndices: [], + partialMatchedIndices: [], + visibleIndices: [], + }); + + private rollupCapsResponse: Promise; + + private currentLoadingTimestampFields = 0; + + private getRollupIndexCaps = async () => { + let response: RollupIndicesCapsResponse = {}; + try { + response = await this.http.get('/api/rollup/indices'); + } catch (e) { + // Silently swallow failure responses such as expired trials + } + this.rollupIndicesCapabilities$.next(response); + return response; + }; + + private getRollupIndices = (rollupCaps: RollupIndicesCapsResponse) => Object.keys(rollupCaps); + + getIsRollupIndex = async () => { + const response = await this.rollupCapsResponse; + return (indexName: string) => this.getRollupIndices(response).includes(indexName); + }; + + loadMatchedIndices = async ( + query: string, + allowHidden: boolean, + allSources: MatchedItem[] + ): Promise<{ + matchedIndicesResult: MatchedIndicesSet; + exactMatched: MatchedItem[]; + partialMatched: MatchedItem[]; + }> => { + const indexRequests = []; + + if (query?.endsWith('*')) { + const exactMatchedQuery = this.getIndicesCached({ + pattern: query, + showAllIndices: allowHidden, + }); + indexRequests.push(exactMatchedQuery); + // provide default value when not making a request for the partialMatchQuery + indexRequests.push(Promise.resolve([])); + } else { + const exactMatchQuery = this.getIndicesCached({ + pattern: query, + showAllIndices: allowHidden, + }); + const partialMatchQuery = this.getIndicesCached({ + pattern: `${query}*`, + showAllIndices: allowHidden, + }); + + indexRequests.push(exactMatchQuery); + indexRequests.push(partialMatchQuery); + } + + const [exactMatched, partialMatched] = (await ensureMinimumTime( + indexRequests + )) as MatchedItem[][]; + + const matchedIndicesResult = getMatchedIndices( + allSources, + partialMatched, + exactMatched, + allowHidden + ); + + this.matchedIndices$.next(matchedIndicesResult); + return { matchedIndicesResult, exactMatched, partialMatched }; + }; + + loadIndices = async (title: string, allowHidden: boolean) => { + const allSrcs = await this.getIndicesCached({ + pattern: '*', + showAllIndices: allowHidden, + }); + + const matchedSet = await this.loadMatchedIndices(title, allowHidden, allSrcs); + + this.isLoadingSources$.next(false); + const matchedIndices = getMatchedIndices( + allSrcs, + matchedSet.partialMatched, + matchedSet.exactMatched, + allowHidden + ); + + this.matchedIndices$.next(matchedIndices); + return matchedIndices; + }; + + loadDataViewNames = async (dataViewName?: string) => { + const dataViewListItems = await this.dataViews.getIdsWithTitle(dataViewName ? true : false); + const dataViewNames = dataViewListItems.map((item) => item.name || item.title); + return dataViewName ? dataViewNames.filter((v) => v !== dataViewName) : dataViewNames; + }; + + private getIndicesMemory: Record> = {}; + getIndicesCached = async (props: { pattern: string; showAllIndices?: boolean | undefined }) => { + const key = JSON.stringify(props); + + const getIndicesPromise = this.getIsRollupIndex().then((isRollupIndex) => + this.dataViews.getIndices({ ...props, isRollupIndex }) + ); + this.getIndicesMemory[key] = this.getIndicesMemory[key] || getIndicesPromise; + + getIndicesPromise.catch(() => { + delete this.getIndicesMemory[key]; + }); + + return await getIndicesPromise; + }; + + private timeStampOptionsMemory: Record> = {}; + private getTimestampOptionsForWildcard = async ( + getFieldsOptions: GetFieldsOptions, + requireTimestampField: boolean + ) => { + const fields = await ensureMinimumTime(this.dataViews.getFieldsForWildcard(getFieldsOptions)); + return extractTimeFields(fields as DataViewField[], requireTimestampField); + }; + + private getTimestampOptionsForWildcardCached = async ( + getFieldsOptions: GetFieldsOptions, + requireTimestampField: boolean + ) => { + const key = JSON.stringify(getFieldsOptions) + requireTimestampField; + + const getTimestampOptionsPromise = this.getTimestampOptionsForWildcard( + getFieldsOptions, + requireTimestampField + ); + this.timeStampOptionsMemory[key] = + this.timeStampOptionsMemory[key] || getTimestampOptionsPromise; + + getTimestampOptionsPromise.catch(() => { + delete this.timeStampOptionsMemory[key]; + }); + + return await getTimestampOptionsPromise; + }; + + loadTimestampFields = async ( + index: string, + type: INDEX_PATTERN_TYPE, + requireTimestampField: boolean, + rollupIndex?: string + ) => { + if (this.matchedIndices$.getValue().exactMatchedIndices.length === 0) { + this.timestampFieldOptions$.next([]); + return; + } + const currentLoadingTimestampFieldsIdx = ++this.currentLoadingTimestampFields; + this.loadingTimestampFields$.next(true); + const getFieldsOptions: GetFieldsOptions = { + pattern: index, + }; + if (type === INDEX_PATTERN_TYPE.ROLLUP) { + getFieldsOptions.type = INDEX_PATTERN_TYPE.ROLLUP; + getFieldsOptions.rollupIndex = rollupIndex; + } + + let timestampOptions: TimestampOption[] = []; + try { + timestampOptions = await this.getTimestampOptionsForWildcardCached( + getFieldsOptions, + requireTimestampField + ); + } finally { + if (currentLoadingTimestampFieldsIdx === this.currentLoadingTimestampFields) { + this.timestampFieldOptions$.next(timestampOptions); + this.loadingTimestampFields$.next(false); + } + } + }; +} diff --git a/src/plugins/data_view_editor/public/open_editor.tsx b/src/plugins/data_view_editor/public/open_editor.tsx index 29ea140daef4b..3f998601f38f1 100644 --- a/src/plugins/data_view_editor/public/open_editor.tsx +++ b/src/plugins/data_view_editor/public/open_editor.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { CoreStart, OverlayRef } from '@kbn/core/public'; import { I18nProvider } from '@kbn/i18n-react'; -import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { createKibanaReactContext, toMountPoint, DataPublicPluginStart } from './shared_imports'; @@ -20,7 +20,7 @@ import { DataViewEditorLazy } from './components/data_view_editor_lazy'; interface Dependencies { core: CoreStart; searchClient: DataPublicPluginStart['search']['search']; - dataViews: DataViewsPublicPluginStart; + dataViews: DataViewsServicePublic; } export const getEditorOpener = diff --git a/src/plugins/data_view_editor/public/plugin.test.tsx b/src/plugins/data_view_editor/public/plugin.test.tsx index 37e8002067613..a95da2edc44f8 100644 --- a/src/plugins/data_view_editor/public/plugin.test.tsx +++ b/src/plugins/data_view_editor/public/plugin.test.tsx @@ -24,6 +24,8 @@ import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/m import { DataViewEditorLazy } from './components/data_view_editor_lazy'; import { DataViewEditorPlugin } from './plugin'; +import { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; + const noop = () => {}; describe('DataViewEditorPlugin', () => { @@ -31,7 +33,7 @@ describe('DataViewEditorPlugin', () => { const pluginStart = { data: dataPluginMock.createStartContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), - dataViews: dataPluginMock.createStartContract().dataViews, + dataViews: dataPluginMock.createStartContract().dataViews as DataViewsServicePublic, }; let plugin: DataViewEditorPlugin; diff --git a/src/plugins/data_view_editor/public/types.ts b/src/plugins/data_view_editor/public/types.ts index b5e506db4d3e9..93fe26ab47627 100644 --- a/src/plugins/data_view_editor/public/types.ts +++ b/src/plugins/data_view_editor/public/types.ts @@ -20,7 +20,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import type { DataView, - DataViewsPublicPluginStart, + DataViewsServicePublic, INDEX_PATTERN_TYPE, MatchedItem, } from '@kbn/data-views-plugin/public'; @@ -33,7 +33,7 @@ export interface DataViewEditorContext { notifications: NotificationsStart; application: ApplicationStart; overlays: OverlayStart; - dataViews: DataViewsPublicPluginStart; + dataViews: DataViewsServicePublic; searchClient: DataPublicPluginStart['search']['search']; } @@ -87,7 +87,7 @@ export interface SetupPlugins {} export interface StartPlugins { data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; + dataViews: DataViewsServicePublic; } export type CloseEditor = () => void; diff --git a/test/functional/apps/management/_index_pattern_create_delete.ts b/test/functional/apps/management/_index_pattern_create_delete.ts index 398489d415c70..e9ceba4439a03 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.ts +++ b/test/functional/apps/management/_index_pattern_create_delete.ts @@ -128,6 +128,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); }); + it('can save with same name', async () => { + await PageObjects.settings.editIndexPattern( + 'logstash-*,hello_world*', + '@timestamp', + 'Logstash Star', + true + ); + + await retry.try(async () => { + expect(await testSubjects.getVisibleText('indexPatternTitle')).to.contain( + `Logstash Star` + ); + }); + }); it('shows edit confirm message when editing index-pattern', async () => { await PageObjects.settings.editIndexPattern( 'logstash-2*', From b24732a96658007a88341de292aff15a9a172b0e Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Tue, 18 Oct 2022 15:32:52 +0200 Subject: [PATCH 31/74] [Osquery] Refactor addToCases and addToTimeline usage (#142365) --- .../osquery/public/cases/add_to_cases.tsx | 14 ++- .../public/cases/add_to_cases_button.tsx | 4 +- .../public/live_queries/form/index.tsx | 38 ++------ .../form/pack_queries_status_table.tsx | 78 +++++---------- .../live_queries/form/pack_results_header.tsx | 95 +++++++++++-------- .../osquery/public/live_queries/index.tsx | 4 - .../osquery/public/results/results_table.tsx | 33 ++++--- .../routes/live_queries/details/index.tsx | 17 +--- .../public/routes/saved_queries/edit/tabs.tsx | 22 +---- .../pack_queries_attachment_wrapper.tsx | 42 +++----- .../osquery_action/index.tsx | 4 - .../pack_field_wrapper.tsx | 7 -- .../osquery_results/index.tsx | 2 - .../osquery_results/osquery_result.tsx | 79 ++++++--------- .../osquery_results/types.ts | 3 - .../timelines/add_to_timeline_button.tsx | 64 +++++++++++++ .../public/timelines/get_add_to_timeline.tsx | 58 ----------- .../event_details/add_to_timeline_button.tsx | 53 ----------- .../components/event_details/osquery_tab.tsx | 9 +- .../components/osquery/osquery_flyout.tsx | 4 - 20 files changed, 230 insertions(+), 400 deletions(-) create mode 100644 x-pack/plugins/osquery/public/timelines/add_to_timeline_button.tsx delete mode 100644 x-pack/plugins/osquery/public/timelines/get_add_to_timeline.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/add_to_timeline_button.tsx diff --git a/x-pack/plugins/osquery/public/cases/add_to_cases.tsx b/x-pack/plugins/osquery/public/cases/add_to_cases.tsx index bc46a76edf361..2cd74e0a23bc8 100644 --- a/x-pack/plugins/osquery/public/cases/add_to_cases.tsx +++ b/x-pack/plugins/osquery/public/cases/add_to_cases.tsx @@ -5,21 +5,29 @@ * 2.0. */ -import React from 'react'; +import React, { useContext } from 'react'; + +import { CasesAttachmentWrapperContext } from '../shared_components/attachments/pack_queries_attachment_wrapper'; import { useKibana } from '../common/lib/kibana'; import type { AddToCaseButtonProps } from './add_to_cases_button'; import { AddToCaseButton } from './add_to_cases_button'; const CASES_OWNER: string[] = []; -export const AddToCaseWrapper: React.FC = React.memo((props) => { +export const AddToCaseWrapper = React.memo((props) => { const { cases } = useKibana().services; + const isCasesAttachment = useContext(CasesAttachmentWrapperContext); + + if (isCasesAttachment || !props.actionId) { + return <>; + } + const casePermissions = cases.helpers.canUseCases(); const CasesContext = cases.ui.getCasesContext(); return ( - {' '} + ); }); diff --git a/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx b/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx index 7124ebd955490..03b5bcebfed40 100644 --- a/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx +++ b/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx @@ -20,7 +20,7 @@ const ADD_TO_CASE = i18n.translate( ); export interface AddToCaseButtonProps { - queryId: string; + queryId?: string; agentIds?: string[]; actionId: string; isIcon?: boolean; @@ -67,6 +67,7 @@ export const AddToCaseButton: React.FC = ({ iconType={'casesApp'} onClick={handleClick} isDisabled={isDisabled || !hasCasesPermissions} + aria-label={ADD_TO_CASE} {...iconProps} /> @@ -79,6 +80,7 @@ export const AddToCaseButton: React.FC = ({ iconType="casesApp" onClick={handleClick} isDisabled={isDisabled || !hasCasesPermissions} + aria-label={ADD_TO_CASE} > {ADD_TO_CASE} diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index aa3a1bd336607..80882d85222e3 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -12,8 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm as useHookForm, FormProvider } from 'react-hook-form'; import { isEmpty, find, pickBy } from 'lodash'; -import { AddToCaseWrapper } from '../../cases/add_to_cases'; -import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline'; +import { PLUGIN_NAME as OSQUERY_PLUGIN_NAME } from '../../../common'; import { QueryPackSelectable } from './query_pack_selectable'; import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form'; import { useKibana } from '../../common/lib/kibana'; @@ -56,7 +55,6 @@ interface LiveQueryFormProps { formType?: FormType; enabled?: boolean; hideAgentsField?: boolean; - addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement; } const LiveQueryFormComponent: React.FC = ({ @@ -66,9 +64,9 @@ const LiveQueryFormComponent: React.FC = ({ formType = 'steps', enabled = true, hideAgentsField = false, - addToTimeline, }) => { - const permissions = useKibana().services.application.capabilities.osquery; + const { application, appName } = useKibana().services; + const permissions = application.capabilities.osquery; const canRunPacks = useMemo( () => !!((permissions.runSavedQueries || permissions.writeLiveQueries) && permissions.readPacks), @@ -211,26 +209,6 @@ const LiveQueryFormComponent: React.FC = ({ const singleQueryDetails = useMemo(() => liveQueryDetails?.queries?.[0], [liveQueryDetails]); const liveQueryActionId = useMemo(() => liveQueryDetails?.action_id, [liveQueryDetails]); - const agentIds = useMemo(() => liveQueryDetails?.agents, [liveQueryDetails?.agents]); - - const addToCaseButton = useCallback( - (payload) => { - if (liveQueryActionId) { - return ( - - ); - } - - return <>; - }, - [agentIds, liveQueryActionId] - ); const resultsStepContent = useMemo( () => @@ -240,8 +218,7 @@ const LiveQueryFormComponent: React.FC = ({ ecsMapping={serializedData.ecs_mapping} endDate={singleQueryDetails?.expiration} agentIds={singleQueryDetails?.agents} - addToTimeline={addToTimeline} - addToCase={addToCaseButton} + liveQueryActionId={liveQueryActionId} /> ) : null, [ @@ -249,8 +226,7 @@ const LiveQueryFormComponent: React.FC = ({ singleQueryDetails?.expiration, singleQueryDetails?.agents, serializedData.ecs_mapping, - addToTimeline, - addToCaseButton, + liveQueryActionId, ] ); @@ -330,9 +306,7 @@ const LiveQueryFormComponent: React.FC = ({ {queryType === 'pack' ? ( ) : ( @@ -349,7 +323,7 @@ const LiveQueryFormComponent: React.FC = ({ {showSavedQueryFlyout ? ( diff --git a/x-pack/plugins/osquery/public/live_queries/form/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/live_queries/form/pack_queries_status_table.tsx index 3d5b888412c33..52f5f0897cb31 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/pack_queries_status_table.tsx @@ -6,7 +6,6 @@ */ import { get, map } from 'lodash'; -import type { ReactElement } from 'react'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { EuiBasicTable, @@ -24,7 +23,6 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import type { ECSMapping } from '@kbn/osquery-io-ts-types'; -import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline'; import { PackResultsHeader } from './pack_results_header'; import { Direction } from '../../../common/search_strategy'; import { removeMultilines } from '../../../common/utils/build_query/remove_multilines'; @@ -32,6 +30,8 @@ import { ResultTabs } from '../../routes/saved_queries/edit/tabs'; import type { PackItem } from '../../packs/types'; import { PackViewInLensAction } from '../../lens/pack_view_in_lens'; import { PackViewInDiscoverAction } from '../../discover/pack_view_in_discover'; +import { AddToCaseWrapper } from '../../cases/add_to_cases'; +import { AddToTimelineButton } from '../../timelines/add_to_timeline_button'; const TruncateTooltipText = styled.div` width: 100%; @@ -116,22 +116,10 @@ type PackQueryStatusItem = Partial<{ interface PackQueriesStatusTableProps { agentIds?: string[]; queryId?: string; - actionId?: string; + actionId: string | undefined; data?: PackQueryStatusItem[]; startDate?: string; expirationDate?: string; - addToTimeline?: (payload: AddToTimelinePayload) => ReactElement; - addToCase?: ({ - actionId, - isIcon, - isDisabled, - queryId, - }: { - actionId?: string; - isIcon?: boolean; - isDisabled?: boolean; - queryId?: string; - }) => ReactElement; showResultsHeader?: boolean; } @@ -142,8 +130,6 @@ const PackQueriesStatusTableComponent: React.FC = ( data, startDate, expirationDate, - addToTimeline, - addToCase, showResultsHeader, }) => { const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>({}); @@ -199,22 +185,7 @@ const PackQueriesStatusTableComponent: React.FC = ( ); const renderLensResultsAction = useCallback((item) => , []); - const handleAddToCase = useCallback( - (payload: { actionId?: string; isIcon?: boolean; queryId: string }) => - // eslint-disable-next-line react/display-name - () => { - if (addToCase) { - return addToCase({ - actionId: payload.actionId, - isIcon: payload.isIcon, - queryId: payload.queryId, - }); - } - return <>; - }, - [addToCase] - ); const getHandleErrorsToggle = useCallback( (item) => () => { setItemIdToExpandedRowMap((prevValue) => { @@ -226,14 +197,13 @@ const PackQueriesStatusTableComponent: React.FC = ( @@ -243,7 +213,7 @@ const PackQueriesStatusTableComponent: React.FC = ( return itemIdToExpandedRowMapValues; }); }, - [startDate, expirationDate, agentIds, addToTimeline, addToCase, handleAddToCase, actionId] + [actionId, startDate, expirationDate, agentIds] ); const renderToggleResultsAction = useCallback( @@ -272,24 +242,27 @@ const PackQueriesStatusTableComponent: React.FC = ( render: renderLensResultsAction, }, { - render: (item: { action_id: string }) => - addToTimeline && addToTimeline({ query: ['action_id', item.action_id], isIcon: true }), + render: (item: { action_id: string }) => ( + + ), }, { render: (item: { action_id: string }) => - addToCase && - addToCase({ - actionId, - queryId: item.action_id, - isIcon: true, - isDisabled: !item.action_id, - }), + actionId && ( + + ), }, ]; return resultActions.map((action) => action.render(row)); }, - [actionId, addToCase, addToTimeline, renderDiscoverResultsAction, renderLensResultsAction] + [actionId, agentIds, renderDiscoverResultsAction, renderLensResultsAction] ); const columns = useMemo( () => [ @@ -377,19 +350,16 @@ const PackQueriesStatusTableComponent: React.FC = ( } }, [agentIds?.length, data, getHandleErrorsToggle, itemIdToExpandedRowMap]); - const queryIds = useMemo( - () => - map(data, (query) => ({ - value: query.action_id || '', - field: 'action_id', - })), - [data] - ); + const queryIds = useMemo(() => map(data, (query) => query.action_id), [data]); return ( <> {showResultsHeader && ( - + )} ; - }) => ReactElement; - queryIds: Array<{ value: string; field: string }>; + queryIds: string[]; + agentIds?: string[]; } const StyledResultsHeading = styled(EuiFlexItem)` @@ -33,35 +28,53 @@ const StyledIconsList = styled(EuiFlexItem)` padding-left: 10px; `; -export const PackResultsHeader = ({ actionId, addToCase }: PackResultsHeadersProps) => ( - <> - - - - -

      - -

      -
      -
      - - - {actionId && - addToCase && - addToCase({ - isIcon: true, - iconProps: { - color: 'text', - size: 'xs', - iconSize: 'l', - }, - })} - - -
      - - +export const PackResultsHeader = React.memo( + ({ actionId, agentIds, queryIds }) => { + const iconProps = useMemo(() => ({ color: 'text', size: 'xs', iconSize: 'l' }), []); + + return ( + <> + + + + +

      + +

      +
      +
      + + + {actionId && ( + + + + + + + + + )} + + +
      + + + ); + } ); + +PackResultsHeader.displayName = 'PackResultsHeader'; diff --git a/x-pack/plugins/osquery/public/live_queries/index.tsx b/x-pack/plugins/osquery/public/live_queries/index.tsx index 67b6194065c81..c0900c8565d37 100644 --- a/x-pack/plugins/osquery/public/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/index.tsx @@ -11,7 +11,6 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { ECSMapping } from '@kbn/osquery-io-ts-types'; -import type { AddToTimelinePayload } from '../timelines/get_add_to_timeline'; import { LiveQueryForm } from './form'; import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; @@ -35,7 +34,6 @@ interface LiveQueryProps { hideAgentsField?: boolean; packId?: string; agentSelection?: AgentSelection; - addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement; } const LiveQueryComponent: React.FC = ({ @@ -55,7 +53,6 @@ const LiveQueryComponent: React.FC = ({ hideAgentsField, packId, agentSelection, - addToTimeline, }) => { const { data: hasActionResultsPrivileges, isLoading } = useActionResultsPrivileges(); @@ -132,7 +129,6 @@ const LiveQueryComponent: React.FC = ({ formType={formType} enabled={enabled} hideAgentsField={hideAgentsField} - addToTimeline={addToTimeline} /> ); }; diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index a0be9d6d3d2fb..c8912328fa463 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -28,7 +28,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; import type { ECSMapping } from '@kbn/osquery-io-ts-types'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; -import type { AddToTimelinePayload } from '../timelines/get_add_to_timeline'; +import { AddToTimelineButton } from '../timelines/add_to_timeline_button'; import { useAllResults } from './use_all_results'; import type { ResultEdges } from '../../common/search_strategy'; import { Direction } from '../../common/search_strategy'; @@ -41,7 +41,8 @@ import { ViewResultsActionButtonType, } from '../packs/pack_queries_status_table'; import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; -import { OSQUERY_INTEGRATION_NAME } from '../../common'; +import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME as OSQUERY_PLUGIN_NAME } from '../../common'; +import { AddToCaseWrapper } from '../cases/add_to_cases'; const DataContext = createContext([]); @@ -52,8 +53,7 @@ export interface ResultsTableComponentProps { ecsMapping?: ECSMapping; endDate?: string; startDate?: string; - addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement; - addToCase?: ({ actionId }: { actionId?: string }) => React.ReactElement; + liveQueryActionId?: string; } const ResultsTableComponent: React.FC = ({ @@ -62,8 +62,7 @@ const ResultsTableComponent: React.FC = ({ ecsMapping, startDate, endDate, - addToTimeline, - addToCase, + liveQueryActionId, }) => { const [isLive, setIsLive] = useState(true); const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); @@ -82,7 +81,11 @@ const ResultsTableComponent: React.FC = ({ skip: !hasActionResultsPrivileges, }); const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]); - const { getUrlForApp } = useKibana().services.application; + const { + application: { getUrlForApp }, + appName, + timelines, + } = useKibana().services; const getFleetAppUrl = useCallback( (agentId) => @@ -305,7 +308,7 @@ const ResultsTableComponent: React.FC = ({ const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { const data = allResultsData?.edges; - if (addToTimeline && data) { + if (timelines && data) { return [ { id: 'timeline', @@ -317,19 +320,19 @@ const ResultsTableComponent: React.FC = ({ }; const eventId = data[visibleRowIndex]?._id; - return addToTimeline({ query: ['_id', eventId], isIcon: true }); + return ; }, }, ]; } return []; - }, [addToTimeline, allResultsData?.edges]); + }, [allResultsData?.edges, timelines]); const toolbarVisibility = useMemo( () => ({ showDisplaySelector: false, - showFullScreenSelector: !addToTimeline, + showFullScreenSelector: appName === OSQUERY_PLUGIN_NAME, additionalControls: ( <> = ({ endDate={endDate} startDate={startDate} /> - {addToTimeline && addToTimeline({ query: ['action_id', actionId] })} - {addToCase && addToCase({ actionId })} + + {liveQueryActionId && ( + + )} ), }), - [actionId, addToCase, addToTimeline, endDate, startDate] + [actionId, agentIds, appName, endDate, liveQueryActionId, startDate] ); useEffect( diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx index bf35a529137f8..12097bc35d207 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx @@ -7,11 +7,10 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import React, { useLayoutEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { AddToCaseWrapper } from '../../../cases/add_to_cases'; import { useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useLiveQueryDetails } from '../../../actions/use_live_query_details'; @@ -58,19 +57,6 @@ const LiveQueryDetailsPageComponent = () => { useLayoutEffect(() => { setIsLive(() => !(data?.status === 'completed')); }, [data?.status]); - const addToCaseButton = useCallback( - (payload) => ( - - ), - [data?.agents, actionId] - ); return ( @@ -81,7 +67,6 @@ const LiveQueryDetailsPageComponent = () => { startDate={data?.['@timestamp']} expirationDate={data?.expiration} agentIds={data?.agents} - addToCase={addToCaseButton} showResultsHeader /> diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx index f4de8b04cb472..2cf2b75b8e744 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx @@ -7,10 +7,8 @@ import { EuiTabbedContent, EuiNotificationBadge } from '@elastic/eui'; import React, { useMemo } from 'react'; -import type { ReactElement } from 'react'; import type { ECSMapping } from '@kbn/osquery-io-ts-types'; -import type { AddToTimelinePayload } from '../../../timelines/get_add_to_timeline'; import { ResultsTable } from '../../../results/results_table'; import { ActionResultsSummary } from '../../../action_results/action_results_summary'; @@ -21,8 +19,7 @@ interface ResultTabsProps { ecsMapping?: ECSMapping; failedAgentsCount?: number; endDate?: string; - addToTimeline?: (payload: AddToTimelinePayload) => ReactElement; - addToCase?: ({ actionId }: { actionId?: string }) => ReactElement; + liveQueryActionId?: string; } const ResultTabsComponent: React.FC = ({ @@ -32,8 +29,7 @@ const ResultTabsComponent: React.FC = ({ endDate, failedAgentsCount, startDate, - addToTimeline, - addToCase, + liveQueryActionId, }) => { const tabs = useMemo( () => [ @@ -47,8 +43,7 @@ const ResultTabsComponent: React.FC = ({ ecsMapping={ecsMapping} startDate={startDate} endDate={endDate} - addToTimeline={addToTimeline} - addToCase={addToCase} + liveQueryActionId={liveQueryActionId} /> ), }, @@ -65,16 +60,7 @@ const ResultTabsComponent: React.FC = ({ ) : null, }, ], - [ - actionId, - agentIds, - ecsMapping, - startDate, - endDate, - addToTimeline, - addToCase, - failedAgentsCount, - ] + [actionId, agentIds, ecsMapping, startDate, endDate, liveQueryActionId, failedAgentsCount] ); return ( diff --git a/x-pack/plugins/osquery/public/shared_components/attachments/pack_queries_attachment_wrapper.tsx b/x-pack/plugins/osquery/public/shared_components/attachments/pack_queries_attachment_wrapper.tsx index c7344f49b32c5..91421938f9b39 100644 --- a/x-pack/plugins/osquery/public/shared_components/attachments/pack_queries_attachment_wrapper.tsx +++ b/x-pack/plugins/osquery/public/shared_components/attachments/pack_queries_attachment_wrapper.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import React, { useCallback, useLayoutEffect, useState } from 'react'; -import { useKibana } from '../../common/lib/kibana'; -import { getAddToTimeline } from '../../timelines/get_add_to_timeline'; +import React, { useLayoutEffect, useState } from 'react'; import { PackQueriesStatusTable } from '../../live_queries/form/pack_queries_status_table'; import { useLiveQueryDetails } from '../../actions/use_live_query_details'; interface PackQueriesAttachmentWrapperProps { - actionId?: string; + actionId: string; agentIds: string[]; queryId: string; } @@ -22,11 +20,7 @@ export const PackQueriesAttachmentWrapper = ({ agentIds, queryId, }: PackQueriesAttachmentWrapperProps) => { - const { - services: { timelines, appName }, - } = useKibana(); const [isLive, setIsLive] = useState(false); - const addToTimelineButton = getAddToTimeline(timelines, appName); const { data } = useLiveQueryDetails({ actionId, @@ -38,26 +32,18 @@ export const PackQueriesAttachmentWrapper = ({ setIsLive(() => !(data?.status === 'completed')); }, [data?.status]); - const addToTimeline = useCallback( - (payload) => { - if (!actionId || !addToTimelineButton) { - return <>; - } - - return addToTimelineButton(payload); - }, - [actionId, addToTimelineButton] - ); - return ( - + + + ); }; + +export const CasesAttachmentWrapperContext = React.createContext(false); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 3095428d478dc..73e6bdb889475 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { OsqueryEmptyPrompt, OsqueryNotAvailablePrompt } from '../prompts'; -import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline'; import { AGENT_STATUS_ERROR, PERMISSION_DENIED, SHORT_EMPTY_TITLE } from './translations'; import { useKibana } from '../../common/lib/kibana'; import { LiveQuery } from '../../live_queries'; @@ -22,7 +21,6 @@ export interface OsqueryActionProps { defaultValues?: {}; formType: 'steps' | 'simple'; hideAgentsField?: boolean; - addToTimeline?: (payload: AddToTimelinePayload) => React.ReactElement; } const OsqueryActionComponent: React.FC = ({ @@ -30,7 +28,6 @@ const OsqueryActionComponent: React.FC = ({ formType = 'simple', defaultValues, hideAgentsField, - addToTimeline, }) => { const permissions = useKibana().services.application.capabilities.osquery; @@ -94,7 +91,6 @@ const OsqueryActionComponent: React.FC = ({ formType={formType} agentId={agentId} hideAgentsField={hideAgentsField} - addToTimeline={addToTimeline} {...defaultValues} /> ); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx index a2ff82094ede7..4cb2d3d401a2d 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_response_action_type/pack_field_wrapper.tsx @@ -6,7 +6,6 @@ */ import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { find } from 'lodash'; import { useWatch } from 'react-hook-form'; @@ -25,17 +24,13 @@ interface PackFieldWrapperProps { action_id?: string; agents?: string[]; }; - addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement; submitButtonContent?: React.ReactNode; - addToCase?: ({ actionId }: { actionId?: string }) => ReactElement; showResultsHeader?: boolean; } export const PackFieldWrapper = ({ liveQueryDetails, - addToTimeline, submitButtonContent, - addToCase, showResultsHeader, }: PackFieldWrapperProps) => { const { data: packsData } = usePacks({}); @@ -69,8 +64,6 @@ export const PackFieldWrapper = ({ agentIds={agentIds} // @ts-expect-error update types data={liveQueryDetails?.queries ?? selectedPackData?.attributes?.queries} - addToTimeline={addToTimeline} - addToCase={addToCase} showResultsHeader={showResultsHeader} /> diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx index 9f6a71406507e..84dc4203b2924 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/index.tsx @@ -24,7 +24,6 @@ const OsqueryActionResultsComponent: React.FC = ({ agentIds, ruleName, alertId, - addToTimeline, }) => { const { data: actionsData } = useAllLiveQueries({ filterQuery: { term: { alert_ids: alertId } }, @@ -49,7 +48,6 @@ const OsqueryActionResultsComponent: React.FC = ({ queryId={queryId} startDate={startDate} ruleName={ruleName} - addToTimeline={addToTimeline} agentIds={agentIds} /> ); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx index b0d0691c14f7d..333e09efce1fb 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx @@ -6,10 +6,9 @@ */ import { EuiComment, EuiSpacer } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React from 'react'; import { FormattedRelative } from '@kbn/i18n-react'; -import { AddToCaseWrapper } from '../../cases/add_to_cases'; import type { OsqueryActionResultsProps } from './types'; import { useLiveQueryDetails } from '../../actions/use_live_query_details'; import { ATTACHED_QUERY } from '../../agents/translations'; @@ -21,52 +20,32 @@ interface OsqueryResultProps extends Omit startDate: string; } -export const OsqueryResult = ({ - actionId, - ruleName, - addToTimeline, - agentIds, - startDate, -}: OsqueryResultProps) => { - const { data } = useLiveQueryDetails({ - actionId, - }); +export const OsqueryResult = React.memo( + ({ actionId, ruleName, agentIds, startDate }) => { + const { data } = useLiveQueryDetails({ + actionId, + }); - const addToCaseButton = useCallback( - (payload) => ( - - ), - [data?.agents, actionId] - ); - - return ( -
      - - } - event={ATTACHED_QUERY} - data-test-subj={'osquery-results-comment'} - > - - - -
      - ); -}; + return ( +
      + + } + event={ATTACHED_QUERY} + data-test-subj={'osquery-results-comment'} + > + + + +
      + ); + } +); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts b/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts index af73a863cfd58..7d8d268be2830 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/types.ts @@ -5,11 +5,8 @@ * 2.0. */ -import type React from 'react'; - export interface OsqueryActionResultsProps { agentIds?: string[]; ruleName?: string[]; alertId: string; - addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement; } diff --git a/x-pack/plugins/osquery/public/timelines/add_to_timeline_button.tsx b/x-pack/plugins/osquery/public/timelines/add_to_timeline_button.tsx new file mode 100644 index 0000000000000..3d6e476bf02f1 --- /dev/null +++ b/x-pack/plugins/osquery/public/timelines/add_to_timeline_button.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useCallback } from 'react'; +import { isArray } from 'lodash'; +import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import { useKibana } from '../common/lib/kibana'; + +const TimelineComponent = React.memo((props) => ); +TimelineComponent.displayName = 'TimelineComponent'; + +export interface AddToTimelineButtonProps { + field: string; + value: string | string[]; + isIcon?: true; + iconProps?: Record; +} + +export const SECURITY_APP_NAME = 'Security'; +export const AddToTimelineButton = (props: AddToTimelineButtonProps) => { + const { timelines, appName } = useKibana().services; + const { field, value, isIcon, iconProps } = props; + + const queryIds = isArray(value) ? value : [value]; + const TimelineIconComponent = useCallback( + (timelineComponentProps) => ( + + ), + [iconProps] + ); + + if (!timelines || appName !== SECURITY_APP_NAME || !queryIds.length) { + return null; + } + + const { getAddToTimelineButton } = timelines.getHoverActions(); + + const providers = queryIds.map((queryId) => ({ + and: [], + enabled: true, + excluded: false, + id: queryId, + kqlQuery: '', + name: queryId, + queryMatch: { + field, + value: queryId, + operator: ':' as const, + }, + })); + + return getAddToTimelineButton({ + dataProvider: providers, + field: queryIds[0], + ownFocus: false, + ...(isIcon + ? { showTooltip: true, Component: TimelineIconComponent } + : { Component: TimelineComponent }), + }); +}; diff --git a/x-pack/plugins/osquery/public/timelines/get_add_to_timeline.tsx b/x-pack/plugins/osquery/public/timelines/get_add_to_timeline.tsx deleted file mode 100644 index 47cfc34679b33..0000000000000 --- a/x-pack/plugins/osquery/public/timelines/get_add_to_timeline.tsx +++ /dev/null @@ -1,58 +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 { EuiButtonEmpty } from '@elastic/eui'; -import type { ServicesWrapperProps } from '../shared_components/services_wrapper'; - -const TimelineComponent = React.memo((props) => ); -TimelineComponent.displayName = 'TimelineComponent'; - -export interface AddToTimelinePayload { - query: [string, string]; - isIcon?: true; -} - -export const SECURITY_APP_NAME = 'Security'; -export const getAddToTimeline = ( - timelines: ServicesWrapperProps['services']['timelines'], - appName?: string -) => { - if (!timelines || appName !== SECURITY_APP_NAME) { - return; - } - - const { getAddToTimelineButton } = timelines.getHoverActions(); - - return (payload: AddToTimelinePayload) => { - const { - query: [field, value], - isIcon, - } = payload; - - const providerA = { - and: [], - enabled: true, - excluded: false, - id: value, - kqlQuery: '', - name: value, - queryMatch: { - field, - value, - operator: ':' as const, - }, - }; - - return getAddToTimelineButton({ - dataProvider: providerA, - field: value, - ownFocus: false, - ...(isIcon ? { showTooltip: true } : { Component: TimelineComponent }), - }); - }; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/add_to_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/add_to_timeline_button.tsx deleted file mode 100644 index f407411bd20a8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/add_to_timeline_button.tsx +++ /dev/null @@ -1,53 +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, { useCallback } from 'react'; -import { EuiButtonEmpty } from '@elastic/eui'; -import type { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { useKibana } from '../../lib/kibana'; - -const TimelineComponent = React.memo((props) => { - return ; -}); -TimelineComponent.displayName = 'TimelineComponent'; - -export const useHandleAddToTimeline = () => { - const { - services: { timelines }, - } = useKibana(); - const { getAddToTimelineButton } = timelines.getHoverActions(); - - return useCallback( - (payload: { query: [string, string]; isIcon?: true }) => { - const { - query: [field, value], - isIcon, - } = payload; - const providerA: DataProvider = { - and: [], - enabled: true, - excluded: false, - id: value, - kqlQuery: '', - name: value, - queryMatch: { - field, - value, - operator: ':', - }, - }; - - return getAddToTimelineButton({ - dataProvider: providerA, - field: value, - ownFocus: false, - ...(isIcon ? { showTooltip: true } : { Component: TimelineComponent }), - }); - }, - [getAddToTimelineButton] - ); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx index 29ed650a9c793..ed2551b94422a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx @@ -22,7 +22,6 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe import { useKibana } from '../../lib/kibana'; import { EventsViewType } from './event_details'; import * as i18n from './translations'; -import { useHandleAddToTimeline } from './add_to_timeline_button'; const TabContentWrapper = styled.div` height: 100%; @@ -64,7 +63,6 @@ export const useOsqueryTab = ({ rawEventData }: { rawEventData?: AlertRawEventDa const { services: { osquery, application }, } = useKibana(); - const handleAddToTimeline = useHandleAddToTimeline(); const responseActionsEnabled = useIsExperimentalFeatureEnabled('responseActionsEnabled'); const emptyPrompt = useMemo( @@ -139,12 +137,7 @@ export const useOsqueryTab = ({ rawEventData }: { rawEventData?: AlertRawEventDa {!application?.capabilities?.osquery?.read ? ( emptyPrompt ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx index bbdf70b094a6b..cd35752d56310 100644 --- a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx @@ -8,7 +8,6 @@ import React from 'react'; import styled from 'styled-components'; import { EuiFlyout, EuiFlyoutFooter, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; -import { useHandleAddToTimeline } from '../../../common/components/event_details/add_to_timeline_button'; import { useKibana } from '../../../common/lib/kibana'; import { OsqueryEventDetailsFooter } from './osquery_flyout_footer'; import { ACTION_OSQUERY } from './translations'; @@ -31,8 +30,6 @@ export const OsqueryFlyoutComponent: React.FC = ({ services: { osquery }, } = useKibana(); - const handleAddToTimeline = useHandleAddToTimeline(); - if (osquery?.OsqueryAction) { return ( = ({ agentId={agentId} formType="steps" defaultValues={defaultValues} - addToTimeline={handleAddToTimeline} /> From ba61ce4a2d8d6a7778878483e981b0584b46e657 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 18 Oct 2022 09:47:10 -0400 Subject: [PATCH 32/74] Add steps to docs to fix Corrupt Saved Objects (#143479) * upgrade mocha to 10.1 --- .../resolving-migration-failures.asciidoc | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 3d3a80fb423ca..6de91d9a58db5 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -62,6 +62,37 @@ Unable to migrate the corrupt saved object document with _id: 'marketing_space:d To delete the documents that cause migrations to fail, take the following steps: +. Create a role as follows: ++ +[source,sh] +-------------------------------------------- +PUT _security/role/grant_kibana_system_indices +{ + "indices": [ + { + "names": [ + ".kibana*" + ], + "privileges": [ + "all" + ], + "allow_restricted_indices": true + } + ] +} +-------------------------------------------- + +. Create a user with the role above and `superuser` built-in role: ++ +[source,sh] +-------------------------------------------- +POST /_security/user/temporarykibanasuperuser +{ + "password" : "l0ng-r4nd0m-p@ssw0rd", + "roles" : [ "superuser", "grant_kibana_system_indices" ] +} +-------------------------------------------- + . Remove the write block which the migration system has placed on the previous index: + [source,sh] From 36b8c148f371f56bc5b5a233c5f55038e5f423e7 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 18 Oct 2022 09:51:28 -0400 Subject: [PATCH 33/74] [Guided onboarding] Support "done" state in dropdown panel (#143124) --- .../public/components/guide_panel.test.tsx | 8 ++- .../public/components/guide_panel.tsx | 54 +++++++++++++++---- .../constants/guides_config/observability.ts | 1 + .../public/constants/guides_config/search.ts | 1 + .../constants/guides_config/security.ts | 5 ++ src/plugins/guided_onboarding/public/types.ts | 5 ++ 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 268117f8af570..4761446f7969c 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -192,8 +192,8 @@ describe('Guided setup', () => { expect(exists('guideProgress')).toBe(true); }); - test('should show the "Continue using Elastic" button when all steps has been completed', async () => { - const { component, exists } = testBed; + test('should show the completed state when all steps has been completed', async () => { + const { component, exists, find } = testBed; const readyToCompleteGuideState: GuideState = { guideId: 'search', @@ -217,6 +217,10 @@ describe('Guided setup', () => { await updateComponentWithState(component, readyToCompleteGuideState, true); + expect(find('guideTitle').text()).toContain('Well done'); + expect(find('guideDescription').text()).toContain( + `You've completed the Elastic Enterprise Search guide` + ); expect(exists('useElasticButton')).toBe(true); }); diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index 84726825240a3..75f1f07cad5bc 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -32,7 +32,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ApplicationStart } from '@kbn/core/public'; import type { GuideState, GuideStep as GuideStepStatus } from '../../common/types'; -import type { StepConfig } from '../types'; +import type { GuideConfig, StepConfig } from '../types'; import type { ApiService } from '../services/api'; import { getGuideConfig } from '../services/helpers'; @@ -99,8 +99,15 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { application.navigateToApp('home', { path: '#getting_started' }); }; - const completeGuide = async () => { + const completeGuide = async ( + completedGuideRedirectLocation: GuideConfig['completedGuideRedirectLocation'] + ) => { await api.completeGuide(guideState!.guideId); + + if (completedGuideRedirectLocation) { + const { appID, path } = completedGuideRedirectLocation; + application.navigateToApp(appID, { path }); + } }; const openQuitGuideModal = () => { @@ -150,6 +157,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { } const stepsCompleted = getProgress(guideState); + const isGuideReadyToComplete = guideState?.status === 'ready_to_complete'; return ( <> @@ -182,7 +190,13 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { -

      {guideConfig?.title}

      +

      + {isGuideReadyToComplete + ? i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutTitle', { + defaultMessage: 'Well done!', + }) + : guideConfig.title} +

      @@ -192,7 +206,19 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
      -

      {guideConfig?.description}

      +

      + {isGuideReadyToComplete + ? i18n.translate( + 'guidedOnboarding.dropdownPanel.completeGuideFlyoutDescription', + { + defaultMessage: `You've completed the Elastic {guideName} guide.`, + values: { + guideName: guideConfig.guideName, + }, + } + ) + : guideConfig.description} +

      {guideConfig.docs && ( @@ -212,9 +238,15 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { { } })} - {guideState?.status === 'ready_to_complete' && ( + {isGuideReadyToComplete && ( - + completeGuide(guideConfig.completedGuideRedirectLocation)} + fill + data-test-subj="useElasticButton" + > {i18n.translate('guidedOnboarding.dropdownPanel.elasticButtonLabel', { defaultMessage: 'Continue using Elastic', })} diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts b/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts index 9fedb88af2253..1ef3c155dfdb5 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts +++ b/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts @@ -11,6 +11,7 @@ import type { GuideConfig } from '../../types'; export const observabilityConfig: GuideConfig = { title: 'Observe my Kubernetes infrastructure', description: `We'll help you quickly gain visibility into your Kubernetes environment using Elastic's out-of-the-box integration. Gain deep insights from your logs, metrics, and traces, and proactively detect issues and take action to resolve issues.`, + guideName: 'Kubernetes', docs: { text: 'Kubernetes documentation', url: 'example.com', // TODO update link to correct docs page diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/search.ts b/src/plugins/guided_onboarding/public/constants/guides_config/search.ts index 3c083a3bd96c1..f1a8de389ea19 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/search.ts +++ b/src/plugins/guided_onboarding/public/constants/guides_config/search.ts @@ -11,6 +11,7 @@ import type { GuideConfig } from '../../types'; export const searchConfig: GuideConfig = { title: 'Search my data', description: `We'll help you build world-class search experiences with your data, using Elastic's out-of-the-box web crawler, connectors, and our robust APIs. Gain deep insights from the built-in search analytics and use that data to inform changes to relevance.`, + guideName: 'Enterprise Search', docs: { text: 'Enterprise Search 101 Documentation', url: 'example.com', diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/security.ts b/src/plugins/guided_onboarding/public/constants/guides_config/security.ts index e67e318a61f01..68ebc849f94c0 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/security.ts +++ b/src/plugins/guided_onboarding/public/constants/guides_config/security.ts @@ -10,6 +10,11 @@ import type { GuideConfig } from '../../types'; export const securityConfig: GuideConfig = { title: 'Get started with SIEM', + guideName: 'Security', + completedGuideRedirectLocation: { + appID: 'security', + path: '/app/security/dashboards', + }, description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.', steps: [ diff --git a/src/plugins/guided_onboarding/public/types.ts b/src/plugins/guided_onboarding/public/types.ts index 0ca4a5801fb15..7ca5d59ce5d73 100755 --- a/src/plugins/guided_onboarding/public/types.ts +++ b/src/plugins/guided_onboarding/public/types.ts @@ -74,10 +74,15 @@ export interface StepConfig { export interface GuideConfig { title: string; description: string; + guideName: string; docs?: { text: string; url: string; }; + completedGuideRedirectLocation?: { + appID: string; + path: string; + }; steps: StepConfig[]; } From c365988e0dddafc3bea4441ff1e2156d6d9f659e Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 18 Oct 2022 09:55:17 -0400 Subject: [PATCH 34/74] Feature/upgrade mocha 10.1 (#143468) * upgrade mocha to 10.1 --- package.json | 2 +- yarn.lock | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 140727a894ca5..d996dc2756a4d 100644 --- a/package.json +++ b/package.json @@ -1382,7 +1382,7 @@ "micromatch": "^4.0.5", "mini-css-extract-plugin": "1.1.0", "minimist": "^1.2.6", - "mocha": "^10.0.0", + "mocha": "^10.1.0", "mocha-junit-reporter": "^2.0.2", "mochawesome": "^7.0.1", "mochawesome-merge": "^4.2.1", diff --git a/yarn.lock b/yarn.lock index 37d42451920af..35cc88efd5eb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9160,11 +9160,6 @@ "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -20516,12 +20511,11 @@ mocha-junit-reporter@^2.0.2: strip-ansi "^6.0.1" xml "^1.0.0" -mocha@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" - integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== +mocha@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" + integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== dependencies: - "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" chokidar "3.5.3" From 6324008c741076227b3c1c26e254ecddfad1edb7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 18 Oct 2022 16:59:08 +0300 Subject: [PATCH 35/74] [Cases] Add the ability to filter for cases without assignees (#143390) * Return unsigneed cases * Fix filter * Change to none * Add and fix testing * PR feedback --- x-pack/plugins/cases/common/constants.ts | 1 + x-pack/plugins/cases/common/ui/types.ts | 4 +- .../all_cases/assignees_filter.test.tsx | 111 +++- .../components/all_cases/assignees_filter.tsx | 31 +- .../components/all_cases/table_filters.tsx | 10 +- .../components/all_cases/translations.ts | 7 + .../components/user_profiles/sort.test.ts | 129 +++- .../public/components/user_profiles/sort.ts | 25 + .../public/components/user_profiles/types.ts | 1 + .../cases/public/containers/api.test.tsx | 127 +++- x-pack/plugins/cases/public/containers/api.ts | 21 +- .../cases/public/containers/utils.test.ts | 36 ++ .../plugins/cases/public/containers/utils.ts | 34 +- .../plugins/cases/server/client/cases/find.ts | 5 +- x-pack/plugins/cases/server/client/types.ts | 22 +- x-pack/plugins/cases/server/client/utils.ts | 49 +- .../tests/trial/cases/assignees.ts | 550 ++++++++++-------- .../test/functional/services/cases/common.ts | 11 + x-pack/test/functional/services/cases/list.ts | 14 +- .../apps/cases/list_view.ts | 29 +- 20 files changed, 909 insertions(+), 308 deletions(-) diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 278cfc1050286..89c7de48b257d 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -179,6 +179,7 @@ export const PUSH_CASES_CAPABILITY = 'push_cases' as const; export const DEFAULT_USER_SIZE = 10; export const MAX_ASSIGNEES_PER_CASE = 10; +export const NO_ASSIGNEES_FILTERING_KEYWORD = 'none'; /** * Delays diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index a1cc7ad9eb91f..5fc6b999c3d61 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -104,7 +104,7 @@ export interface FilterOptions { severity: CaseSeverityWithAll; status: CaseStatusWithAllStatus; tags: string[]; - assignees: string[]; + assignees: Array | null; reporters: User[]; owner: string[]; } @@ -127,7 +127,7 @@ export type ElasticUser = SnakeToCamelCase; export interface FetchCasesProps extends ApiProps { queryParams?: QueryParams; - filterOptions?: FilterOptions & { owner: string[] }; + filterOptions?: FilterOptions; } export interface ApiProps { diff --git a/x-pack/plugins/cases/public/components/all_cases/assignees_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/assignees_filter.test.tsx index 0f50eb0421093..c1b0d0cf2445f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/assignees_filter.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/assignees_filter.test.tsx @@ -168,9 +168,9 @@ describe('AssigneesFilterPopover', () => { await waitForEuiPopoverOpen(); const assignees = screen.getAllByRole('option'); - expect(within(assignees[0]).getByText('Wet Dingo')).toBeInTheDocument(); - expect(within(assignees[1]).getByText('Damaged Raccoon')).toBeInTheDocument(); - expect(within(assignees[2]).getByText('Physical Dinosaur')).toBeInTheDocument(); + expect(within(assignees[1]).getByText('Wet Dingo')).toBeInTheDocument(); + expect(within(assignees[2]).getByText('Damaged Raccoon')).toBeInTheDocument(); + expect(within(assignees[3]).getByText('Physical Dinosaur')).toBeInTheDocument(); }); it('does not show the number of filters', async () => { @@ -184,4 +184,109 @@ describe('AssigneesFilterPopover', () => { expect(screen.queryByText('3')).not.toBeInTheDocument(); }); + + it('show the no assignee filter option', async () => { + const props = { + ...defaultProps, + currentUserProfile: userProfiles[2], + }; + + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByText('Wet Dingo')).toBeInTheDocument(); + }); + + await waitForEuiPopoverOpen(); + + expect(screen.getByText('No assignees')).toBeInTheDocument(); + }); + + it('filters cases with no assignees', async () => { + const onSelectionChange = jest.fn(); + const props = { ...defaultProps, onSelectionChange }; + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByPlaceholderText('Search users')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + userEvent.click(screen.getByText('No assignees')); + + expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + null, + ] + `); + }); + + it('filters cases with no assignees and users', async () => { + const onSelectionChange = jest.fn(); + const props = { ...defaultProps, onSelectionChange }; + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByPlaceholderText('Search users')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + userEvent.click(screen.getByText('No assignees')); + userEvent.click(screen.getByText('WD')); + userEvent.click(screen.getByText('damaged_raccoon@elastic.co')); + + expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(` + Array [ + null, + ] + `); + + expect(onSelectionChange.mock.calls[1][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + + expect(onSelectionChange.mock.calls[2][0]).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + ] + `); + }); + + it('hides no assignee filtering when searching', async () => { + const onSelectionChange = jest.fn(); + const props = { ...defaultProps, onSelectionChange }; + appMockRender.render(); + + await waitFor(() => { + userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + expect(screen.getByPlaceholderText('Search users')).toBeInTheDocument(); + }); + await waitForEuiPopoverOpen(); + + fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } }); + expect(screen.queryByText('No assignees')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx index 1cf4f2c9a2bd4..67086f44e1fef 100644 --- a/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/assignees_filter.tsx @@ -6,7 +6,6 @@ */ import { EuiFilterButton } from '@elastic/eui'; -import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { UserProfilesPopover } from '@kbn/user-profile-components'; import { isEmpty } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; @@ -17,14 +16,17 @@ import type { CurrentUserProfile } from '../types'; import { EmptyMessage } from '../user_profiles/empty_message'; import { NoMatches } from '../user_profiles/no_matches'; import { SelectedStatusMessage } from '../user_profiles/selected_status_message'; -import { bringCurrentUserToFrontAndSort } from '../user_profiles/sort'; +import { bringCurrentUserToFrontAndSort, orderAssigneesIncludingNone } from '../user_profiles/sort'; +import type { AssigneesFilteringSelection } from '../user_profiles/types'; import * as i18n from './translations'; +export const NO_ASSIGNEES_VALUE = null; + export interface AssigneesFilterPopoverProps { - selectedAssignees: UserProfileWithAvatar[]; + selectedAssignees: AssigneesFilteringSelection[]; currentUserProfile: CurrentUserProfile; isLoading: boolean; - onSelectionChange: (users: UserProfileWithAvatar[]) => void; + onSelectionChange: (users: AssigneesFilteringSelection[]) => void; } const AssigneesFilterPopoverComponent: React.FC = ({ @@ -42,9 +44,10 @@ const AssigneesFilterPopoverComponent: React.FC = ( const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []); const onChange = useCallback( - (users: UserProfileWithAvatar[]) => { - const sortedUsers = bringCurrentUserToFrontAndSort(currentUserProfile, users); - onSelectionChange(sortedUsers ?? []); + (users: AssigneesFilteringSelection[]) => { + const sortedUsers = orderAssigneesIncludingNone(currentUserProfile, users); + + onSelectionChange(sortedUsers); }, [currentUserProfile, onSelectionChange] ); @@ -77,10 +80,15 @@ const AssigneesFilterPopoverComponent: React.FC = ( onDebounce, }); - const searchResultProfiles = useMemo( - () => bringCurrentUserToFrontAndSort(currentUserProfile, userProfiles), - [userProfiles, currentUserProfile] - ); + const searchResultProfiles = useMemo(() => { + const sortedUsers = bringCurrentUserToFrontAndSort(currentUserProfile, userProfiles) ?? []; + + if (isEmpty(searchTerm)) { + return [null, ...sortedUsers]; + } + + return sortedUsers; + }, [currentUserProfile, userProfiles, searchTerm]); const isLoadingData = isLoading || isLoadingSuggest; @@ -118,6 +126,7 @@ const AssigneesFilterPopoverComponent: React.FC = ( emptyMessage: , noMatchesMessage: !isUserTyping && !isLoadingData ? : , singleSelection: false, + nullOptionLabel: i18n.NO_ASSIGNEES, }} /> ); diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index 553132e606aa6..5032922a06d12 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -10,7 +10,6 @@ import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup, EuiButton } from '@elastic/eui'; -import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import type { CaseStatusWithAllStatus, CaseSeverityWithAll } from '../../../common/ui/types'; import { StatusAll } from '../../../common/ui/types'; import { CaseStatuses } from '../../../common/api'; @@ -24,6 +23,7 @@ import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; import { AssigneesFilterPopover } from './assignees_filter'; import type { CurrentUserProfile } from '../types'; import { useCasesFeatures } from '../../common/use_cases_features'; +import type { AssigneesFilteringSelection } from '../user_profiles/types'; interface CasesTableFiltersProps { countClosedCases: number | null; @@ -68,15 +68,17 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [selectedOwner, setSelectedOwner] = useState([]); - const [selectedAssignees, setSelectedAssignees] = useState([]); + const [selectedAssignees, setSelectedAssignees] = useState([]); const { data: tags = [] } = useGetTags(); const { caseAssignmentAuthorized } = useCasesFeatures(); const handleSelectedAssignees = useCallback( - (newAssignees: UserProfileWithAvatar[]) => { + (newAssignees: AssigneesFilteringSelection[]) => { if (!isEqual(newAssignees, selectedAssignees)) { setSelectedAssignees(newAssignees); - onFilterChanged({ assignees: newAssignees.map((assignee) => assignee.uid) }); + onFilterChanged({ + assignees: newAssignees.map((assignee) => assignee?.uid ?? null), + }); } }, [selectedAssignees, onFilterChanged] diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index 332c0d493101b..aedfefd35c360 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -130,3 +130,10 @@ export const TOTAL_ASSIGNEES_FILTERED = (total: number) => defaultMessage: '{total, plural, one {# assignee} other {# assignees}} filtered', values: { total }, }); + +export const NO_ASSIGNEES = i18n.translate( + 'xpack.cases.allCasesView.filterAssignees.noAssigneesLabel', + { + defaultMessage: 'No assignees', + } +); diff --git a/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts b/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts index d2f64a05e7ce1..da6fdf6e9052f 100644 --- a/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts +++ b/x-pack/plugins/cases/public/components/user_profiles/sort.test.ts @@ -6,7 +6,11 @@ */ import { userProfiles } from '../../containers/user_profiles/api.mock'; -import { bringCurrentUserToFrontAndSort, moveCurrentUserToBeginning } from './sort'; +import { + bringCurrentUserToFrontAndSort, + moveCurrentUserToBeginning, + orderAssigneesIncludingNone, +} from './sort'; describe('sort', () => { describe('moveCurrentUserToBeginning', () => { @@ -115,4 +119,127 @@ describe('sort', () => { expect(bringCurrentUserToFrontAndSort(userProfiles[2], undefined)).toBeUndefined(); }); }); + + describe('orderAssigneesIncludingNone', () => { + it('returns a sorted list of users with null', () => { + const unsortedProfiles = [...userProfiles].reverse(); + + expect(orderAssigneesIncludingNone(userProfiles[0], [null, ...unsortedProfiles])) + .toMatchInlineSnapshot(` + Array [ + null, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('returns a sorted list of users without null', () => { + const unsortedProfiles = [...userProfiles].reverse(); + + expect(orderAssigneesIncludingNone(userProfiles[0], unsortedProfiles)).toMatchInlineSnapshot(` + Array [ + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + + it('adds null in the front', () => { + const unsortedProfiles = [...userProfiles].reverse(); + + expect(orderAssigneesIncludingNone(userProfiles[0], [...unsortedProfiles, null])) + .toMatchInlineSnapshot(` + Array [ + null, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", + "user": Object { + "email": "damaged_raccoon@elastic.co", + "full_name": "Damaged Raccoon", + "username": "damaged_raccoon", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", + "user": Object { + "email": "physical_dinosaur@elastic.co", + "full_name": "Physical Dinosaur", + "username": "physical_dinosaur", + }, + }, + Object { + "data": Object {}, + "enabled": true, + "uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0", + "user": Object { + "email": "wet_dingo@elastic.co", + "full_name": "Wet Dingo", + "username": "wet_dingo", + }, + }, + ] + `); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/user_profiles/sort.ts b/x-pack/plugins/cases/public/components/user_profiles/sort.ts index 83a4608095b0b..12437cdc6647e 100644 --- a/x-pack/plugins/cases/public/components/user_profiles/sort.ts +++ b/x-pack/plugins/cases/public/components/user_profiles/sort.ts @@ -7,7 +7,9 @@ import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { sortBy } from 'lodash'; +import { NO_ASSIGNEES_VALUE } from '../all_cases/assignees_filter'; import type { CurrentUserProfile } from '../types'; +import type { AssigneesFilteringSelection } from './types'; export const getSortField = (profile: UserProfileWithAvatar) => profile.user.full_name?.toLowerCase() ?? @@ -51,3 +53,26 @@ export const sortProfiles = (profiles?: UserProfileWithAvatar[]) => { return sortBy(profiles, getSortField); }; + +export const orderAssigneesIncludingNone = ( + currentUserProfile: CurrentUserProfile, + assignees: AssigneesFilteringSelection[] +) => { + const usersWithNoAssigneeSelection = removeNoAssigneesSelection(assignees); + const sortedUsers = + bringCurrentUserToFrontAndSort(currentUserProfile, usersWithNoAssigneeSelection) ?? []; + + const hasNoAssigneesSelection = assignees.find((assignee) => assignee === NO_ASSIGNEES_VALUE); + + const sortedUsersWithNoAssigneeIfExisted = + hasNoAssigneesSelection !== undefined ? [NO_ASSIGNEES_VALUE, ...sortedUsers] : sortedUsers; + + return sortedUsersWithNoAssigneeIfExisted; +}; + +const removeNoAssigneesSelection = ( + assignees: AssigneesFilteringSelection[] +): UserProfileWithAvatar[] => + assignees.filter( + (assignee): assignee is UserProfileWithAvatar => assignee !== NO_ASSIGNEES_VALUE + ); diff --git a/x-pack/plugins/cases/public/components/user_profiles/types.ts b/x-pack/plugins/cases/public/components/user_profiles/types.ts index 2d2384d21b67b..1e775733f4c3a 100644 --- a/x-pack/plugins/cases/public/components/user_profiles/types.ts +++ b/x-pack/plugins/cases/public/components/user_profiles/types.ts @@ -17,3 +17,4 @@ export interface AssigneeWithProfile extends Assignee { } export type UserInfoWithAvatar = Partial>; +export type AssigneesFilteringSelection = UserProfileWithAvatar | null; diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index d5618ff8a7e9b..d7f522358e5bd 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -186,40 +186,40 @@ describe('Cases API', () => { fetchMock.mockClear(); fetchMock.mockResolvedValue(allCasesSnake); }); - test('should be called with correct check url, method, signal', async () => { + + test('should be called with correct check url, method, signal with empty defaults', async () => { await getCases({ - filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, + filterOptions: DEFAULT_FILTER_OPTIONS, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { method: 'GET', query: { ...DEFAULT_QUERY_PARAMS, searchFields: DEFAULT_FILTER_OPTIONS.searchFields, - assignees: [], - reporters: [], - tags: [], - owner: [SECURITY_SOLUTION_OWNER], }, signal: abortCtrl.signal, }); }); - test('should applies correct filters', async () => { + test('should applies correct all filters', async () => { await getCases({ filterOptions: { - ...DEFAULT_FILTER_OPTIONS, + searchFields: DEFAULT_FILTER_OPTIONS.searchFields, assignees: ['123'], reporters: [{ username: 'username', full_name: null, email: null }], tags, status: CaseStatuses.open, + severity: CaseSeverity.HIGH, search: 'hello', owner: [SECURITY_SOLUTION_OWNER], }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { method: 'GET', query: { @@ -230,6 +230,7 @@ describe('Cases API', () => { search: 'hello', searchFields: DEFAULT_FILTER_OPTIONS.searchFields, status: CaseStatuses.open, + severity: CaseSeverity.HIGH, owner: [SECURITY_SOLUTION_OWNER], }, signal: abortCtrl.signal, @@ -245,14 +246,12 @@ describe('Cases API', () => { queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { method: 'GET', query: { ...DEFAULT_QUERY_PARAMS, searchFields: DEFAULT_FILTER_OPTIONS.searchFields, - assignees: [], - reporters: [], - tags: [], severity: CaseSeverity.HIGH, }, signal: abortCtrl.signal, @@ -268,14 +267,115 @@ describe('Cases API', () => { queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { method: 'GET', query: { ...DEFAULT_QUERY_PARAMS, searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + }, + signal: abortCtrl.signal, + }); + }); + + test('should apply the severity field correctly (with status value)', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + status: CaseStatuses.open, + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + status: CaseStatuses.open, + }, + signal: abortCtrl.signal, + }); + }); + + test('should not send the severity field with "all" status value', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + status: 'all', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + }, + signal: abortCtrl.signal, + }); + }); + + test('should not send the assignees field if it an empty array', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, assignees: [], - reporters: [], - tags: [], + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + }, + signal: abortCtrl.signal, + }); + }); + + test('should convert a single null value to none', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + assignees: null, + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + assignees: 'none', + }, + signal: abortCtrl.signal, + }); + }); + + test('should converts null value in the array to none', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + assignees: [null, '123'], + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + searchFields: DEFAULT_FILTER_OPTIONS.searchFields, + assignees: ['none', '123'], }, signal: abortCtrl.signal, }); @@ -297,6 +397,7 @@ describe('Cases API', () => { queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { method: 'GET', query: { diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 4ce79cf6d22d5..651de220ded2b 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -7,7 +7,6 @@ import type { ValidFeatureId } from '@kbn/rule-data-utils'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants'; -import { isEmpty } from 'lodash'; import type { Cases, CaseUpdateRequest, @@ -71,6 +70,8 @@ import { decodeCaseUserActionsResponse, decodeCaseResolveResponse, decodeSingleCaseMetricsResponse, + constructAssigneesFilter, + constructReportersFilter, } from './utils'; import { decodeCasesFindResponse } from '../api/decoders'; @@ -182,9 +183,9 @@ export const getCases = async ({ const query = { ...(filterOptions.status !== StatusAll ? { status: filterOptions.status } : {}), ...(filterOptions.severity !== SeverityAll ? { severity: filterOptions.severity } : {}), - assignees: filterOptions.assignees, - reporters: constructReportersFilter(filterOptions.reporters), - tags: filterOptions.tags, + ...constructAssigneesFilter(filterOptions.assignees), + ...constructReportersFilter(filterOptions.reporters), + ...(filterOptions.tags.length > 0 ? { tags: filterOptions.tags } : {}), ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), ...(filterOptions.searchFields.length > 0 ? { searchFields: filterOptions.searchFields } : {}), ...(filterOptions.owner.length > 0 ? { owner: filterOptions.owner } : {}), @@ -200,18 +201,6 @@ export const getCases = async ({ return convertAllCasesToCamel(decodeCasesFindResponse(response)); }; -export const constructReportersFilter = (reporters: User[]) => { - return reporters - .map((reporter) => { - if (reporter.profile_uid != null) { - return reporter.profile_uid; - } - - return reporter.username ?? ''; - }) - .filter((reporterID) => !isEmpty(reporterID)); -}; - export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise => { const response = await KibanaServices.get().http.fetch(CASES_URL, { method: 'POST', diff --git a/x-pack/plugins/cases/public/containers/utils.test.ts b/x-pack/plugins/cases/public/containers/utils.test.ts index 0886093b9164f..fbb9f7a72d1d0 100644 --- a/x-pack/plugins/cases/public/containers/utils.test.ts +++ b/x-pack/plugins/cases/public/containers/utils.test.ts @@ -9,6 +9,8 @@ import { valueToUpdateIsSettings, valueToUpdateIsStatus, createUpdateSuccessToaster, + constructAssigneesFilter, + constructReportersFilter, } from './utils'; import type { Case } from './types'; @@ -143,4 +145,38 @@ describe('utils', () => { }); }); }); + + describe('constructAssigneesFilter', () => { + it('returns an empty object if the array is empty', () => { + expect(constructAssigneesFilter([])).toEqual({}); + }); + + it('returns none if the assignees are null', () => { + expect(constructAssigneesFilter(null)).toEqual({ assignees: 'none' }); + }); + + it('returns none for null values in the assignees array', () => { + expect(constructAssigneesFilter([null, '123'])).toEqual({ assignees: ['none', '123'] }); + }); + }); + + describe('constructReportersFilter', () => { + it('returns an empty object if the array is empty', () => { + expect(constructReportersFilter([])).toEqual({}); + }); + + it('returns the reporters correctly', () => { + expect( + constructReportersFilter([ + { username: 'test', full_name: 'Test', email: 'elastic@elastic.co' }, + { + username: 'test2', + full_name: 'Test 2', + email: 'elastic@elastic.co', + profile_uid: '123', + }, + ]) + ).toEqual({ reporters: ['test', '123'] }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index e1f5b954a23dc..f0c37ddb9e424 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { isObject, transform, snakeCase } from 'lodash'; +import { isObject, transform, snakeCase, isEmpty } from 'lodash'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import type { ToastInputFields } from '@kbn/core/public'; +import { NO_ASSIGNEES_FILTERING_KEYWORD } from '../../common/constants'; import type { CaseResponse, CasesResponse, @@ -20,6 +21,7 @@ import type { CasePatchRequest, CaseResolveResponse, SingleCaseMetricsResponse, + User, } from '../../common/api'; import { CaseResponseRt, @@ -32,7 +34,7 @@ import { CaseResolveResponseRt, SingleCaseMetricsResponseRt, } from '../../common/api'; -import type { Case, UpdateByKey } from './types'; +import type { Case, FilterOptions, UpdateByKey } from './types'; import * as i18n from './translations'; export const getTypedPayload = (a: unknown): T => a as T; @@ -131,3 +133,31 @@ export const createUpdateSuccessToaster = ( return toast; }; + +export const constructAssigneesFilter = ( + assignees: FilterOptions['assignees'] +): { assignees?: string | string[] } => + assignees === null || assignees.length > 0 + ? { + assignees: + assignees?.map((assignee) => + assignee === null ? NO_ASSIGNEES_FILTERING_KEYWORD : assignee + ) ?? NO_ASSIGNEES_FILTERING_KEYWORD, + } + : {}; + +export const constructReportersFilter = (reporters: User[]) => { + return reporters.length > 0 + ? { + reporters: reporters + .map((reporter) => { + if (reporter.profile_uid != null) { + return reporter.profile_uid; + } + + return reporter.username ?? ''; + }) + .filter((reporterID) => !isEmpty(reporterID)), + } + : {}; +}; diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index db980c058fbb5..cb64123c99de9 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -20,8 +20,8 @@ import { constructQueryOptions } from '../utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { Operations } from '../../authorization'; import type { CasesClientArgs } from '..'; -import type { ConstructQueryParams } from '../types'; import { LICENSING_CASE_ASSIGNMENT_FEATURE } from '../../common/constants'; +import type { CasesFindQueryParams } from '../types'; /** * Retrieves a case and optionally its comments. @@ -65,7 +65,7 @@ export const find = async ( licensingService.notifyUsage(LICENSING_CASE_ASSIGNMENT_FEATURE); } - const queryArgs: ConstructQueryParams = { + const queryArgs: CasesFindQueryParams = { tags: queryParams.tags, reporters: queryParams.reporters, sortByField: queryParams.sortField, @@ -82,6 +82,7 @@ export const find = async ( status: undefined, authorizationFilter, }); + const caseQueryOptions = constructQueryOptions({ ...queryArgs, authorizationFilter }); const [cases, statusStats] = await Promise.all([ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 1232cd0f9affe..1ce57b0aa801f 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -9,10 +9,10 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { SavedObjectsClientContract, Logger } from '@kbn/core/server'; import type { ActionsClient } from '@kbn/actions-plugin/server'; import type { LensServerPluginSetup } from '@kbn/lens-plugin/server'; -import type { KueryNode } from '@kbn/es-query'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { IBasePath } from '@kbn/core-http-browser'; -import type { CaseSeverity, CaseStatuses, User } from '../../common/api'; +import type { KueryNode } from '@kbn/es-query'; +import type { CasesFindRequest, User } from '../../common/api'; import type { Authorization } from '../authorization/authorization'; import type { CaseConfigureService, @@ -53,15 +53,9 @@ export interface CasesClientArgs { readonly publicBaseUrl?: IBasePath['publicBaseUrl']; } -export interface ConstructQueryParams { - tags?: string | string[]; - reporters?: string | string[]; - status?: CaseStatuses; - severity?: CaseSeverity; - sortByField?: string; - owner?: string | string[]; - authorizationFilter?: KueryNode; - from?: string; - to?: string; - assignees?: string | string[]; -} +export type CasesFindQueryParams = Partial< + Pick< + CasesFindRequest, + 'tags' | 'reporters' | 'status' | 'severity' | 'owner' | 'from' | 'to' | 'assignees' + > & { sortByField?: string; authorizationFilter?: KueryNode } +>; diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index a23e300531f92..eff291e32bfa3 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -18,7 +18,7 @@ import { isCommentRequestTypeExternalReference, isCommentRequestTypePersistableState, } from '../../common/utils/attachments'; -import { CASE_SAVED_OBJECT } from '../../common/constants'; +import { CASE_SAVED_OBJECT, NO_ASSIGNEES_FILTERING_KEYWORD } from '../../common/constants'; import type { CaseStatuses, CommentRequest, @@ -46,7 +46,7 @@ import { assertUnreachable, } from '../common/utils'; import type { SavedObjectFindOptionsKueryNode } from '../common/types'; -import type { ConstructQueryParams } from './types'; +import type { CasesFindQueryParams } from './types'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { @@ -322,6 +322,43 @@ export const buildRangeFilter = ({ } }; +export const buildAssigneesFilter = ({ + assignees, +}: { + assignees: CasesFindQueryParams['assignees']; +}): KueryNode | undefined => { + if (assignees === undefined) { + return; + } + + const assigneesAsArray = Array.isArray(assignees) ? assignees : [assignees]; + + if (assigneesAsArray.length === 0) { + return; + } + + const assigneesWithoutNone = assigneesAsArray.filter( + (assignee) => assignee !== NO_ASSIGNEES_FILTERING_KEYWORD + ); + const hasNoneAssignee = assigneesAsArray.some( + (assignee) => assignee === NO_ASSIGNEES_FILTERING_KEYWORD + ); + + const assigneesFilter = assigneesWithoutNone.map((filter) => + nodeBuilder.is(`${CASE_SAVED_OBJECT}.attributes.assignees.uid`, escapeKuery(filter)) + ); + + if (!hasNoneAssignee) { + return nodeBuilder.or(assigneesFilter); + } + + const filterCasesWithoutAssigneesKueryNode = fromKueryExpression( + `not ${CASE_SAVED_OBJECT}.attributes.assignees.uid: *` + ); + + return nodeBuilder.or([...assigneesFilter, filterCasesWithoutAssigneesKueryNode]); +}; + export const constructQueryOptions = ({ tags, reporters, @@ -333,7 +370,7 @@ export const constructQueryOptions = ({ from, to, assignees, -}: ConstructQueryParams): SavedObjectFindOptionsKueryNode => { +}: CasesFindQueryParams): SavedObjectFindOptionsKueryNode => { const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' }); const reportersFilter = createReportersFilter(reporters); const sortField = sortToSnake(sortByField); @@ -342,11 +379,7 @@ export const constructQueryOptions = ({ const statusFilter = status != null ? addStatusFilter({ status }) : undefined; const severityFilter = severity != null ? addSeverityFilter({ severity }) : undefined; const rangeFilter = buildRangeFilter({ from, to }); - const assigneesFilter = buildFilter({ - filters: assignees, - field: 'assignees.uid', - operator: 'or', - }); + const assigneesFilter = buildAssigneesFilter({ assignees }); const filters = combineFilters([ statusFilter, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/assignees.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/assignees.ts index 1835c88c4dafb..c2b53a008f43a 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/assignees.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/assignees.ts @@ -33,276 +33,366 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - it('allows the assignees field to be an empty array', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - - expect(postedCase.assignees).to.eql([]); - }); + describe('assign users to a case', () => { + it('allows the assignees field to be an empty array', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); - it('allows creating a case without the assignees field in the request', async () => { - const postReq = getPostCaseRequest(); - const { assignees, ...restRequest } = postReq; + expect(postedCase.assignees).to.eql([]); + }); - const postedCase = await createCase(supertest, restRequest); + it('allows creating a case without the assignees field in the request', async () => { + const postReq = getPostCaseRequest(); + const { assignees, ...restRequest } = postReq; - expect(postedCase.assignees).to.eql([]); - }); + const postedCase = await createCase(supertest, restRequest); - it('assigns a user to a case and retrieves the users profile', async () => { - const profile = await suggestUserProfiles({ - supertest: supertestWithoutAuth, - req: { - name: 'delete', - owners: ['securitySolutionFixture'], - size: 1, - }, - auth: { user: superUser, space: 'space1' }, + expect(postedCase.assignees).to.eql([]); }); - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: profile[0].uid }], - }) - ); - - const retrievedProfiles = await bulkGetUserProfiles({ - supertest, - req: { - uids: postedCase.assignees.map((assignee) => assignee.uid), - dataPath: 'avatar', - }, - }); + it('assigns a user to a case and retrieves the users profile', async () => { + const profile = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'delete', + owners: ['securitySolutionFixture'], + size: 1, + }, + auth: { user: superUser, space: 'space1' }, + }); - expect(retrievedProfiles[0]).to.eql(profile[0]); - }); + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profile[0].uid }], + }) + ); - it('assigns multiple users to a case and retrieves their profiles', async () => { - const profiles = await suggestUserProfiles({ - supertest: supertestWithoutAuth, - req: { - name: 'only', - owners: ['securitySolutionFixture'], - size: 2, - }, - auth: { user: superUser, space: 'space1' }, - }); + const retrievedProfiles = await bulkGetUserProfiles({ + supertest, + req: { + uids: postedCase.assignees.map((assignee) => assignee.uid), + dataPath: 'avatar', + }, + }); - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - assignees: profiles.map((profile) => ({ uid: profile.uid })), - }) - ); - - const retrievedProfiles = await bulkGetUserProfiles({ - supertest, - req: { - uids: postedCase.assignees.map((assignee) => assignee.uid), - dataPath: 'avatar', - }, + expect(retrievedProfiles[0]).to.eql(profile[0]); }); - expect(retrievedProfiles).to.eql(profiles); - }); + it('assigns multiple users to a case and retrieves their profiles', async () => { + const profiles = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'only', + owners: ['securitySolutionFixture'], + size: 2, + }, + auth: { user: superUser, space: 'space1' }, + }); - it('removes duplicate assignees when creating a case', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: '123' }, { uid: '123' }], - }) - ); + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + assignees: profiles.map((profile) => ({ uid: profile.uid })), + }) + ); - expect(postedCase.assignees).to.eql([{ uid: '123' }]); - }); + const retrievedProfiles = await bulkGetUserProfiles({ + supertest, + req: { + uids: postedCase.assignees.map((assignee) => assignee.uid), + dataPath: 'avatar', + }, + }); - it('assigns a user to a case and retrieves the users profile from a get case call', async () => { - const profile = await suggestUserProfiles({ - supertest: supertestWithoutAuth, - req: { - name: 'delete', - owners: ['securitySolutionFixture'], - size: 1, - }, - auth: { user: superUser, space: 'space1' }, + expect(retrievedProfiles).to.eql(profiles); }); - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: profile[0].uid }], - }) - ); - - const retrievedCase = await getCase({ caseId: postedCase.id, supertest }); - - const retrievedProfiles = await bulkGetUserProfiles({ - supertest, - req: { - uids: retrievedCase.assignees.map((assignee) => assignee.uid), - dataPath: 'avatar', - }, + it('removes duplicate assignees when creating a case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: '123' }, { uid: '123' }], + }) + ); + + expect(postedCase.assignees).to.eql([{ uid: '123' }]); }); - expect(retrievedProfiles[0]).to.eql(profile[0]); - }); + it('assigns a user to a case and retrieves the users profile from a get case call', async () => { + const profile = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'delete', + owners: ['securitySolutionFixture'], + size: 1, + }, + auth: { user: superUser, space: 'space1' }, + }); - it('filters cases using the assigned user', async () => { - const profile = await suggestUserProfiles({ - supertest: supertestWithoutAuth, - req: { - name: 'delete', - owners: ['securitySolutionFixture'], - size: 1, - }, - auth: { user: superUser, space: 'space1' }, - }); + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profile[0].uid }], + }) + ); - await createCase(supertest, postCaseReq); - const caseWithDeleteAssignee1 = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: profile[0].uid }], - }) - ); - const caseWithDeleteAssignee2 = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: profile[0].uid }], - }) - ); - - const cases = await findCases({ - supertest, - query: { assignees: [profile[0].uid] }, - }); + const retrievedCase = await getCase({ caseId: postedCase.id, supertest }); - expect(cases).to.eql({ - ...findCasesResp, - total: 2, - cases: [caseWithDeleteAssignee1, caseWithDeleteAssignee2], - count_open_cases: 2, + const retrievedProfiles = await bulkGetUserProfiles({ + supertest, + req: { + uids: retrievedCase.assignees.map((assignee) => assignee.uid), + dataPath: 'avatar', + }, + }); + + expect(retrievedProfiles[0]).to.eql(profile[0]); }); }); - it("filters cases using the assigned users by constructing an or'd filter", async () => { - const profileUidsToFilter = await suggestUserProfiles({ - supertest: supertestWithoutAuth, - req: { - name: 'only', - owners: ['securitySolutionFixture'], - size: 2, - }, - auth: { user: superUser, space: 'space1' }, - }); + describe('filter cases by assignees', () => { + it('filters cases using the assigned user', async () => { + const profile = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'delete', + owners: ['securitySolutionFixture'], + size: 1, + }, + auth: { user: superUser, space: 'space1' }, + }); - await createCase(supertest, postCaseReq); - const caseWithDeleteAssignee1 = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: profileUidsToFilter[0].uid }], - }) - ); - const caseWithDeleteAssignee2 = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: profileUidsToFilter[1].uid }], - }) - ); - - const cases = await findCases({ - supertest, - query: { assignees: [profileUidsToFilter[0].uid, profileUidsToFilter[1].uid] }, - }); + const [_, caseWithDeleteAssignee1, caseWithDeleteAssignee2] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profile[0].uid }], + }) + ), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profile[0].uid }], + }) + ), + ]); + + const cases = await findCases({ + supertest, + query: { assignees: [profile[0].uid] }, + }); - expect(cases).to.eql({ - ...findCasesResp, - total: 2, - cases: [caseWithDeleteAssignee1, caseWithDeleteAssignee2], - count_open_cases: 2, + expect(cases).to.eql({ + ...findCasesResp, + total: 2, + cases: [caseWithDeleteAssignee1, caseWithDeleteAssignee2], + count_open_cases: 2, + }); }); - }); - it('updates the assignees on a case', async () => { - const profiles = await suggestUserProfiles({ - supertest: supertestWithoutAuth, - req: { - name: 'delete', - owners: ['securitySolutionFixture'], - size: 1, - }, - auth: { user: superUser, space: 'space1' }, - }); + it("filters cases using the assigned users by constructing an or'd filter", async () => { + const profileUidsToFilter = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'only', + owners: ['securitySolutionFixture'], + size: 2, + }, + auth: { user: superUser, space: 'space1' }, + }); - const postedCase = await createCase(supertest, getPostCaseRequest()); - - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - assignees: [{ uid: profiles[0].uid }], - }, - ], - }, + const [_, caseWithDeleteAssignee1, caseWithDeleteAssignee2] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profileUidsToFilter[0].uid }], + }) + ), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profileUidsToFilter[1].uid }], + }) + ), + ]); + + const cases = await findCases({ + supertest, + query: { assignees: [profileUidsToFilter[0].uid, profileUidsToFilter[1].uid] }, + }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 2, + cases: [caseWithDeleteAssignee1, caseWithDeleteAssignee2], + count_open_cases: 2, + }); }); - const retrievedProfiles = await bulkGetUserProfiles({ - supertest, - req: { - uids: patchedCases[0].assignees.map((assignee) => assignee.uid), - dataPath: 'avatar', - }, + it('filters cases with no assignees', async () => { + const profile = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'delete', + owners: ['securitySolutionFixture'], + size: 1, + }, + auth: { user: superUser, space: 'space1' }, + }); + + const [caseWithNoAssignees] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profile[0].uid }], + }) + ), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profile[0].uid }], + }) + ), + ]); + + const cases = await findCases({ + supertest, + query: { assignees: 'none' }, + }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [caseWithNoAssignees], + count_open_cases: 1, + }); }); - expect(retrievedProfiles).to.eql(profiles); + it('filters cases with no assignees and multiple users', async () => { + const profileUidsToFilter = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'only', + owners: ['securitySolutionFixture'], + size: 2, + }, + auth: { user: superUser, space: 'space1' }, + }); + + const [caseWithNoAssignees, caseWithDeleteAssignee1] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profileUidsToFilter[0].uid }], + }) + ), + createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: profileUidsToFilter[1].uid }], + }) + ), + ]); + + const cases = await findCases({ + supertest, + query: { assignees: ['none', profileUidsToFilter[0].uid] }, + }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 2, + cases: [caseWithNoAssignees, caseWithDeleteAssignee1], + count_open_cases: 2, + }); + }); }); - it('remove duplicate assignees when updating a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - assignees: [{ uid: '123' }, { uid: '123' }], - }, - ], - }, + describe('update assignees', () => { + it('updates the assignees on a case', async () => { + const profiles = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { + name: 'delete', + owners: ['securitySolutionFixture'], + size: 1, + }, + auth: { user: superUser, space: 'space1' }, + }); + + const postedCase = await createCase(supertest, getPostCaseRequest()); + + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + assignees: [{ uid: profiles[0].uid }], + }, + ], + }, + }); + + const retrievedProfiles = await bulkGetUserProfiles({ + supertest, + req: { + uids: patchedCases[0].assignees.map((assignee) => assignee.uid), + dataPath: 'avatar', + }, + }); + + expect(retrievedProfiles).to.eql(profiles); }); - expect(patchedCases[0].assignees).to.eql([{ uid: '123' }]); - }); + it('remove duplicate assignees when updating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); - it('does not set the assignees to an empty array when the field is not updated', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - assignees: [{ uid: '123' }], - }) - ); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'abc', - }, - ], - }, + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + assignees: [{ uid: '123' }, { uid: '123' }], + }, + ], + }, + }); + + expect(patchedCases[0].assignees).to.eql([{ uid: '123' }]); }); - const updatedCase = await getCase({ supertest, caseId: postedCase.id }); - expect(updatedCase.assignees).to.eql([{ uid: '123' }]); + it('does not set the assignees to an empty array when the field is not updated', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + assignees: [{ uid: '123' }], + }) + ); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'abc', + }, + ], + }, + }); + + const updatedCase = await getCase({ supertest, caseId: postedCase.id }); + expect(updatedCase.assignees).to.eql([{ uid: '123' }]); + }); }); describe('validation', () => { diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts index 2eee4724d63a1..ef6787c17c3a8 100644 --- a/x-pack/test/functional/services/cases/common.ts +++ b/x-pack/test/functional/services/cases/common.ts @@ -110,5 +110,16 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro await header.waitUntilLoadingHasFinished(); }, + + async selectRowsInAssigneesPopover(indexes: number[]) { + const rows = await find.allByCssSelector('.euiSelectableListItem__content'); + for (const [index, row] of rows.entries()) { + if (indexes.includes(index)) { + await row.click(); + } + } + + await header.waitUntilLoadingHasFinished(); + }, }; } diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index c2ca924c88fac..1fe6525282aa4 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -144,7 +144,7 @@ export function CasesTableServiceProvider( }, async filterByAssignee(assignee: string) { - await common.clickAndValidate('options-filter-popover-button-assignees', 'euiSelectableList'); + await this.openAssigneesPopover(); await casesCommon.setSearchTextInAssigneesPopover(assignee); await casesCommon.selectFirstRowInAssigneesPopover(); @@ -176,6 +176,10 @@ export function CasesTableServiceProvider( await find.existsByCssSelector('[data-test-subj*="case-action-popover-"'); }, + async openAssigneesPopover() { + await common.clickAndValidate('options-filter-popover-button-assignees', 'euiSelectableList'); + }, + async selectAllCasesAndOpenBulkActions() { await testSubjects.setCheckbox('checkboxSelectAll', 'check'); await testSubjects.existOrFail('case-table-bulk-actions-link-icon'); @@ -246,5 +250,13 @@ export function CasesTableServiceProvider( await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); await this.bulkChangeSeverity(severity); }, + + async getCaseTitle(index: number) { + const titleElement = await ( + await this.getCaseFromTable(index) + ).findByTestSubject('case-details-link'); + + return await titleElement.getVisibleText(); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts index f01bd4d95f13e..5d03ccd4bfcd4 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts @@ -217,7 +217,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.refreshTable(); await cases.casesTable.validateCasesTableHasNthRows(1); const row = await cases.casesTable.getCaseFromTable(0); - const tags = await row.findByCssSelector('[data-test-subj="case-table-column-tags-one"]'); + const tags = await row.findByTestSubject('case-table-column-tags-one'); expect(await tags.getVisibleText()).to.be('one'); }); @@ -240,6 +240,33 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.validateCasesTableHasNthRows(1); await testSubjects.exists('case-user-profile-avatar-cases_all_user2'); }); + + it('filters cases without assignees', async () => { + await cases.casesTable.openAssigneesPopover(); + await cases.common.selectFirstRowInAssigneesPopover(); + await cases.casesTable.validateCasesTableHasNthRows(2); + + const firstCaseTitle = await ( + await cases.casesTable.getCaseFromTable(0) + ).findByTestSubject('case-details-link'); + + const secondCaseTitle = await ( + await cases.casesTable.getCaseFromTable(1) + ).findByTestSubject('case-details-link'); + + expect(await firstCaseTitle.getVisibleText()).be('test2'); + expect(await secondCaseTitle.getVisibleText()).be('matchme'); + }); + + it('filters cases with and without assignees', async () => { + await cases.casesTable.openAssigneesPopover(); + await cases.common.selectRowsInAssigneesPopover([0, 2]); + await cases.casesTable.validateCasesTableHasNthRows(3); + + expect(await cases.casesTable.getCaseTitle(0)).be('test4'); + expect(await cases.casesTable.getCaseTitle(1)).be('test2'); + expect(await cases.casesTable.getCaseTitle(2)).be('matchme'); + }); }); }); From 6271d82f8f4db6528acc09071a88365e68b546f6 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 18 Oct 2022 16:02:15 +0200 Subject: [PATCH 36/74] [Synthetics] Monitor summary error sparkline (#143378) --- .../configurations/lens_attributes.ts | 46 ++++++++----------- .../synthetics/kpi_over_time_config.ts | 14 ++++++ .../test_data/mobile_test_attribute.ts | 1 + .../columns/operation_type_select.tsx | 6 +++ .../monitor_error_sparklines.tsx | 46 +++++++++++++++++++ .../monitor_summary/monitor_errors_count.tsx | 33 +++++++------ .../monitor_summary/monitor_summary.tsx | 7 ++- 7 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 50701e12f94be..1aece50b812ef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -257,23 +257,6 @@ export class LensAttributes { }; } - getCardinalityColumn({ - sourceField, - label, - seriesConfig, - }: { - sourceField: string; - label?: string; - seriesConfig: SeriesConfig; - }) { - return this.getNumberOperationColumn({ - sourceField, - operationType: 'unique_count', - label, - seriesConfig, - }); - } - getFiltersColumn({ label, paramFilters, @@ -389,15 +372,24 @@ export class LensAttributes { | CardinalityIndexPatternColumn { return { ...buildNumberColumn(sourceField), - label: i18n.translate('xpack.observability.expView.columns.operation.label', { - defaultMessage: '{operationType} of {sourceField}', - values: { - sourceField: label || seriesConfig.labels[sourceField], - operationType: capitalize(operationType), - }, - }), + label: + operationType === 'unique_count' + ? label || seriesConfig.labels[sourceField] + : i18n.translate('xpack.observability.expView.columns.operation.label', { + defaultMessage: '{operationType} of {sourceField}', + values: { + sourceField: label || seriesConfig.labels[sourceField], + operationType: capitalize(operationType), + }, + }), filter: columnFilter, operationType, + params: + operationType === 'unique_count' + ? { + emptyAsNull: true, + } + : {}, }; } @@ -588,11 +580,13 @@ export class LensAttributes { seriesConfig: layerConfig.seriesConfig, }); } - if (operationType === 'unique_count') { - return this.getCardinalityColumn({ + if (operationType === 'unique_count' || fieldType === 'string') { + return this.getNumberOperationColumn({ sourceField: fieldName, + operationType: 'unique_count', label: columnLabel || label, seriesConfig: layerConfig.seriesConfig, + columnFilter: columnFilters?.[0], }); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 6a53e438d280a..76b9a6c2ade41 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -93,6 +93,20 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig columnType: FORMULA_COLUMN, formula: "1- (count(kql='summary.down > 0') / count())", }, + { + label: 'Monitor Errors', + id: 'state.id', + // columnType: FORMULA_COLUMN, + // formula: "unique_count(state.id, kql='status.up: 0')", + field: 'state.id', + columnType: OPERATION_COLUMN, + columnFilters: [ + { + language: 'kuery', + query: `state.id: * and state.up: 0`, + }, + ], + }, { field: SUMMARY_UP, id: SUMMARY_UP, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts index 874d6e45b2234..1c296f9cdc1fd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts @@ -41,6 +41,7 @@ export const testMobileKPIAttr = { isBucketed: false, label: 'Median of System memory usage', operationType: 'median', + params: {}, scale: 'ratio', sourceField: 'system.memory.usage', dataType: 'number', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx index a3fd5bf45e0f2..f67f348ec101d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx @@ -82,6 +82,12 @@ export function OperationTypeComponent({ defaultMessage: 'Last value', }), }, + { + value: 'unique_count' as OperationType, + inputDisplay: i18n.translate('xpack.observability.expView.operationType.uniqueCount', { + defaultMessage: 'Unique count', + }), + }, { value: '25th' as OperationType, inputDisplay: i18n.translate('xpack.observability.expView.operationType.25thPercentile', { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx new file mode 100644 index 0000000000000..731b623c65e1b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useEuiTheme } from '@elastic/eui'; +import { ClientPluginsStart } from '../../../../../plugin'; + +export const MonitorErrorSparklines = () => { + const { observability } = useKibana().services; + + const { ExploratoryViewEmbeddable } = observability; + + const { monitorId } = useParams<{ monitorId: string }>(); + + const { euiTheme } = useEuiTheme(); + + return ( + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx index 9c5bf090607c9..4076e24de49d7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx @@ -9,6 +9,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import React from 'react'; import { ReportTypes } from '@kbn/observability-plugin/public'; import { useParams } from 'react-router-dom'; +import { KpiWrapper } from './kpi_wrapper'; import { ClientPluginsStart } from '../../../../../plugin'; export const MonitorErrorsCount = () => { @@ -19,21 +20,23 @@ export const MonitorErrorsCount = () => { const { monitorId } = useParams<{ monitorId: string }>(); return ( - + + ]} + /> + ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx index 6d4dd6018acaf..139b129d1d4c0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_summary.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { MonitorErrorSparklines } from './monitor_error_sparklines'; import { DurationSparklines } from './duration_sparklines'; import { MonitorDurationTrend } from './duration_trend'; import { StepDurationPanel } from './step_duration_panel'; @@ -47,7 +48,7 @@ export const MonitorSummary = () => {

      {LAST_30DAYS_LABEL}

      - + @@ -63,7 +64,9 @@ export const MonitorSummary = () => { - {/* TODO: Add error sparkline*/} + + + From 39cd1bc29f51aec015cad07f575742628fb2eaff Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 18 Oct 2022 09:03:56 -0500 Subject: [PATCH 37/74] [data views] Provide method of passing errors to API consumers instead of handling error (#143336) * refactor methods that display toasts * type fix * add docs --- .../common/data_views/data_views.ts | 259 +++++++++++------- 1 file changed, 164 insertions(+), 95 deletions(-) diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index fa0db477fcd75..60dd372dd7b8f 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -140,25 +140,37 @@ export interface DataViewsServicePublicMethods { * Create data view based on the provided spec. * @param spec - Data view spec. * @param skipFetchFields - If true, do not fetch fields. + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - create: (spec: DataViewSpec, skipFetchFields?: boolean) => Promise; + create: ( + spec: DataViewSpec, + skipFetchFields?: boolean, + displayErrors?: boolean + ) => Promise; /** * Create and save data view based on provided spec. * @param spec - Data view spec. * @param override - If true, save over existing data view * @param skipFetchFields - If true, do not fetch fields. + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ createAndSave: ( spec: DataViewSpec, override?: boolean, - skipFetchFields?: boolean + skipFetchFields?: boolean, + displayErrors?: boolean ) => Promise; /** * Save data view * @param dataView - Data view instance to save. * @param override - If true, save over existing data view + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - createSavedObject: (indexPattern: DataView, override?: boolean) => Promise; + createSavedObject: ( + indexPattern: DataView, + override?: boolean, + displayErrors?: boolean + ) => Promise; /** * Delete data view * @param indexPatternId - Id of the data view to delete. @@ -180,8 +192,9 @@ export interface DataViewsServicePublicMethods { /** * Get data view by id. * @param id - Id of the data view to get. + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - get: (id: string) => Promise; + get: (id: string, displayErrors?: boolean) => Promise; /** * Get populated data view saved object cache. */ @@ -192,8 +205,9 @@ export interface DataViewsServicePublicMethods { getCanSave: () => Promise; /** * Get default data view as data view instance. + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - getDefault: () => Promise; + getDefault: (displayErrors?: boolean) => Promise; /** * Get default data view id. */ @@ -240,10 +254,11 @@ export interface DataViewsServicePublicMethods { * Refresh fields for data view instance * @params dataView - Data view instance */ - refreshFields: (indexPattern: DataView) => Promise; + refreshFields: (indexPattern: DataView, displayErrors?: boolean) => Promise; /** * Converts data view saved object to spec * @params savedObject - Data view saved object + * @params displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ savedObjectToSpec: (savedObject: SavedObject) => DataViewSpec; /** @@ -257,11 +272,13 @@ export interface DataViewsServicePublicMethods { * @param indexPattern - data view instance * @param saveAttempts - number of times to try saving * @oaram ignoreErrors - if true, do not throw error on failure + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ updateSavedObject: ( indexPattern: DataView, saveAttempts?: number, - ignoreErrors?: boolean + ignoreErrors?: boolean, + displayErrors?: boolean ) => Promise; } @@ -435,11 +452,12 @@ export class DataViewsService { /** * Get default index pattern + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - getDefault = async () => { + getDefault = async (displayErrors: boolean = true) => { const defaultIndexPatternId = await this.getDefaultId(); if (defaultIndexPatternId) { - return await this.get(defaultIndexPatternId); + return await this.get(defaultIndexPatternId, displayErrors); } return null; @@ -529,34 +547,43 @@ export class DataViewsService { }); }; + private refreshFieldsFn = async (indexPattern: DataView) => { + const { fields, indices } = await this.getFieldsAndIndicesForDataView(indexPattern); + fields.forEach((field) => (field.isMapped = true)); + const scripted = indexPattern.getScriptedFields().map((field) => field.spec); + const fieldAttrs = indexPattern.getFieldAttrs(); + const fieldsWithSavedAttrs = Object.values( + this.fieldArrayToMap([...fields, ...scripted], fieldAttrs) + ); + const runtimeFieldsMap = this.getRuntimeFields( + indexPattern.getRuntimeMappings() as Record, + indexPattern.getFieldAttrs() + ); + const runtimeFieldsArray = Object.values(runtimeFieldsMap).filter( + (runtimeField) => + !fieldsWithSavedAttrs.find((mappedField) => mappedField.name === runtimeField.name) + ); + indexPattern.fields.replaceAll([...runtimeFieldsArray, ...fieldsWithSavedAttrs]); + indexPattern.matchedIndices = indices; + }; + /** * Refresh field list for a given index pattern. * @param indexPattern + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - refreshFields = async (indexPattern: DataView) => { + refreshFields = async (dataView: DataView, displayErrors: boolean = true) => { + if (!displayErrors) { + return this.refreshFieldsFn(dataView); + } + try { - const { fields, indices } = await this.getFieldsAndIndicesForDataView(indexPattern); - fields.forEach((field) => (field.isMapped = true)); - const scripted = indexPattern.getScriptedFields().map((field) => field.spec); - const fieldAttrs = indexPattern.getFieldAttrs(); - const fieldsWithSavedAttrs = Object.values( - this.fieldArrayToMap([...fields, ...scripted], fieldAttrs) - ); - const runtimeFieldsMap = this.getRuntimeFields( - indexPattern.getRuntimeMappings() as Record, - indexPattern.getFieldAttrs() - ); - const runtimeFieldsArray = Object.values(runtimeFieldsMap).filter( - (runtimeField) => - !fieldsWithSavedAttrs.find((mappedField) => mappedField.name === runtimeField.name) - ); - indexPattern.fields.replaceAll([...runtimeFieldsArray, ...fieldsWithSavedAttrs]); - indexPattern.matchedIndices = indices; + await this.refreshFieldsFn(dataView); } catch (err) { if (err instanceof DataViewMissingIndices) { this.onNotification( { title: err.message, color: 'danger', iconType: 'alert' }, - `refreshFields:${indexPattern.getIndexPattern()}` + `refreshFields:${dataView.getIndexPattern()}` ); } @@ -565,10 +592,10 @@ export class DataViewsService { { title: i18n.translate('dataViews.fetchFieldErrorTitle', { defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id: indexPattern.id, title: indexPattern.getIndexPattern() }, + values: { id: dataView.id, title: dataView.getIndexPattern() }, }), }, - indexPattern.getIndexPattern() + dataView.getIndexPattern() ); } }; @@ -696,7 +723,10 @@ export class DataViewsService { }; }; - private getSavedObjectAndInit = async (id: string): Promise => { + private getSavedObjectAndInit = async ( + id: string, + displayErrors: boolean = true + ): Promise => { const savedObject = await this.savedObjectsClient.get( DATA_VIEW_SAVED_OBJECT_TYPE, id @@ -706,71 +736,95 @@ export class DataViewsService { throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews'); } - return this.initFromSavedObject(savedObject); + return this.initFromSavedObject(savedObject, displayErrors); + }; + + private initFromSavedObjectLoadFields = async ({ + savedObjectId, + spec, + }: { + savedObjectId: string; + spec: DataViewSpec; + }) => { + const { title, type, typeMeta, runtimeFieldMap } = spec; + const { fields, indices } = await this.refreshFieldSpecMap( + spec.fields || {}, + savedObjectId, + spec.title as string, + { + pattern: title as string, + metaFields: await this.config.get(META_FIELDS), + type, + rollupIndex: typeMeta?.params?.rollup_index, + allowNoIndex: spec.allowNoIndex, + }, + spec.fieldAttrs + ); + + const runtimeFieldSpecs = this.getRuntimeFields(runtimeFieldMap, spec.fieldAttrs); + // mapped fields overwrite runtime fields + return { fields: { ...runtimeFieldSpecs, ...fields }, indices: indices || [] }; }; private initFromSavedObject = async ( - savedObject: SavedObject + savedObject: SavedObject, + displayErrors: boolean = true ): Promise => { const spec = this.savedObjectToSpec(savedObject); - const { title, type, typeMeta, runtimeFieldMap } = spec; spec.fieldAttrs = savedObject.attributes.fieldAttrs ? JSON.parse(savedObject.attributes.fieldAttrs) : {}; - let matchedIndices: string[] = []; + let fields: Record = {}; + let indices: string[] = []; - try { - const { fields, indices } = await this.refreshFieldSpecMap( - spec.fields || {}, - savedObject.id, - spec.title as string, - { - pattern: title as string, - metaFields: await this.config.get(META_FIELDS), - type, - rollupIndex: typeMeta?.params?.rollup_index, - allowNoIndex: spec.allowNoIndex, - }, - spec.fieldAttrs - ); - - spec.fields = fields; - matchedIndices = indices || []; - - const runtimeFieldSpecs = this.getRuntimeFields(runtimeFieldMap, spec.fieldAttrs); - // mapped fields overwrite runtime fields - spec.fields = { ...runtimeFieldSpecs, ...spec.fields }; - } catch (err) { - if (err instanceof DataViewMissingIndices) { - this.onNotification( - { - title: err.message, - color: 'danger', - iconType: 'alert', - }, - `initFromSavedObject:${title}` - ); - } else { - this.onError( - err, - { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id: savedObject.id, title }, - }), - }, - title || '' - ); + if (!displayErrors) { + const fieldsAndIndices = await this.initFromSavedObjectLoadFields({ + savedObjectId: savedObject.id, + spec, + }); + fields = fieldsAndIndices.fields; + indices = fieldsAndIndices.indices; + } else { + try { + const fieldsAndIndices = await this.initFromSavedObjectLoadFields({ + savedObjectId: savedObject.id, + spec, + }); + fields = fieldsAndIndices.fields; + indices = fieldsAndIndices.indices; + } catch (err) { + if (err instanceof DataViewMissingIndices) { + this.onNotification( + { + title: err.message, + color: 'danger', + iconType: 'alert', + }, + `initFromSavedObject:${spec.title}` + ); + } else { + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id: savedObject.id, title: spec.title }, + }), + }, + spec.title || '' + ); + } } } + spec.fields = fields; spec.fieldFormats = savedObject.attributes.fieldFormatMap ? JSON.parse(savedObject.attributes.fieldFormatMap) : {}; - const indexPattern = await this.create(spec, true); - indexPattern.matchedIndices = matchedIndices; + const indexPattern = await this.create(spec, true, displayErrors); + indexPattern.matchedIndices = indices; indexPattern.resetOriginalSavedObjectBody(); return indexPattern; }; @@ -822,10 +876,12 @@ export class DataViewsService { /** * Get an index pattern by id, cache optimized. * @param id + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - get = async (id: string): Promise => { + get = async (id: string, displayErrors: boolean = true): Promise => { const indexPatternPromise = - this.dataViewCache.get(id) || this.dataViewCache.set(id, this.getSavedObjectAndInit(id)); + this.dataViewCache.get(id) || + this.dataViewCache.set(id, this.getSavedObjectAndInit(id, displayErrors)); // don't cache failed requests indexPatternPromise.catch(() => { @@ -839,11 +895,13 @@ export class DataViewsService { * Create a new data view instance. * @param spec data view spec * @param skipFetchFields if true, will not fetch fields + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. * @returns DataView */ async create( { id, name, title, ...restOfSpec }: DataViewSpec, - skipFetchFields = false + skipFetchFields = false, + displayErrors = true ): Promise { const shortDotsEnable = await this.config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(META_FIELDS); @@ -863,7 +921,7 @@ export class DataViewsService { }); if (!skipFetchFields) { - await this.refreshFields(indexPattern); + await this.refreshFields(indexPattern, displayErrors); } this.dataViewCache.set(indexPattern.id!, Promise.resolve(indexPattern)); @@ -876,11 +934,17 @@ export class DataViewsService { * @param spec data view spec * @param override Overwrite if existing index pattern exists. * @param skipFetchFields Whether to skip field refresh step. + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - async createAndSave(spec: DataViewSpec, override = false, skipFetchFields = false) { - const indexPattern = await this.create(spec, skipFetchFields); - const createdIndexPattern = await this.createSavedObject(indexPattern, override); + async createAndSave( + spec: DataViewSpec, + override = false, + skipFetchFields = false, + displayErrors = true + ) { + const indexPattern = await this.create(spec, skipFetchFields, displayErrors); + const createdIndexPattern = await this.createSavedObject(indexPattern, override, displayErrors); await this.setDefault(createdIndexPattern.id!); return createdIndexPattern!; } @@ -889,9 +953,10 @@ export class DataViewsService { * Save a new data view. * @param dataView data view instance * @param override Overwrite if existing index pattern exists + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - async createSavedObject(dataView: DataView, override = false) { + async createSavedObject(dataView: DataView, override = false, displayErrors = true) { if (!(await this.getCanSave())) { throw new DataViewInsufficientAccessError(); } @@ -914,7 +979,7 @@ export class DataViewsService { } )) as SavedObject; - const createdIndexPattern = await this.initFromSavedObject(response); + const createdIndexPattern = await this.initFromSavedObject(response, displayErrors); if (this.savedObjectsCache) { this.savedObjectsCache.push(response as SavedObject); } @@ -926,12 +991,14 @@ export class DataViewsService { * @param indexPattern * @param saveAttempts * @param ignoreErrors + * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ async updateSavedObject( indexPattern: DataView, saveAttempts: number = 0, - ignoreErrors: boolean = false + ignoreErrors: boolean = false, + displayErrors: boolean = true ): Promise { if (!indexPattern.id) return; if (!(await this.getCanSave())) { @@ -962,7 +1029,7 @@ export class DataViewsService { }) .catch(async (err) => { if (err?.res?.status === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) { - const samePattern = await this.get(indexPattern.id as string); + const samePattern = await this.get(indexPattern.id as string, displayErrors); // What keys changed from now and what the server returned const updatedBody = samePattern.getAsSavedObjectBody(); @@ -998,10 +1065,12 @@ export class DataViewsService { 'Unable to write data view! Refresh the page to get the most up to date changes for this data view.', }); - this.onNotification( - { title, color: 'danger' }, - `updateSavedObject:${indexPattern.getIndexPattern()}` - ); + if (displayErrors) { + this.onNotification( + { title, color: 'danger' }, + `updateSavedObject:${indexPattern.getIndexPattern()}` + ); + } throw err; } @@ -1016,7 +1085,7 @@ export class DataViewsService { this.dataViewCache.clear(indexPattern.id!); // Try the save again - return this.updateSavedObject(indexPattern, saveAttempts, ignoreErrors); + return this.updateSavedObject(indexPattern, saveAttempts, ignoreErrors, displayErrors); } throw err; }); From 74a4d1b3e76f6d57feca66bdb8175d10e6397583 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Tue, 18 Oct 2022 17:27:57 +0300 Subject: [PATCH 38/74] [Cloud Posture] improved getHealthyAgents logic (#143526) --- .../server/routes/status/status.test.ts | 12 ++++++++---- .../server/routes/status/status.ts | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) 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 974b4f5a391b1..d3e37b7c4b6c7 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 @@ -184,7 +184,8 @@ describe('CspSetupStatus route', () => { ] as unknown as AgentPolicy[]); mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - total: 1, + online: 1, + updating: 0, } as unknown as GetAgentStatusResponse['results']); // Act @@ -269,7 +270,8 @@ describe('CspSetupStatus route', () => { ] as unknown as AgentPolicy[]); mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - total: 0, + online: 0, + updating: 0, } as unknown as GetAgentStatusResponse['results']); // Act @@ -323,7 +325,8 @@ describe('CspSetupStatus route', () => { ] as unknown as AgentPolicy[]); mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - total: 1, + online: 1, + updating: 0, } as unknown as GetAgentStatusResponse['results']); // Act @@ -379,7 +382,8 @@ describe('CspSetupStatus route', () => { ] as unknown as AgentPolicy[]); mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - total: 1, + online: 1, + updating: 0, } as unknown as GetAgentStatusResponse['results']); // Act 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 a85d64e22c994..64899a925acff 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 @@ -44,8 +44,10 @@ const getHealthyAgents = async ( agentPolicies ); - // TODO: should be fixed - currently returns all agents instead of healthy agents only - return Object.values(agentStatusesByAgentPolicyId).reduce((sum, status) => sum + status.total, 0); + return Object.values(agentStatusesByAgentPolicyId).reduce( + (sum, status) => sum + status.online + status.updating, + 0 + ); }; const calculateCspStatusCode = ( From d666c132284a6711c4ef827d02ed6d8bc2098396 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 18 Oct 2022 07:48:56 -0700 Subject: [PATCH 39/74] [DOCS] Clarify deletion sub-feature privileges for cases (#143133) --- docs/management/cases/manage-cases.asciidoc | 2 +- docs/management/cases/setup-cases.asciidoc | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/management/cases/manage-cases.asciidoc b/docs/management/cases/manage-cases.asciidoc index fd26221ba3432..08a57240799d0 100644 --- a/docs/management/cases/manage-cases.asciidoc +++ b/docs/management/cases/manage-cases.asciidoc @@ -58,7 +58,7 @@ it by clicking the *Open Visualization* option in the comment menu. === Manage cases In *Management > {stack-manage-app} > Cases*, you can search cases and filter -them by tags, reporter. +them by severity, status, tags, and assignees. To view a case, click on its name. You can then: diff --git a/docs/management/cases/setup-cases.asciidoc b/docs/management/cases/setup-cases.asciidoc index bd99d499c81d1..51165cd7c4691 100644 --- a/docs/management/cases/setup-cases.asciidoc +++ b/docs/management/cases/setup-cases.asciidoc @@ -15,9 +15,15 @@ a| * `All` for the *Cases* feature under *Management*. * `All` for the *Actions and Connectors* feature under *Management*. -NOTE: The *Actions and Connectors* feature privilege is required to create, add, +[NOTE] +==== +The *Actions and Connectors* feature privilege is required to create, add, delete, and modify case connectors and to send updates to external systems. +By default, `All` for the *Cases* feature includes authority to delete cases +and comments unless you customize the sub-feature privileges. +==== + | Give assignee access to cases a| `All` for the *Cases* feature under *Management*. From 467c773d084b3ab948d6d93e2e980f05270ada8b Mon Sep 17 00:00:00 2001 From: James Rucker Date: Tue, 18 Oct 2022 08:00:29 -0700 Subject: [PATCH 40/74] Fall back to cloud Enterprise Search host when publicUrl is not available (#143477) * Move decodeCloudId to shared utils * Add getCloudEnterpriseSearchHost * Use getCloudEnterpriseSearchHost and allow cloud to be undefined * Remove barrel files Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/curl_request/curl_request.tsx | 2 +- .../public/applications/index.tsx | 4 +- .../decode_cloud_id}/decode_cloud_id.test.ts | 0 .../decode_cloud_id}/decode_cloud_id.ts | 0 .../get_cloud_enterprise_search_host.test.ts | 59 +++++++++++++++++++ .../get_cloud_enterprise_search_host.ts | 33 +++++++++++ 6 files changed, 96 insertions(+), 2 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/{enterprise_search_content/utils => shared/decode_cloud_id}/decode_cloud_id.test.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/{enterprise_search_content/utils => shared/decode_cloud_id}/decode_cloud_id.ts (100%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/curl_request/curl_request.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/curl_request/curl_request.tsx index 4e99b6e8eb780..6dfbf1c508361 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/curl_request/curl_request.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/curl_request/curl_request.tsx @@ -12,7 +12,7 @@ import { EuiCodeBlock } from '@elastic/eui'; import { IngestPipelineParams } from '../../../../../../../common/types/connectors'; import { useCloudDetails } from '../../../../../shared/cloud_details/cloud_details'; -import { decodeCloudId } from '../../../../utils/decode_cloud_id'; +import { decodeCloudId } from '../../../../../shared/decode_cloud_id/decode_cloud_id'; interface CurlRequestParams { apiKey?: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 2e34733f919a7..65903dde2b68f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -23,6 +23,7 @@ import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; import { externalUrl } from './shared/enterprise_search_url'; import { mountFlashMessagesLogic, Toasts } from './shared/flash_messages'; +import { getCloudEnterpriseSearchHost } from './shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host'; import { mountHttpLogic } from './shared/http'; import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; @@ -39,7 +40,8 @@ export const renderApp = ( { config, data }: { config: ClientConfigType; data: ClientData } ) => { const { publicUrl, errorConnectingMessage, ...initialData } = data; - externalUrl.enterpriseSearchUrl = publicUrl || config.host || ''; + const entCloudHost = getCloudEnterpriseSearchHost(plugins.cloud); + externalUrl.enterpriseSearchUrl = publicUrl || entCloudHost || config.host || ''; const noProductAccess: ProductAccess = { hasAppSearchAccess: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/decode_cloud_id.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/decode_cloud_id/decode_cloud_id.test.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/decode_cloud_id.test.ts rename to x-pack/plugins/enterprise_search/public/applications/shared/decode_cloud_id/decode_cloud_id.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/decode_cloud_id.ts b/x-pack/plugins/enterprise_search/public/applications/shared/decode_cloud_id/decode_cloud_id.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/decode_cloud_id.ts rename to x-pack/plugins/enterprise_search/public/applications/shared/decode_cloud_id/decode_cloud_id.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.test.ts new file mode 100644 index 0000000000000..7744c5d96a400 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getCloudEnterpriseSearchHost } from './get_cloud_enterprise_search_host'; + +const defaultPortCloud = { + cloudId: + 'gcp-cluster:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJDhhMDI4M2FmMDQxZjE5NWY3NzI5YmMwNGM2NmEwZmNlJDBjZDVjZDU2OGVlYmU1M2M4OWViN2NhZTViYWM4YjM3', + isCloudEnabled: true, + registerCloudService: jest.fn(), +}; +// 9243 +const customPortCloud = { + cloudId: + 'custom-port:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjkyNDMkYWMzMWViYjkwMjQxNzczMTU3MDQzYzM0ZmQyNmZkNDYkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA=', + isCloudEnabled: true, + registerCloudService: jest.fn(), +}; +const missingDeploymentIdCloud = { + cloudId: + 'dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjkyNDMkYWMzMWViYjkwMjQxNzczMTU3MDQzYzM0ZmQyNmZkNDYkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA=', + isCloudEnabled: true, + registerCloudService: jest.fn(), +}; +const noCloud = { + cloudId: undefined, + isCloudEnabled: false, + registerCloudService: jest.fn(), +}; + +describe('getCloudEnterpriseSearchHost', () => { + it('uses the default port', () => { + expect(getCloudEnterpriseSearchHost(defaultPortCloud)).toBe( + 'https://gcp-cluster.ent.us-central1.gcp.cloud.es.io' + ); + }); + + it('allows a custom port', () => { + expect(getCloudEnterpriseSearchHost(customPortCloud)).toBe( + 'https://custom-port.ent.us-central1.gcp.cloud.es.io:9243' + ); + }); + + it('is undefined when there is no deployment id', () => { + expect(getCloudEnterpriseSearchHost(missingDeploymentIdCloud)).toBe(undefined); + }); + + it('is undefined with an undefined cloud id', () => { + expect(getCloudEnterpriseSearchHost(noCloud)).toBe(undefined); + }); + + it('is undefined when cloud is undefined', () => { + expect(getCloudEnterpriseSearchHost(undefined)).toBe(undefined); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.ts b/x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.ts new file mode 100644 index 0000000000000..e7a61113b51ea --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/get_cloud_enterprise_search_host/get_cloud_enterprise_search_host.ts @@ -0,0 +1,33 @@ +/* + * 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 { CloudSetup } from '@kbn/cloud-plugin/public'; + +import { decodeCloudId } from '../decode_cloud_id/decode_cloud_id'; + +export function getCloudEnterpriseSearchHost(cloud: CloudSetup | undefined): string | undefined { + if (cloud && cloud.isCloudEnabled && cloud.cloudId) { + const deploymentId = getDeploymentId(cloud.cloudId); + const res = decodeCloudId(cloud.cloudId); + if (!(deploymentId && res)) { + return; + } + + // Enterprise Search Server url are formed like this `https://.ent. + return `https://${deploymentId}.ent.${res.host}${ + res.defaultPort !== '443' ? `:${res.defaultPort}` : '' + }`; + } +} + +function getDeploymentId(cloudId: string): string | undefined { + const [deploymentId, rest] = cloudId.split(':'); + + if (deploymentId && rest) { + return deploymentId; + } +} From 61f0889d66307131c06e097524def79de2a163ae Mon Sep 17 00:00:00 2001 From: Ioana Tagirta Date: Tue, 18 Oct 2022 17:24:34 +0200 Subject: [PATCH 41/74] Curations settings: use analytics_enabled flag instead of fetching log settings (#143506) * Use analytics_enabled flag instead of fetching log settings * Fix type check * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx Co-authored-by: Byron Hulcher Co-authored-by: Byron Hulcher --- .../curations_settings.test.tsx | 78 +++---------------- .../curations_settings/curations_settings.tsx | 36 ++++----- .../app_search/components/engine/types.ts | 1 + 3 files changed, 27 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx index 73559b2c4b757..0c3e9483e9233 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.test.tsx @@ -17,8 +17,6 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButtonEmpty, EuiCallOut, EuiSwitch } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; - import { docLinks } from '../../../../../shared/doc_links'; import { Loading } from '../../../../../shared/loading'; @@ -35,15 +33,12 @@ const MOCK_VALUES = { enabled: true, mode: 'automatic', }, - // LogRetentionLogic - isLogRetentionUpdating: false, - logRetention: { - [LogRetentionOptions.Analytics]: { - enabled: true, - }, - }, // LicensingLogic hasPlatinumLicense: true, + // EngineLogic + engine: { + analytics_enabled: true, + }, }; const MOCK_ACTIONS = { @@ -52,8 +47,6 @@ const MOCK_ACTIONS = { onSkipLoadingCurationsSettings: jest.fn(), toggleCurationsEnabled: jest.fn(), toggleCurationsMode: jest.fn(), - // LogRetentionLogic - fetchLogRetention: jest.fn(), }; describe('CurationsSettings', () => { @@ -62,14 +55,6 @@ describe('CurationsSettings', () => { setMockActions(MOCK_ACTIONS); }); - it('loads curations and log retention settings on load', () => { - setMockValues(MOCK_VALUES); - mountWithIntl(); - - expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalled(); - expect(MOCK_ACTIONS.fetchLogRetention).toHaveBeenCalled(); - }); - it('contains a switch to toggle curations settings', () => { let wrapper: ShallowWrapper; @@ -135,10 +120,8 @@ describe('CurationsSettings', () => { it('display a callout and disables form elements when analytics retention is disabled', () => { setMockValues({ ...MOCK_VALUES, - logRetention: { - [LogRetentionOptions.Analytics]: { - enabled: false, - }, + engine: { + analytics_enabled: false, }, }); const wrapper = shallow(); @@ -158,57 +141,25 @@ describe('CurationsSettings', () => { expect(wrapper.is(Loading)).toBe(true); }); - it('returns a loading state when log retention data is loading', () => { - setMockValues({ - ...MOCK_VALUES, - isLogRetentionUpdating: true, - }); - const wrapper = shallow(); - - expect(wrapper.is(Loading)).toBe(true); - }); - - describe('loading curation settings based on log retention', () => { - it('loads curation settings when log retention is enabled', () => { - setMockValues({ - ...MOCK_VALUES, - logRetention: { - [LogRetentionOptions.Analytics]: { - enabled: true, - }, - }, - }); - + describe('loading curation settings based on analytics logs availability', () => { + it('loads curation settings when analytics logs are enabled', () => { shallow(); expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(1); }); - it('skips loading curation settings when log retention is enabled', () => { + it('skips loading curation settings when analytics logs are disabled', () => { setMockValues({ ...MOCK_VALUES, - logRetention: { - [LogRetentionOptions.Analytics]: { - enabled: false, - }, + engine: { + analytics_enabled: false, }, }); shallow(); - expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(1); - }); - - it('takes no action if log retention has not yet been loaded', () => { - setMockValues({ - ...MOCK_VALUES, - logRetention: null, - }); - - shallow(); - expect(MOCK_ACTIONS.loadCurationsSettings).toHaveBeenCalledTimes(0); - expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(0); + expect(MOCK_ACTIONS.onSkipLoadingCurationsSettings).toHaveBeenCalledTimes(1); }); }); @@ -220,11 +171,6 @@ describe('CurationsSettings', () => { }); }); - it('it does not fetch log retention', () => { - shallow(); - expect(MOCK_ACTIONS.fetchLogRetention).toHaveBeenCalledTimes(0); - }); - it('shows a CTA to upgrade your license when the user when the user', () => { const wrapper = shallow(); expect(wrapper.is(DataPanel)).toBe(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx index ffefea96d3a22..4b7d8a777c2bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings.tsx @@ -30,7 +30,8 @@ import { Loading } from '../../../../../shared/loading'; import { EuiButtonTo } from '../../../../../shared/react_router_helpers'; import { SETTINGS_PATH } from '../../../../routes'; import { DataPanel } from '../../../data_panel'; -import { LogRetentionLogic, LogRetentionOptions } from '../../../log_retention'; + +import { EngineLogic } from '../../../engine'; import { AutomatedIcon } from '../../components/automated_icon'; @@ -50,26 +51,17 @@ export const CurationsSettings: React.FC = () => { toggleCurationsMode, } = useActions(CurationsSettingsLogic); - const { isLogRetentionUpdating, logRetention } = useValues(LogRetentionLogic); - const { fetchLogRetention } = useActions(LogRetentionLogic); - - const analyticsDisabled = !logRetention?.[LogRetentionOptions.Analytics].enabled; - - useEffect(() => { - if (hasPlatinumLicense) { - fetchLogRetention(); - } - }, [hasPlatinumLicense]); + const { + engine: { analytics_enabled: analyticsEnabled }, + } = useValues(EngineLogic); useEffect(() => { - if (logRetention) { - if (!analyticsDisabled) { - loadCurationsSettings(); - } else { - onSkipLoadingCurationsSettings(); - } + if (analyticsEnabled && dataLoading) { + loadCurationsSettings(); + } else { + onSkipLoadingCurationsSettings(); } - }, [logRetention]); + }, [analyticsEnabled, dataLoading]); if (!hasPlatinumLicense) return ( @@ -117,7 +109,7 @@ export const CurationsSettings: React.FC = () => { ); - if (dataLoading || isLogRetentionUpdating) return ; + if (dataLoading) return ; return ( <> @@ -139,7 +131,7 @@ export const CurationsSettings: React.FC = () => {
      - {analyticsDisabled && ( + {!analyticsEnabled && ( <> { } )} checked={enabled} - disabled={analyticsDisabled} + disabled={!analyticsEnabled} onChange={toggleCurationsEnabled} /> @@ -202,7 +194,7 @@ export const CurationsSettings: React.FC = () => { } )} checked={mode === 'automatic'} - disabled={analyticsDisabled} + disabled={!analyticsEnabled} onChange={toggleCurationsMode} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index 1b4aa08980ef5..e8c9dfaa63483 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -63,6 +63,7 @@ export interface EngineDetails extends Engine { includedEngines?: EngineDetails[]; adaptive_relevance_suggestions?: SearchRelevanceSuggestionDetails; adaptive_relevance_suggestions_active: boolean; + analytics_enabled?: boolean; } interface ResultField { From 608a05d2d9430b40f639e7056b8e435e58b5bfce Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Tue, 18 Oct 2022 10:24:55 -0500 Subject: [PATCH 42/74] [Enterprise Search] promote copy ingest pipeline (#143396) Promited the copy and customize action from within the modal to the ingest pipeline card. This is to make it more discoverable for users and give better UX. --- .../customize_pipeline_item.test.tsx | 68 +++++++++++++++ .../customize_pipeline_item.tsx | 67 +++++++++++++++ .../ingest_pipeline_modal.tsx | 41 --------- .../ingest_pipelines_card.test.tsx | 79 +++++++++++++++++ .../ingest_pipelines_card.tsx | 86 +++++++++---------- 5 files changed, 257 insertions(+), 84 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.test.tsx new file mode 100644 index 0000000000000..6d2e832387315 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../../__mocks__/kea_logic'; +import { crawlerIndex } from '../../../../__mocks__/view_index.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText, EuiButtonEmpty } from '@elastic/eui'; + +import { CustomizeIngestPipelineItem } from './customize_pipeline_item'; + +const DEFAULT_VALUES = { + // LicensingLogic + hasPlatinumLicense: true, + // IndexViewLogic + indexName: crawlerIndex.name, + ingestionMethod: 'crawler', + // KibanaLogic + isCloud: false, +}; + +describe('CustomizeIngestPipelineItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({ ...DEFAULT_VALUES }); + }); + it('renders cta with license', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(1); + expect(wrapper.find(EuiText).children().text()).toContain('create an index-specific version'); + expect(wrapper.find(EuiText).children().text()).not.toContain('With a platinum license'); + }); + it('renders cta on cloud', () => { + setMockValues({ + ...DEFAULT_VALUES, + hasPlatinumLicense: false, + isCloud: true, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiText)).toHaveLength(1); + expect(wrapper.find(EuiText).children().text()).toContain('create an index-specific version'); + expect(wrapper.find(EuiText).children().text()).not.toContain('With a platinum license'); + }); + it('gates cta without license', () => { + setMockValues({ + ...DEFAULT_VALUES, + hasPlatinumLicense: false, + isCloud: false, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(1); + + const ctaButton = wrapper.find(EuiButtonEmpty); + expect(ctaButton.prop('disabled')).toBe(true); + expect(ctaButton.prop('iconType')).toBe('lock'); + + expect(wrapper.find(EuiText).children().text()).toContain('With a platinum license'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx new file mode 100644 index 0000000000000..a16274b26236d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { KibanaLogic } from '../../../../../shared/kibana'; +import { LicensingLogic } from '../../../../../shared/licensing'; +import { CreateCustomPipelineApiLogic } from '../../../../api/index/create_custom_pipeline_api_logic'; + +import { IndexViewLogic } from '../../index_view_logic'; + +export const CustomizeIngestPipelineItem: React.FC = () => { + const { indexName, ingestionMethod } = useValues(IndexViewLogic); + const { isCloud } = useValues(KibanaLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); + const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic); + const isGated = !isCloud && !hasPlatinumLicense; + + return ( + <> + {isGated ? ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.copyAndCustomize.platinumText', + { + defaultMessage: + 'With a platinum license, you can create an index-specific version of this configuration and modify it for your use case.', + } + )} + + ) : ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.copyAndCustomize.description', + { + defaultMessage: + 'You can create an index-specific version of this configuration and modify it for your use case.', + } + )} + + )} + + createCustomPipeline({ indexName })} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.ingestModal.copyButtonLabel', + { defaultMessage: 'Copy and customize' } + )} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx index 6be65b6387d49..291108a251e37 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipeline_modal.tsx @@ -27,8 +27,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DEFAULT_PIPELINE_NAME } from '../../../../../../../common/constants'; - import { IngestPipelineParams } from '../../../../../../../common/types/connectors'; import { CurlRequest } from '../../components/curl_request/curl_request'; @@ -37,11 +35,9 @@ import { PipelineSettingsForm } from '../pipeline_settings_form'; interface IngestPipelineModalProps { closeModal: () => void; - createCustomPipelines: () => void; displayOnly: boolean; indexName: string; ingestionMethod: string; - isGated: boolean; isLoading: boolean; pipeline: IngestPipelineParams; savePipeline: () => void; @@ -50,11 +46,9 @@ interface IngestPipelineModalProps { export const IngestPipelineModal: React.FC = ({ closeModal, - createCustomPipelines, displayOnly, indexName, ingestionMethod, - isGated, isLoading, pipeline, savePipeline, @@ -62,9 +56,6 @@ export const IngestPipelineModal: React.FC = ({ }) => { const { name } = pipeline; - // can't customize if you already have a custom pipeline! - const canCustomize = name === DEFAULT_PIPELINE_NAME; - return ( @@ -192,38 +183,6 @@ export const IngestPipelineModal: React.FC = ({ )} - {canCustomize && ( - <> - - - - {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.ingestModal.platinumText', - { - defaultMessage: - 'With a platinum license, you can create an index-specific version of this configuration and modify it for your use case.', - } - )} - - - - - - - {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.ingestModal.copyButtonLabel', - { defaultMessage: 'Copy and customize' } - )} - - - - - )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx new file mode 100644 index 0000000000000..0ca347da0712e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../../__mocks__/kea_logic'; +import { crawlerIndex } from '../../../../__mocks__/view_index.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; + +import { DEFAULT_PIPELINE_NAME } from '../../../../../../../common/constants'; + +import { CustomPipelineItem } from './custom_pipeline_item'; +import { CustomizeIngestPipelineItem } from './customize_pipeline_item'; +import { DefaultPipelineItem } from './default_pipeline_item'; +import { IngestPipelinesCard } from './ingest_pipelines_card'; + +const DEFAULT_VALUES = { + // IndexViewLogic + indexName: crawlerIndex.name, + ingestionMethod: 'crawler', + // FetchCustomPipelineApiLogic + data: undefined, + // PipelinesLogic + canSetPipeline: true, + hasIndexIngestionPipeline: false, + index: crawlerIndex, + pipelineName: DEFAULT_PIPELINE_NAME, + pipelineState: { + extract_binary_content: true, + name: DEFAULT_PIPELINE_NAME, + reduce_whitespace: true, + run_ml_inference: false, + }, + showModal: false, +}; + +describe('IngestPipelinesCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({ ...DEFAULT_VALUES }); + }); + it('renders with default ingest pipeline', () => { + const wrapper = shallow(); + expect(wrapper.find(DefaultPipelineItem)).toHaveLength(1); + expect(wrapper.find(CustomizeIngestPipelineItem)).toHaveLength(1); + expect(wrapper.find(CustomPipelineItem)).toHaveLength(0); + }); + it('does not render customize cta with index ingest pipeline', () => { + const pipelineName = crawlerIndex.name; + const pipelines: Record = { + [pipelineName]: {}, + [`${pipelineName}@custom`]: { + processors: [], + }, + }; + setMockValues({ + ...DEFAULT_VALUES, + data: pipelines, + hasIndexIngestionPipeline: true, + pipelineName, + pipelineState: { + ...DEFAULT_VALUES.pipelineState, + name: pipelineName, + }, + }); + + const wrapper = shallow(); + expect(wrapper.find(CustomizeIngestPipelineItem)).toHaveLength(0); + expect(wrapper.find(DefaultPipelineItem)).toHaveLength(1); + expect(wrapper.find(CustomPipelineItem)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx index 7406d9e89150b..92ec7ac7c0105 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx @@ -11,32 +11,31 @@ import { useActions, useValues } from 'kea'; import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { KibanaLogic } from '../../../../../shared/kibana'; - -import { LicensingLogic } from '../../../../../shared/licensing'; -import { CreateCustomPipelineApiLogic } from '../../../../api/index/create_custom_pipeline_api_logic'; import { FetchCustomPipelineApiLogic } from '../../../../api/index/fetch_custom_pipeline_api_logic'; import { IndexViewLogic } from '../../index_view_logic'; import { PipelinesLogic } from '../pipelines_logic'; import { CustomPipelineItem } from './custom_pipeline_item'; +import { CustomizeIngestPipelineItem } from './customize_pipeline_item'; import { DefaultPipelineItem } from './default_pipeline_item'; import { IngestPipelineModal } from './ingest_pipeline_modal'; export const IngestPipelinesCard: React.FC = () => { const { indexName, ingestionMethod } = useValues(IndexViewLogic); - const { canSetPipeline, index, pipelineName, pipelineState, showModal } = - useValues(PipelinesLogic); + const { + canSetPipeline, + hasIndexIngestionPipeline, + index, + pipelineName, + pipelineState, + showModal, + } = useValues(PipelinesLogic); const { closeModal, openModal, setPipelineState, savePipeline } = useActions(PipelinesLogic); const { makeRequest: fetchCustomPipeline } = useActions(FetchCustomPipelineApiLogic); - const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic); const { data: customPipelines } = useValues(FetchCustomPipelineApiLogic); - const { isCloud } = useValues(KibanaLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); - const isGated = !isCloud && !hasPlatinumLicense; const customPipeline = customPipelines ? customPipelines[`${indexName}@custom`] : undefined; useEffect(() => { @@ -44,45 +43,46 @@ export const IngestPipelinesCard: React.FC = () => { }, [indexName]); return ( - - {showModal && ( - createCustomPipeline({ indexName })} - displayOnly={!canSetPipeline} - indexName={indexName} - ingestionMethod={ingestionMethod} - isGated={isGated} - isLoading={false} - pipeline={{ ...pipelineState, name: pipelineName }} - savePipeline={savePipeline} - setPipeline={setPipelineState} - /> - )} - - - + {!hasIndexIngestionPipeline && } + + {showModal && ( + - - - {customPipeline && ( + )} - - + - )} - + {customPipeline && ( + + + + + + )} + + ); }; From 34ad684839e7729e0712475570214d83340e7324 Mon Sep 17 00:00:00 2001 From: Arpit Bhardwaj Date: Tue, 18 Oct 2022 21:32:30 +0530 Subject: [PATCH 43/74] Changed default test for option list (#143413) --- .../public/options_list/components/options_list_strings.ts | 2 +- .../functional/apps/dashboard_elements/controls/options_list.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/controls/public/options_list/components/options_list_strings.ts b/src/plugins/controls/public/options_list/components/options_list_strings.ts index 536c403413e27..8e3d55ae6b764 100644 --- a/src/plugins/controls/public/options_list/components/options_list_strings.ts +++ b/src/plugins/controls/public/options_list/components/options_list_strings.ts @@ -16,7 +16,7 @@ export const OptionsListStrings = { }), getPlaceholder: () => i18n.translate('controls.optionsList.control.placeholder', { - defaultMessage: 'Select...', + defaultMessage: 'Any', }), }, editor: { diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 56c1778d31def..0c8dea528d9e4 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -145,7 +145,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.controlEditorSave(); const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId); - expect(selectionString).to.be('Select...'); + expect(selectionString).to.be('Any'); }); it('editing other control settings keeps selections', async () => { From aed3a0742239c402504334e7cc5d70ea60ede360 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Tue, 18 Oct 2022 09:10:53 -0700 Subject: [PATCH 44/74] fixed output aggregate query. bucket_sort options incorrect. was always running max 10 results. (#143486) Co-authored-by: Karl Godard --- .../session_view/server/routes/io_events_route.ts | 11 +++-------- .../server/routes/process_events_route.ts | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/session_view/server/routes/io_events_route.ts b/x-pack/plugins/session_view/server/routes/io_events_route.ts index 7a88eacdeed7e..790173511ff35 100644 --- a/x-pack/plugins/session_view/server/routes/io_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/io_events_route.ts @@ -79,7 +79,7 @@ export const searchProcessWithIOEvents = async ( ? [ { range: { - '@timestamp': { + [TIMESTAMP]: { gte: range[0], lte: range[1], }, @@ -105,13 +105,7 @@ export const searchProcessWithIOEvents = async ( custom_agg: { terms: { field: PROCESS_ENTITY_ID_PROPERTY, - }, - aggs: { - bucket_sort: { - bucket_sort: { - size: PROCESS_EVENTS_PER_PAGE, - }, - }, + size: PROCESS_EVENTS_PER_PAGE, }, }, }, @@ -126,6 +120,7 @@ export const searchProcessWithIOEvents = async ( event: { kind: EventKind.event, action: EventAction.text_output, + id: bucket.key, }, process: { entity_id: bucket.key, diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index 26b666307dc1e..f022839940912 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -133,7 +133,7 @@ export const fetchEventsAndScopedAlerts = async ( const processesWithIOEvents = await searchProcessWithIOEvents(client, sessionEntityId, range); - events = [...alertsBody.events, ...processesWithIOEvents, ...events]; // we place process events at the end, as they have proper cursor info. (putting the 'faked' io event indicators at end breaks pagination, since they lack a timestamp). + events = [...events, ...alertsBody.events, ...processesWithIOEvents]; } return { From 2c746e02e921cb9ffa4f54d6b8c351f47b3596e9 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 18 Oct 2022 18:22:47 +0200 Subject: [PATCH 45/74] [Synthetics] Monitor errors tab errors list (#143166) --- .../common/constants/data_filters.ts | 12 ++ .../plugins/synthetics/common/constants/ui.ts | 2 + .../common/runtime_types/ping/error_state.ts | 19 +++ .../common/runtime_types/ping/ping.ts | 2 + .../date_picker/synthetics_date_picker.tsx | 3 +- .../error_details/error_details_page.tsx | 17 ++ .../hooks/use_error_failed_step.tsx | 66 ++++++++ .../hooks/use_monitor_errors.tsx | 92 +++++++++++ .../monitor_errors/errors_list.tsx | 154 ++++++++++++++++++ .../monitor_errors/monitor_errors.tsx | 77 ++++++++- .../monitor_summary/monitor_errors_count.tsx | 2 +- .../public/apps/synthetics/routes.tsx | 19 +++ .../monitor_test_result/test_time_formats.ts | 19 ++- 13 files changed, 475 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/synthetics/common/constants/data_filters.ts create mode 100644 x-pack/plugins/synthetics/common/runtime_types/ping/error_state.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_error_failed_step.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx diff --git a/x-pack/plugins/synthetics/common/constants/data_filters.ts b/x-pack/plugins/synthetics/common/constants/data_filters.ts new file mode 100644 index 0000000000000..d7ca3b43376b9 --- /dev/null +++ b/x-pack/plugins/synthetics/common/constants/data_filters.ts @@ -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. + */ + +export const STEP_END_FILTER = { + terms: { + 'synthetics.type': ['step/end'], + }, +}; diff --git a/x-pack/plugins/synthetics/common/constants/ui.ts b/x-pack/plugins/synthetics/common/constants/ui.ts index 4fb8a374f944b..100c49ccb8e80 100644 --- a/x-pack/plugins/synthetics/common/constants/ui.ts +++ b/x-pack/plugins/synthetics/common/constants/ui.ts @@ -33,6 +33,8 @@ export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps'; export const MAPPING_ERROR_ROUTE = '/mapping-error'; +export const ERROR_DETAILS_ROUTE = '/error-details/:errorStateId'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/synthetics/common/runtime_types/ping/error_state.ts b/x-pack/plugins/synthetics/common/runtime_types/ping/error_state.ts new file mode 100644 index 0000000000000..1830ff11f70e7 --- /dev/null +++ b/x-pack/plugins/synthetics/common/runtime_types/ping/error_state.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; + +export const ErrorStateCodec = t.type({ + duration_ms: t.number, + checks: t.number, + ends: t.union([t.string, t.null]), + started_at: t.string, + id: t.string, + up: t.number, + down: t.number, + status: t.string, +}); diff --git a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts index d903a855018d7..ebfe80c3a57e2 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { ErrorStateCodec } from './error_state'; import { DateRangeType } from '../common'; import { SyntheticsDataType } from './synthetics'; @@ -232,6 +233,7 @@ export const PingType = t.intersection([ name: t.string, }), config_id: t.string, + state: ErrorStateCodec, data_stream: t.interface({ namespace: t.string, type: t.string, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx index b9da1c098716d..61398c562d136 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx @@ -21,7 +21,7 @@ const isSyntheticsDefaultDateRange = (dateRangeStart: string, dateRangeEnd: stri return dateRangeStart === DATE_RANGE_START && dateRangeEnd === DATE_RANGE_END; }; -export const SyntheticsDatePicker = () => { +export const SyntheticsDatePicker = ({ fullWidth }: { fullWidth?: boolean }) => { const [getUrlParams, updateUrl] = useUrlParams(); const { commonlyUsedRanges } = useContext(SyntheticsSettingsContext); const { refreshApp } = useContext(SyntheticsRefreshContext); @@ -66,6 +66,7 @@ export const SyntheticsDatePicker = () => { return ( + TODO: +
      + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_error_failed_step.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_error_failed_step.tsx new file mode 100644 index 0000000000000..cdc75477c5134 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_error_failed_step.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEsSearch } from '@kbn/observability-plugin/public'; +import { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { STEP_END_FILTER } from '../../../../../../common/constants/data_filters'; +import { asMutableArray } from '../../../../../../common/utils/as_mutable_array'; +import { Ping } from '../../../../../../common/runtime_types'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { useSyntheticsRefreshContext } from '../../../contexts'; + +export function useErrorFailedStep(checkGroups: string[]) { + const { lastRefresh } = useSyntheticsRefreshContext(); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const { data, loading } = useEsSearch( + { + index: checkGroups?.length > 0 ? SYNTHETICS_INDEX_PATTERN : '', + body: { + size: checkGroups.length, + query: { + bool: { + filter: [ + STEP_END_FILTER, + { + exists: { + field: 'synthetics.error', + }, + }, + { + terms: { + 'monitor.check_group': checkGroups, + }, + }, + ] as QueryDslQueryContainer[], + }, + }, + sort: asMutableArray([ + { 'synthetics.step.index': { order: 'asc' } }, + { '@timestamp': { order: 'asc' } }, + ] as const), + _source: ['synthetics.step', 'synthetics.error', 'monitor.check_group'], + }, + }, + [lastRefresh, monitorId, checkGroups], + { name: 'getMonitorErrorFailedStep' } + ); + + return useMemo(() => { + const failedSteps = (data?.hits.hits ?? []).map((doc) => { + return doc._source as Ping; + }); + + return { + failedSteps, + loading, + }; + }, [data, loading]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx new file mode 100644 index 0000000000000..dd8f9b9ff7c7b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_errors.tsx @@ -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 { useEsSearch } from '@kbn/observability-plugin/public'; +import { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { Ping } from '../../../../../../common/runtime_types'; +import { + EXCLUDE_RUN_ONCE_FILTER, + SUMMARY_FILTER, +} from '../../../../../../common/constants/client_defaults'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { useSyntheticsRefreshContext } from '../../../contexts'; +import { useGetUrlParams } from '../../../hooks'; + +export function useMonitorErrors(monitorIdArg?: string) { + const { lastRefresh } = useSyntheticsRefreshContext(); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + + const { data, loading } = useEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + filter: [ + SUMMARY_FILTER, + EXCLUDE_RUN_ONCE_FILTER, + { + range: { + '@timestamp': { + gte: dateRangeStart, + lte: dateRangeEnd, + }, + }, + }, + { + term: { + 'state.up': 0, + }, + }, + { + term: { + config_id: monitorIdArg ?? monitorId, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': 'desc' }], + aggs: { + errorStates: { + terms: { + field: 'state.id', + size: 1000, + }, + aggs: { + summary: { + top_hits: { + size: 1, + _source: ['error', 'state', 'monitor'], + sort: [{ '@timestamp': 'desc' }], + }, + }, + }, + }, + }, + }, + }, + [lastRefresh, monitorId, monitorIdArg, dateRangeStart, dateRangeEnd], + { name: 'getMonitorErrors' } + ); + + return useMemo(() => { + const errorStates = (data?.aggregations?.errorStates.buckets ?? []).map((loc) => { + return loc.summary.hits.hits?.[0]._source as Ping; + }); + + return { + errorStates, + loading, + }; + }, [data, loading]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx new file mode 100644 index 0000000000000..f16a570f6f812 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx @@ -0,0 +1,154 @@ +/* + * 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'; +import React, { MouseEvent, useMemo, useState } from 'react'; +import { EuiBasicTable, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { Ping } from '../../../../../../common/runtime_types'; +import { useErrorFailedStep } from '../hooks/use_error_failed_step'; +import { + formatTestDuration, + formatTestRunAt, +} from '../../../utils/monitor_test_result/test_time_formats'; +import { useMonitorErrors } from '../hooks/use_monitor_errors'; +import { useSyntheticsSettingsContext } from '../../../contexts'; + +export const ErrorsList = () => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [sortField, setSortField] = useState('@timestamp'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + + const { errorStates, loading } = useMonitorErrors(); + + const items = errorStates.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); + + const checkGroups = useMemo(() => { + const currentPage = errorStates.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); + + return currentPage.map((error) => error.monitor.check_group!); + }, [errorStates, pageIndex, pageSize]); + + const { failedSteps } = useErrorFailedStep(checkGroups); + + const isBrowserType = errorStates[0]?.monitor.type === 'browser'; + + const { basePath } = useSyntheticsSettingsContext(); + + const history = useHistory(); + + const columns = [ + { + field: '@timestamp', + name: TIMESTAMP_LABEL, + sortable: true, + render: (value: string, item: Ping) => { + return ( + + {formatTestRunAt(item.state!.started_at)} + + ); + }, + }, + { + field: 'monitor.check_group', + name: !isBrowserType ? ERROR_MESSAGE_LABEL : FAILED_STEP_LABEL, + truncateText: true, + render: (value: string, item: Ping) => { + if (!isBrowserType) { + return {item.error?.message ?? '--'}; + } + const failedStep = failedSteps.find((step) => step.monitor.check_group === value); + if (!failedStep) { + return <>--; + } + return ( + + {failedStep.synthetics?.step?.index}. {failedStep.synthetics?.step?.name} + + ); + }, + }, + { + field: 'state.duration_ms', + name: ERROR_DURATION_LABEL, + align: 'right' as const, + render: (value: number) => {formatTestDuration(value, true)}, + }, + ]; + + const pagination = { + pageIndex, + pageSize, + totalItemCount: errorStates.length, + pageSizeOptions: [3, 5, 8], + }; + + const getRowProps = (item: Ping) => { + const { state } = item; + if (state?.id) { + return { + height: '85px', + 'data-test-subj': `row-${state.id}`, + onClick: (evt: MouseEvent) => { + history.push(`/error-details/${state.id}`); + }, + }; + } + }; + + return ( +
      + + { + const { index: pIndex, size: pSize } = page; + + const { field: sField, direction: sDirection } = sort; + + setPageIndex(pIndex!); + setPageSize(pSize!); + setSortField(sField!); + setSortDirection(sDirection!); + }} + rowProps={getRowProps} + /> +
      + ); +}; + +const ERRORS_LIST_LABEL = i18n.translate('xpack.synthetics.errorsList.label', { + defaultMessage: 'Errors list', +}); + +const ERROR_DURATION_LABEL = i18n.translate('xpack.synthetics.errorDuration.label', { + defaultMessage: 'Error duration', +}); + +const ERROR_MESSAGE_LABEL = i18n.translate('xpack.synthetics.errorMessage.label', { + defaultMessage: 'Error message', +}); + +const FAILED_STEP_LABEL = i18n.translate('xpack.synthetics.failedStep.label', { + defaultMessage: 'Failed step', +}); + +const TIMESTAMP_LABEL = i18n.translate('xpack.synthetics.timestamp.label', { + defaultMessage: '@timestamp', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx index 479aa6ac72bfb..b84669856ec3b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/monitor_errors.tsx @@ -4,9 +4,82 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiText } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useGetUrlParams } from '../../../hooks'; +import { SyntheticsDatePicker } from '../../common/date_picker/synthetics_date_picker'; +import { MonitorErrorsCount } from '../monitor_summary/monitor_errors_count'; +import { ErrorsList } from './errors_list'; export const MonitorErrors = () => { - return Monitor errors tabs content; + const { euiTheme } = useEuiTheme(); + + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + + return ( + <> + + + + + + +

      {OVERVIEW_LABEL}

      +
      + +
      +
      + + + +

      {FAILED_TESTS_LABEL}

      +
      +
      +
      +
      + + + + +

      {ERRORS_LABEL}

      +
      + +
      +
      + + + +

      + {FAILED_TESTS_BY_STEPS_LABEL} +

      +
      +
      +
      +
      + + ); }; + +const ERRORS_LABEL = i18n.translate('xpack.synthetics.errors.label', { + defaultMessage: 'Errors', +}); + +const OVERVIEW_LABEL = i18n.translate('xpack.synthetics.errors.overview', { + defaultMessage: 'Overview', +}); + +const FAILED_TESTS_LABEL = i18n.translate('xpack.synthetics.errors.failedTests', { + defaultMessage: 'Failed tests', +}); + +const FAILED_TESTS_BY_STEPS_LABEL = i18n.translate('xpack.synthetics.errors.failedTests.byStep', { + defaultMessage: 'Failed tests by step', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx index 4076e24de49d7..e9eb11878d8bf 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx @@ -12,7 +12,7 @@ import { useParams } from 'react-router-dom'; import { KpiWrapper } from './kpi_wrapper'; import { ClientPluginsStart } from '../../../../../plugin'; -export const MonitorErrorsCount = () => { +export const MonitorErrorsCount = ({ time }: { time?: { to: string; from: string } }) => { const { observability } = useKibana().services; const { ExploratoryViewEmbeddable } = observability; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 92ec0afce43ff..6319610f8e8c7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -23,6 +23,7 @@ import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; +import { ErrorDetailsPage } from './components/error_details/error_details_page'; import { MonitorAddPage } from './components/monitor_add_edit/monitor_add_page'; import { MonitorEditPage } from './components/monitor_add_edit/monitor_edit_page'; import { MonitorDetailsPageTitle } from './components/monitor_details/monitor_details_page_title'; @@ -45,6 +46,7 @@ import { MONITOR_ERRORS_ROUTE, MONITOR_HISTORY_ROUTE, MONITOR_ROUTE, + ERROR_DETAILS_ROUTE, OVERVIEW_ROUTE, } from '../../../common/constants'; import { PLUGIN } from '../../../common/constants/plugin'; @@ -283,6 +285,23 @@ const getRoutes = ( ], }, }, + { + title: i18n.translate('xpack.synthetics.errorDetailsRoute.title', { + defaultMessage: 'Error details | {baseTitle}', + values: { baseTitle }, + }), + path: ERROR_DETAILS_ROUTE, + component: () => , + dataTestSubj: 'syntheticsMonitorEditPage', + pageHeader: { + pageTitle: ( + + ), + }, + }, ]; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts index 005872224b08e..33b62a6c65c27 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts @@ -10,16 +10,25 @@ import { i18n } from '@kbn/i18n'; /** * Formats the microseconds (µ) into either milliseconds (ms) or seconds (s) based on the duration value - * @param us {number} duration value in microseconds + * @param duration + * @param isMilli */ -export const formatTestDuration = (us?: number) => { - const microSecs = us ?? 0; - const secs = microSecs / (1000 * 1000); +export const formatTestDuration = (duration = 0, isMilli = false) => { + const secs = isMilli ? duration / 1e3 : duration / 1e6; + + if (secs >= 60) { + return `${(secs / 60).toFixed(1)} min`; + } + if (secs >= 1) { return `${secs.toFixed(1)} s`; } - return `${(microSecs / 1000).toFixed(0)} ms`; + if (isMilli) { + return `${duration.toFixed(0)} ms`; + } + + return `${(duration / 1000).toFixed(0)} ms`; }; export function formatTestRunAt(timestamp: string) { From de7c17357c4856ca106ea34aa75e4b3601c7fa4d Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 18 Oct 2022 12:50:16 -0400 Subject: [PATCH 46/74] [Guided onboarding] Implement Observability e2e guide (#143332) --- .../constants/guides_config/observability.ts | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts b/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts index 1ef3c155dfdb5..90a397e44e953 100644 --- a/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts +++ b/src/plugins/guided_onboarding/public/constants/guides_config/observability.ts @@ -6,22 +6,36 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; + import type { GuideConfig } from '../../types'; export const observabilityConfig: GuideConfig = { - title: 'Observe my Kubernetes infrastructure', - description: `We'll help you quickly gain visibility into your Kubernetes environment using Elastic's out-of-the-box integration. Gain deep insights from your logs, metrics, and traces, and proactively detect issues and take action to resolve issues.`, + title: i18n.translate('guidedOnboarding.observabilityGuide.title', { + defaultMessage: 'Observe my Kubernetes infrastructure', + }), + description: i18n.translate('guidedOnboarding.observabilityGuide.description', { + defaultMessage: `We'll help you quickly gain visibility into your Kubernetes environment using Elastic's out-of-the-box integration. Gain deep insights from your logs, metrics, and traces, and proactively detect issues and take action to resolve issues.`, + }), guideName: 'Kubernetes', docs: { - text: 'Kubernetes documentation', - url: 'example.com', // TODO update link to correct docs page + text: i18n.translate('guidedOnboarding.observabilityGuide.documentationLink', { + defaultMessage: 'Kubernetes documentation', + }), + url: 'https://docs.elastic.co/en/integrations/kubernetes', }, steps: [ { id: 'add_data', - title: 'Add data', + title: i18n.translate('guidedOnboarding.observabilityGuide.addDataStep.title', { + defaultMessage: 'Add data', + }), integration: 'kubernetes', - descriptionList: ['Start by adding your data by setting up the Kubernetes integration.'], + descriptionList: [ + i18n.translate('guidedOnboarding.observabilityGuide.addDataStep.description', { + defaultMessage: 'Start by adding your data by setting up the Kubernetes integration.', + }), + ], location: { appID: 'integrations', path: '/detail/kubernetes/overview', @@ -29,20 +43,44 @@ export const observabilityConfig: GuideConfig = { }, { id: 'view_dashboard', - title: 'Explore Kubernetes metrics', - descriptionList: ['Stream, visualize, and analyze your Kubernetes infrastructure metrics.'], + title: i18n.translate('guidedOnboarding.observabilityGuide.viewDashboardStep.title', { + defaultMessage: 'Explore Kubernetes metrics', + }), + descriptionList: [ + i18n.translate('guidedOnboarding.observabilityGuide.viewDashboardStep.description', { + defaultMessage: 'Stream, visualize, and analyze your Kubernetes infrastructure metrics.', + }), + ], location: { appID: 'dashboards', path: '#/view/kubernetes-e0195ce0-bcaf-11ec-b64f-7dd6e8e82013', }, + manualCompletion: { + title: i18n.translate( + 'guidedOnboarding.observabilityGuide.viewDashboardStep.manualCompletionPopoverTitle', + { + defaultMessage: 'Explore the pre-built Kubernetes dashboards', + } + ), + description: i18n.translate( + 'guidedOnboarding.observabilityGuide.viewDashboardStep.manualCompletionPopoverDescription', + { + defaultMessage: `Take your time to explore out-of-the-box dashboards that are included with the Kubernetes integration. When you're ready, you can access the next step of the guide in the button above.`, + } + ), + readyToCompleteOnNavigation: true, + }, }, { id: 'tour_observability', - title: 'Tour Elastic Observability', + title: i18n.translate('guidedOnboarding.observabilityGuide.tourObservabilityStep.title', { + defaultMessage: 'Tour Elastic Observability', + }), descriptionList: [ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Nullam ligula enim, malesuada a finibus vel, cursus sed risus.', - 'Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.', + i18n.translate('guidedOnboarding.observabilityGuide.tourObservabilityStep.description', { + defaultMessage: + 'Take a look at the capabilities of our Observability solution and be inspired to add more integrations.', + }), ], }, ], From 137b4bd31673dcc8dd503b0272b513266c9d2c36 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 18 Oct 2022 19:07:43 +0200 Subject: [PATCH 47/74] [Exploratory view] Added timings breakdown (#143170) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../definitions/formula/formula_public_api.ts | 5 +- .../common/field_names/infra_logs.ts | 10 + .../common/field_names/infra_metrics.ts | 11 + .../common/field_names/synthetics.ts | 35 +++ x-pack/plugins/observability/common/index.ts | 13 + .../e2e/journeys/exploratory_view.ts | 2 +- .../components/filter_label.tsx | 4 +- .../configurations/constants/constants.ts | 33 ++ .../constants/field_names/synthetics.ts | 20 ++ .../configurations/constants/labels.ts | 7 + .../configurations/lens_attributes.test.ts | 46 +-- .../configurations/lens_attributes.ts | 289 ++++++++++++------ .../sample_formula_metric_attribute.ts | 10 +- .../single_metric_attributes.ts | 109 ++++--- .../synthetics/field_formats.ts | 8 + .../synthetics/kpi_over_time_config.ts | 23 +- .../synthetics/runtime_fields.ts | 62 ++++ .../synthetics/single_metric_config.ts | 19 +- .../test_data/mobile_test_attribute.ts | 8 +- .../test_data/sample_attribute.ts | 8 +- .../test_data/sample_attribute_cwv.ts | 8 +- .../test_data/sample_attribute_kpi.ts | 8 +- .../sample_attribute_with_reference_lines.ts | 8 +- .../exploratory_view/configurations/utils.ts | 6 +- .../hooks/use_series_filters.ts | 6 +- .../columns/filter_value_btn.tsx | 2 +- .../series_editor/expanded_series_row.tsx | 6 +- .../shared/exploratory_view/types.ts | 9 +- .../utils/stringify_kueries.ts | 2 +- .../filter_value_label/filter_value_label.tsx | 12 +- .../observability_data_views.ts | 22 +- 31 files changed, 609 insertions(+), 202 deletions(-) create mode 100644 x-pack/plugins/observability/common/field_names/infra_logs.ts create mode 100644 x-pack/plugins/observability/common/field_names/infra_metrics.ts create mode 100644 x-pack/plugins/observability/common/field_names/synthetics.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/runtime_fields.ts diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula_public_api.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula_public_api.ts index 231cd82804c8f..ab17fd81e84ef 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula_public_api.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/formula_public_api.ts @@ -6,6 +6,7 @@ */ import type { DataView } from '@kbn/data-views-plugin/public'; +import { Query } from '@kbn/es-query'; import { convertDataViewIntoLensIndexPattern } from '../../../../../data_views_service/loader'; import type { IndexPattern } from '../../../../../types'; import type { PersistedIndexPatternLayer } from '../../../types'; @@ -31,6 +32,7 @@ export interface FormulaPublicApi { column: { formula: string; label?: string; + filter?: Query; format?: { id: string; params?: { @@ -58,7 +60,7 @@ export const createFormulaPublicApi = (): FormulaPublicApi => { }; return { - insertOrReplaceFormulaColumn: (id, { formula, label, format }, layer, dataView) => { + insertOrReplaceFormulaColumn: (id, { formula, label, format, filter }, layer, dataView) => { const indexPattern = getCachedLensIndexPattern(dataView); return insertOrReplaceFormulaColumn( @@ -70,6 +72,7 @@ export const createFormulaPublicApi = (): FormulaPublicApi => { dataType: 'number', references: [], isBucketed: false, + filter, params: { formula, format, diff --git a/x-pack/plugins/observability/common/field_names/infra_logs.ts b/x-pack/plugins/observability/common/field_names/infra_logs.ts new file mode 100644 index 0000000000000..4e81264fd7fbd --- /dev/null +++ b/x-pack/plugins/observability/common/field_names/infra_logs.ts @@ -0,0 +1,10 @@ +/* + * 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 { DOCUMENT_FIELD_NAME } from '@kbn/lens-plugin/common'; + +export const LOG_RATE = DOCUMENT_FIELD_NAME; diff --git a/x-pack/plugins/observability/common/field_names/infra_metrics.ts b/x-pack/plugins/observability/common/field_names/infra_metrics.ts new file mode 100644 index 0000000000000..26683dd2a206e --- /dev/null +++ b/x-pack/plugins/observability/common/field_names/infra_metrics.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SYSTEM_CPU_PERCENTAGE_FIELD = 'system.cpu.total.norm.pct'; +export const SYSTEM_MEMORY_PERCENTAGE_FIELD = 'system.memory.used.pct'; +export const DOCKER_CPU_PERCENTAGE_FIELD = 'docker.cpu.total.pct'; +export const K8S_POD_CPU_PERCENTAGE_FIELD = 'kubernetes.pod.cpu.usage.node.pct'; diff --git a/x-pack/plugins/observability/common/field_names/synthetics.ts b/x-pack/plugins/observability/common/field_names/synthetics.ts new file mode 100644 index 0000000000000..003be106ffaaa --- /dev/null +++ b/x-pack/plugins/observability/common/field_names/synthetics.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const MONITOR_DURATION_US = 'monitor.duration.us'; +export const SYNTHETICS_CLS = 'browser.experience.cls'; +export const SYNTHETICS_LCP = 'browser.experience.lcp.us'; +export const SYNTHETICS_FCP = 'browser.experience.fcp.us'; +export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us'; +export const SYNTHETICS_DCL = 'browser.experience.dcl.us'; +export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword'; +export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; + +export const SYNTHETICS_DNS_TIMINGS = 'synthetics.payload.timings.dns'; +export const SYNTHETICS_SSL_TIMINGS = 'synthetics.payload.timings.ssl'; +export const SYNTHETICS_BLOCKED_TIMINGS = 'synthetics.payload.timings.blocked'; +export const SYNTHETICS_CONNECT_TIMINGS = 'synthetics.payload.timings.connect'; +export const SYNTHETICS_RECEIVE_TIMINGS = 'synthetics.payload.timings.receive'; +export const SYNTHETICS_SEND_TIMINGS = 'synthetics.payload.timings.send'; +export const SYNTHETICS_WAIT_TIMINGS = 'synthetics.payload.timings.wait'; +export const SYNTHETICS_TOTAL_TIMINGS = 'synthetics.payload.timings.total'; + +export const NETWORK_TIMINGS_FIELDS = [ + SYNTHETICS_DNS_TIMINGS, + SYNTHETICS_SSL_TIMINGS, + SYNTHETICS_BLOCKED_TIMINGS, + SYNTHETICS_CONNECT_TIMINGS, + SYNTHETICS_RECEIVE_TIMINGS, + SYNTHETICS_SEND_TIMINGS, + SYNTHETICS_WAIT_TIMINGS, + SYNTHETICS_TOTAL_TIMINGS, +]; diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 6fa68a617eead..3c64645f9b1e8 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -52,3 +52,16 @@ export const casesPath = '/cases'; export const uptimeOverviewLocatorID = 'UPTIME_OVERVIEW_LOCATOR'; export const syntheticsMonitorDetailLocatorID = 'SYNTHETICS_MONITOR_DETAIL_LOCATOR'; export const syntheticsEditMonitorLocatorID = 'SYNTHETICS_EDIT_MONITOR_LOCATOR'; + +export { + NETWORK_TIMINGS_FIELDS, + SYNTHETICS_BLOCKED_TIMINGS, + SYNTHETICS_CONNECT_TIMINGS, + SYNTHETICS_DNS_TIMINGS, + SYNTHETICS_RECEIVE_TIMINGS, + SYNTHETICS_SEND_TIMINGS, + SYNTHETICS_SSL_TIMINGS, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_TOTAL_TIMINGS, + SYNTHETICS_WAIT_TIMINGS, +} from './field_names/synthetics'; diff --git a/x-pack/plugins/observability/e2e/journeys/exploratory_view.ts b/x-pack/plugins/observability/e2e/journeys/exploratory_view.ts index cd65988e76faa..ccb75f5fdb538 100644 --- a/x-pack/plugins/observability/e2e/journeys/exploratory_view.ts +++ b/x-pack/plugins/observability/e2e/journeys/exploratory_view.ts @@ -49,7 +49,7 @@ journey('Exploratory view', async ({ page, params }) => { await page.click('[aria-label="Toggle series information"] >> text=Page views', TIMEOUT_60_SEC); await page.click('[aria-label="Edit series"]', TIMEOUT_60_SEC); await page.click('button:has-text("No breakdown")'); - await page.click('button[role="option"]:has-text("Operating system")'); + await page.click('button[role="option"]:has-text("Operating system")', TIMEOUT_60_SEC); await page.click('button:has-text("Apply changes")'); await page.click('text=Chrome OS'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index cb258ccf46f9f..8b6343119f121 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -14,13 +14,13 @@ import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string | string[]; + value: string | Array; seriesId: number; series: SeriesUrl; negate: boolean; definitionFilter?: boolean; dataView: DataView; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + removeFilter: (field: string, value: string | Array, notVal: boolean) => void; } export function FilterLabel({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index c50332487f184..9f9664602a672 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -6,6 +6,7 @@ */ import { OperationType } from '@kbn/lens-plugin/public'; import { DOCUMENT_FIELD_NAME } from '@kbn/lens-plugin/common/constants'; +import { i18n } from '@kbn/i18n'; import { ReportViewType } from '../../types'; import { CLS_FIELD, @@ -61,13 +62,21 @@ import { } from './labels'; import { MONITOR_DURATION_US, + SYNTHETICS_BLOCKED_TIMINGS, SYNTHETICS_CLS, + SYNTHETICS_CONNECT_TIMINGS, SYNTHETICS_DCL, + SYNTHETICS_DNS_TIMINGS, SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_RECEIVE_TIMINGS, + SYNTHETICS_SEND_TIMINGS, + SYNTHETICS_SSL_TIMINGS, SYNTHETICS_STEP_DURATION, SYNTHETICS_STEP_NAME, + SYNTHETICS_TOTAL_TIMINGS, + SYNTHETICS_WAIT_TIMINGS, } from './field_names/synthetics'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -103,6 +112,30 @@ export const FieldLabels: Record = { [SYNTHETICS_DOCUMENT_ONLOAD]: PAGE_LOAD_TIME_LABEL, [TRANSACTION_TIME_TO_FIRST_BYTE]: BACKEND_TIME_LABEL, [TRANSACTION_DURATION]: PAGE_LOAD_TIME_LABEL, + [SYNTHETICS_CONNECT_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.connect', { + defaultMessage: 'Connect', + }), + [SYNTHETICS_DNS_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.dns', { + defaultMessage: 'DNS', + }), + [SYNTHETICS_WAIT_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.wait', { + defaultMessage: 'Wait', + }), + [SYNTHETICS_SSL_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.ssl', { + defaultMessage: 'SSL', + }), + [SYNTHETICS_BLOCKED_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.blocked', { + defaultMessage: 'Blocked', + }), + [SYNTHETICS_SEND_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.send', { + defaultMessage: 'Send', + }), + [SYNTHETICS_RECEIVE_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.receive', { + defaultMessage: 'Receive', + }), + [SYNTHETICS_TOTAL_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.total', { + defaultMessage: 'Total', + }), 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts index 0f28648552728..003be106ffaaa 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts @@ -13,3 +13,23 @@ export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us'; export const SYNTHETICS_DCL = 'browser.experience.dcl.us'; export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword'; export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; + +export const SYNTHETICS_DNS_TIMINGS = 'synthetics.payload.timings.dns'; +export const SYNTHETICS_SSL_TIMINGS = 'synthetics.payload.timings.ssl'; +export const SYNTHETICS_BLOCKED_TIMINGS = 'synthetics.payload.timings.blocked'; +export const SYNTHETICS_CONNECT_TIMINGS = 'synthetics.payload.timings.connect'; +export const SYNTHETICS_RECEIVE_TIMINGS = 'synthetics.payload.timings.receive'; +export const SYNTHETICS_SEND_TIMINGS = 'synthetics.payload.timings.send'; +export const SYNTHETICS_WAIT_TIMINGS = 'synthetics.payload.timings.wait'; +export const SYNTHETICS_TOTAL_TIMINGS = 'synthetics.payload.timings.total'; + +export const NETWORK_TIMINGS_FIELDS = [ + SYNTHETICS_DNS_TIMINGS, + SYNTHETICS_SSL_TIMINGS, + SYNTHETICS_BLOCKED_TIMINGS, + SYNTHETICS_CONNECT_TIMINGS, + SYNTHETICS_RECEIVE_TIMINGS, + SYNTHETICS_SEND_TIMINGS, + SYNTHETICS_WAIT_TIMINGS, + SYNTHETICS_TOTAL_TIMINGS, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index 432d8dfae75f2..83c7781f692a7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -86,6 +86,13 @@ export const CLS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels defaultMessage: 'Cumulative layout shift', }); +export const NETWORK_TIMINGS_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.networkTimings', + { + defaultMessage: 'Network timings', + } +); + export const DCL_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.dcl', { defaultMessage: 'DOM content loaded', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index a21684bd0d08c..b031bff08b0f8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -131,7 +131,7 @@ describe('Lens Attribute', () => { sourceField: '@timestamp', }, ...PERCENTILE_RANKS.reduce((acc: Record, rank, index) => { - acc[`y-axis-column-${index === 0 ? 'layer' + index : index}`] = { + acc[`y-axis-column-${index === 0 ? 'layer' + index + '-0' : index}`] = { customLabel: true, dataType: 'number', filter: { @@ -153,24 +153,26 @@ describe('Lens Attribute', () => { }); it('should return main y axis', function () { - expect(lnsAttr.getMainYAxis(layerConfig, 'layer0', '')).toEqual({ - customLabel: true, - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'formula', - params: { - format: { - id: 'percent', - params: { - decimals: 0, + expect(lnsAttr.getMainYAxis(layerConfig, 'layer0', '')).toEqual([ + { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'formula', + params: { + format: { + id: 'percent', + params: { + decimals: 0, + }, }, + formula: 'count() / overall_sum(count())', + isFormulaBroken: false, }, - formula: 'count() / overall_sum(count())', - isFormulaBroken: false, + references: ['y-axis-column-layer0X3'], }, - references: ['y-axis-column-layer0X3'], - }); + ]); }); it('should return expected field type', function () { @@ -363,13 +365,13 @@ describe('Lens Attribute', () => { gridlinesVisibilitySettings: { x: false, yLeft: true, yRight: true }, layers: [ { - accessors: ['y-axis-column-layer0'], + accessors: ['y-axis-column-layer0-0'], layerId: 'layer0', layerType: 'data', palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0', axisMode: 'left' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0-0', axisMode: 'left' }], }, { accessors: [ @@ -468,14 +470,14 @@ describe('Lens Attribute', () => { expect(lnsAttr.visualization?.layers).toEqual([ { - accessors: ['y-axis-column-layer0'], + accessors: ['y-axis-column-layer0-0'], layerId: 'layer0', layerType: 'data', palette: undefined, seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0', axisMode: 'left' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0-0', axisMode: 'left' }], }, ]); @@ -483,7 +485,7 @@ describe('Lens Attribute', () => { columnOrder: [ 'breakdown-column-layer0', 'x-axis-column-layer0', - 'y-axis-column-layer0', + 'y-axis-column-layer0-0', 'y-axis-column-layer0X0', 'y-axis-column-layer0X1', 'y-axis-column-layer0X2', @@ -534,7 +536,7 @@ describe('Lens Attribute', () => { scale: 'interval', sourceField: LCP_FIELD, }, - 'y-axis-column-layer0': { + 'y-axis-column-layer0-0': { customLabel: true, dataType: 'number', filter: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 1aece50b812ef..8e39ff3bdd2c6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -16,11 +16,15 @@ import { DateHistogramIndexPatternColumn, FieldBasedIndexPatternColumn, FiltersIndexPatternColumn, + FormulaIndexPatternColumn, + FormulaPublicApi, + LastValueIndexPatternColumn, + MaxIndexPatternColumn, MedianIndexPatternColumn, + MinIndexPatternColumn, OperationMetadata, OperationType, PercentileIndexPatternColumn, - LastValueIndexPatternColumn, PersistedIndexPatternLayer, RangeIndexPatternColumn, SeriesType, @@ -30,25 +34,21 @@ import { XYCurveType, XYState, YAxisMode, - MinIndexPatternColumn, - MaxIndexPatternColumn, - FormulaPublicApi, - FormulaIndexPatternColumn, } from '@kbn/lens-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { PersistableFilter } from '@kbn/lens-plugin/common'; import { urlFiltersToKueryString } from '../utils/stringify_kueries'; import { FILTER_RECORDS, + FORMULA_COLUMN, + PERCENTILE, + PERCENTILE_RANKS, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, REPORT_METRIC_FIELD, + ReportTypes, TERMS_COLUMN, USE_BREAK_DOWN_COLUMN, - PERCENTILE, - PERCENTILE_RANKS, - ReportTypes, - FORMULA_COLUMN, } from './constants'; import { ColumnFilter, @@ -84,14 +84,37 @@ export function getPercentileParam(operationType: string) { export const parseCustomFieldName = ( seriesConfig: SeriesConfig, selectedMetricField?: string -): Partial & { fieldName: string; columnLabel?: string; columnField?: string } => { +): + | (Partial & { fieldName: string; columnLabel?: string; columnField?: string }) + | MetricOption[] => { const metricOptions = seriesConfig.metricOptions ?? []; if (selectedMetricField) { if (metricOptions) { - const currField = metricOptions.find( - ({ field, id }) => field === selectedMetricField || id === selectedMetricField - ); + const currField = metricOptions.find((opt) => { + if ('items' in opt) { + return opt.id === selectedMetricField; + } else { + return opt.field === selectedMetricField || opt.id === selectedMetricField; + } + }); + + if (currField && 'items' in currField) { + const currFieldItem = currField.items.find( + (item) => item.id === selectedMetricField || item.field === selectedMetricField + ); + + if (currFieldItem) { + return { + ...(currFieldItem ?? {}), + fieldName: selectedMetricField, + columnLabel: currFieldItem?.label, + columnField: currFieldItem?.field, + }; + } + + return currField.items; + } return { ...(currField ?? {}), @@ -107,6 +130,8 @@ export const parseCustomFieldName = ( }; }; +type MainYAxisColType = ReturnType; + export interface LayerConfig { filters?: UrlFilter[]; seriesConfig: SeriesConfig; @@ -216,7 +241,7 @@ export class LensAttributes { params: { orderBy: isFormulaColumn ? { type: 'custom' } - : { type: 'column', columnId: `y-axis-column-${layerId}` }, + : { type: 'column', columnId: `y-axis-column-${layerId}-0` }, size: 10, orderDirection: 'desc', otherBucket: true, @@ -283,6 +308,7 @@ export class LensAttributes { columnType, columnFilter, operationType, + shortLabel, }: { sourceField: string; columnType?: string; @@ -290,6 +316,7 @@ export class LensAttributes { operationType?: SupportedOperations | 'last_value'; label?: string; seriesConfig: SeriesConfig; + shortLabel?: boolean; }) { if (columnType === 'operation' || operationType) { if ( @@ -302,6 +329,7 @@ export class LensAttributes { label, seriesConfig, columnFilter, + shortLabel, }); } if (operationType === 'last_value') { @@ -336,13 +364,7 @@ export class LensAttributes { return { ...buildNumberColumn(sourceField), operationType, - label: i18n.translate('xpack.observability.expView.columns.operation.label', { - defaultMessage: '{operationType} of {sourceField}', - values: { - sourceField: label || seriesConfig.labels[sourceField], - operationType: capitalize(operationType), - }, - }), + label: label || seriesConfig.labels[sourceField], filter: columnFilter, params: { sortField: '@timestamp', @@ -357,12 +379,14 @@ export class LensAttributes { seriesConfig, operationType, columnFilter, + shortLabel, }: { sourceField: string; operationType: SupportedOperations; label?: string; seriesConfig: SeriesConfig; columnFilter?: ColumnFilter; + shortLabel?: boolean; }): | MinIndexPatternColumn | MaxIndexPatternColumn @@ -373,7 +397,7 @@ export class LensAttributes { return { ...buildNumberColumn(sourceField), label: - operationType === 'unique_count' + operationType === 'unique_count' || shortLabel ? label || seriesConfig.labels[sourceField] : i18n.translate('xpack.observability.expView.columns.operation.label', { defaultMessage: '{operationType} of {sourceField}', @@ -472,7 +496,7 @@ export class LensAttributes { const { xAxisColumn } = layerConfig.seriesConfig; if (!xAxisColumn.sourceField) { - return xAxisColumn as LastValueIndexPatternColumn; + return [xAxisColumn as LastValueIndexPatternColumn]; } if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { @@ -507,15 +531,21 @@ export class LensAttributes { operationType, colIndex, layerId, + metricOption, + shortLabel, }: { sourceField: string; + metricOption?: MetricOption; operationType?: SupportedOperations; label?: string; layerId: string; layerConfig: LayerConfig; colIndex?: number; + shortLabel?: boolean; }) { const { breakdown, seriesConfig } = layerConfig; + const fieldMetaInfo = this.getFieldMeta(sourceField, layerConfig, metricOption); + const { formula, fieldMeta, @@ -525,7 +555,7 @@ export class LensAttributes { timeScale, columnFilters, showPercentileAnnotations, - } = this.getFieldMeta(sourceField, layerConfig); + } = fieldMetaInfo; if (columnType === FORMULA_COLUMN) { return getDistributionInPercentageColumn({ @@ -578,6 +608,7 @@ export class LensAttributes { operationType, label: columnLabel || label, seriesConfig: layerConfig.seriesConfig, + shortLabel, }); } if (operationType === 'unique_count' || fieldType === 'string') { @@ -604,8 +635,24 @@ export class LensAttributes { return parseCustomFieldName(layerConfig.seriesConfig, sourceField); } - getFieldMeta(sourceField: string, layerConfig: LayerConfig) { + getFieldMeta(sourceField: string, layerConfig: LayerConfig, metricOpt?: MetricOption) { if (sourceField === REPORT_METRIC_FIELD) { + const metricOption = metricOpt + ? { + ...metricOpt, + columnLabel: metricOpt.label, + columnField: metricOpt.field, + fieldName: metricOpt.field!, + } + : parseCustomFieldName(layerConfig.seriesConfig, layerConfig.selectedMetricField); + + if (Array.isArray(metricOption)) { + return { + fieldName: sourceField, + items: metricOption, + }; + } + const { palette, fieldName, @@ -616,7 +663,7 @@ export class LensAttributes { paramFilters, showPercentileAnnotations, formula, - } = parseCustomFieldName(layerConfig.seriesConfig, layerConfig.selectedMetricField); + } = metricOption; const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName!); return { formula, @@ -644,29 +691,51 @@ export class LensAttributes { layerConfig.seriesConfig.yAxisColumns[0]; if (sourceField === RECORDS_PERCENTAGE_FIELD) { - return getDistributionInPercentageColumn({ - label, - layerId, - columnFilter, - dataView: layerConfig.indexPattern, - lensFormulaHelper: this.lensFormulaHelper!, - }).main; + return [ + getDistributionInPercentageColumn({ + label, + layerId, + columnFilter, + dataView: layerConfig.indexPattern, + lensFormulaHelper: this.lensFormulaHelper!, + }).main, + ]; } if (sourceField === RECORDS_FIELD || !sourceField) { - return this.getRecordsColumn(label, undefined, timeScale); + return [this.getRecordsColumn(label, undefined, timeScale)]; } - return this.getColumnBasedOnType({ - sourceField, - label, - layerConfig, - colIndex: 0, - operationType: (breakdown === PERCENTILE - ? PERCENTILE_RANKS[0] - : operationType) as SupportedOperations, - layerId, - }); + const fieldMetaInfo = this.getFieldMeta(sourceField, layerConfig); + + if ('items' in fieldMetaInfo) { + const { items } = fieldMetaInfo; + + return items?.map((item, index) => { + return this.getColumnBasedOnType({ + layerConfig, + layerId, + shortLabel: true, + label: item.label, + sourceField: REPORT_METRIC_FIELD, + metricOption: item, + operationType: operationType as SupportedOperations, + }); + }); + } + + return [ + this.getColumnBasedOnType({ + sourceField, + label, + layerConfig, + colIndex: 0, + operationType: (breakdown === PERCENTILE + ? PERCENTILE_RANKS[0] + : operationType) as SupportedOperations, + layerId, + }), + ]; } getChildYAxises( @@ -859,60 +928,102 @@ export class LensAttributes { const layerId = `layer${index}`; const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); - const mainYAxis = this.getMainYAxis(layerConfig, layerId, columnFilter); + const mainYAxises = this.getMainYAxis(layerConfig, layerId, columnFilter); const { sourceField } = seriesConfig.xAxisColumn; - const label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; + const hasBreakdownColumn = + // do nothing since this will be used a x axis source + Boolean(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE); + layers[layerId] = this.getDataLayer({ + layerId, + layerConfig, + mainYAxises, + columnFilter, + timeShift, + hasBreakdownColumn, + }); + }); + + Object.entries(this.seriesReferenceLines).forEach(([id, { layerData }]) => { + layers[id] = layerData; + }); + + return layers; + } + + getDataLayer({ + hasBreakdownColumn, + layerId, + layerConfig, + columnFilter, + mainYAxises, + timeShift, + }: { + hasBreakdownColumn: boolean; + layerId: string; + timeShift: string | null; + layerConfig: LayerConfig; + columnFilter: string; + mainYAxises: MainYAxisColType; + }) { + const allYAxisColumns: Record = {}; + + mainYAxises?.forEach((mainYAxis, index) => { let filterQuery = columnFilter || mainYAxis.filter?.query; if (columnFilter && mainYAxis.filter?.query) { filterQuery = `${columnFilter} and ${mainYAxis.filter.query}`; } - layers[layerId] = { - columnOrder: [ - ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE - ? [`breakdown-column-${layerId}`] - : []), - `x-axis-column-${layerId}`, - `y-axis-column-${layerId}`, - ...Object.keys(this.getChildYAxises(layerConfig, layerId, columnFilter)), - ], - columns: { - [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), - [`y-axis-column-${layerId}`]: { - ...mainYAxis, - label, - filter: { - query: filterQuery ?? '', - language: 'kuery', - }, - ...(timeShift ? { timeShift } : {}), - }, - ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE - ? // do nothing since this will be used a x axis source - { - [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ - layerId, - sourceField: breakdown, - indexPattern: layerConfig.indexPattern, - labels: layerConfig.seriesConfig.labels, - layerConfig, - }), - } - : {}), - ...this.getChildYAxises(layerConfig, layerId, columnFilter), + const label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; + + allYAxisColumns[`y-axis-column-${layerId}-${index}`] = { + ...mainYAxis, + label, + filter: { + query: filterQuery ?? '', + language: 'kuery', }, - incompleteColumns: {}, + ...(timeShift ? { timeShift } : {}), }; }); - Object.entries(this.seriesReferenceLines).forEach(([id, { layerData }]) => { - layers[id] = layerData; - }); + const { breakdown } = layerConfig; - return layers; + const breakDownColumn = hasBreakdownColumn + ? this.getBreakdownColumn({ + layerId, + sourceField: breakdown!, + indexPattern: layerConfig.indexPattern, + labels: layerConfig.seriesConfig.labels, + layerConfig, + }) + : null; + + const xAxises = { + [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), + }; + + return { + columnOrder: [ + ...(hasBreakdownColumn ? [`breakdown-column-${layerId}`] : []), + ...Object.keys(xAxises), + ...Object.keys(allYAxisColumns), + ...Object.keys(this.getChildYAxises(layerConfig, layerId, columnFilter)), + ], + columns: { + ...xAxises, + ...allYAxisColumns, + ...(hasBreakdownColumn + ? { + [`breakdown-column-${layerId}`]: breakDownColumn!, + } + : {}), + ...this.getChildYAxises(layerConfig, layerId, columnFilter), + }, + incompleteColumns: {}, + }; } getXyState(): XYState { @@ -949,9 +1060,15 @@ export class LensAttributes { } } + const layerId = `layer${index}`; + + const columnFilter = this.getLayerFilters(layerConfig, this.layerConfigs.length); + + const mainYAxises = this.getMainYAxis(layerConfig, layerId, columnFilter) ?? []; + return { accessors: [ - `y-axis-column-layer${index}`, + ...mainYAxises.map((key, yIndex) => `y-axis-column-${layerId}-${yIndex}`), ...Object.keys(this.getChildYAxises(layerConfig, `layer${index}`, undefined, true)), ], layerId: `layer${index}`, @@ -960,7 +1077,7 @@ export class LensAttributes { palette: palette ?? layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ { - forAccessor: `y-axis-column-layer${index}`, + forAccessor: `y-axis-column-layer${index}-0`, color: layerConfig.color, /* if the fields format matches the field format of the first layer, use the default y axis (right) * if not, use the secondary y axis (left) */ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/sample_formula_metric_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/sample_formula_metric_attribute.ts index c70d0ee031451..6894838d705e8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/sample_formula_metric_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/sample_formula_metric_attribute.ts @@ -34,6 +34,10 @@ export const sampleMetricFormulaAttribute = { 'layer-0-column-1': { customLabel: true, dataType: 'number', + filter: { + language: 'kuery', + query: 'summary.up: *', + }, isBucketed: false, label: 'Availability', operationType: 'formula', @@ -54,7 +58,7 @@ export const sampleMetricFormulaAttribute = { dataType: 'number', filter: { language: 'kuery', - query: 'summary.down > 0', + query: '(summary.up: *) AND (summary.down > 0)', }, isBucketed: false, label: 'Part of Availability', @@ -68,6 +72,10 @@ export const sampleMetricFormulaAttribute = { 'layer-0-column-1X1': { customLabel: true, dataType: 'number', + filter: { + language: 'kuery', + query: 'summary.up: *', + }, isBucketed: false, label: 'Part of Availability', operationType: 'count', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/single_metric_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/single_metric_attributes.ts index f674ab1f9914f..9a1f5498dc17e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/single_metric_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes/single_metric_attributes.ts @@ -14,6 +14,7 @@ import { import type { DataView } from '@kbn/data-views-plugin/common'; +import { Query } from '@kbn/es-query'; import { FORMULA_COLUMN } from '../constants'; import { ColumnFilter, MetricOption } from '../../types'; import { SeriesConfig } from '../../../../..'; @@ -43,63 +44,76 @@ export class SingleMetricLensAttributes extends LensAttributes { this.columnId = 'layer-0-column-1'; this.globalFilter = this.getGlobalFilter(this.isMultiSeries); - this.layers = this.getSingleMetricLayer(); + this.layers = this.getSingleMetricLayer()!; } getSingleMetricLayer() { const { seriesConfig, selectedMetricField, operationType, indexPattern } = this.layerConfigs[0]; - const { - columnFilter, - columnField, - columnLabel, - columnType, - formula, - metricStateOptions, - format, - } = parseCustomFieldName(seriesConfig, selectedMetricField); + const metricOption = parseCustomFieldName(seriesConfig, selectedMetricField); - this.metricStateOptions = metricStateOptions; - - if (columnType === FORMULA_COLUMN && formula) { - return this.getFormulaLayer({ formula, label: columnLabel, dataView: indexPattern, format }); - } - - const getSourceField = () => { - if (selectedMetricField.startsWith('Records') || selectedMetricField.startsWith('records')) { - return 'Records'; + if (!Array.isArray(metricOption)) { + const { + columnFilter, + columnField, + columnLabel, + columnType, + formula, + metricStateOptions, + format, + } = metricOption; + + this.metricStateOptions = metricStateOptions; + + if (columnType === FORMULA_COLUMN && formula) { + return this.getFormulaLayer({ + formula, + label: columnLabel, + dataView: indexPattern, + format, + filter: columnFilter, + }); } - return columnField || selectedMetricField; - }; - - const sourceField = getSourceField(); - - const isPercentileColumn = operationType?.includes('th'); - if (isPercentileColumn) { - return this.getPercentileLayer({ - sourceField, - operationType, - seriesConfig, - columnLabel, - columnFilter, - }); - } + const getSourceField = () => { + if ( + selectedMetricField.startsWith('Records') || + selectedMetricField.startsWith('records') + ) { + return 'Records'; + } + return columnField || selectedMetricField; + }; + + const sourceField = getSourceField(); + + const isPercentileColumn = operationType?.includes('th'); + + if (isPercentileColumn) { + return this.getPercentileLayer({ + sourceField, + operationType, + seriesConfig, + columnLabel, + columnFilter, + }); + } - return { - layer0: { - columns: { - [this.columnId]: { - ...buildNumberColumn(sourceField), - label: columnLabel ?? '', - operationType: sourceField === 'Records' ? 'count' : operationType || 'median', - filter: columnFilter, + return { + layer0: { + columns: { + [this.columnId]: { + ...buildNumberColumn(sourceField), + label: columnLabel ?? '', + operationType: sourceField === 'Records' ? 'count' : operationType || 'median', + filter: columnFilter, + }, }, + columnOrder: [this.columnId], + incompleteColumns: {}, }, - columnOrder: [this.columnId], - incompleteColumns: {}, - }, - }; + }; + } } getFormulaLayer({ @@ -107,10 +121,12 @@ export class SingleMetricLensAttributes extends LensAttributes { label, dataView, format, + filter, }: { formula: string; label?: string; format?: string; + filter?: Query; dataView: DataView; }) { const layer = this.lensFormulaHelper?.insertOrReplaceFormulaColumn( @@ -118,6 +134,7 @@ export class SingleMetricLensAttributes extends LensAttributes { { formula, label, + filter, format: format === 'percent' || !format ? { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts index f3e3fe0817845..9744a08c7ced9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts @@ -14,6 +14,14 @@ import { SYNTHETICS_STEP_DURATION, } from '../constants/field_names/synthetics'; +export const MS_TO_HUMANIZE_PRECISE = { + inputFormat: 'milliseconds' as const, + outputFormat: 'humanizePrecise' as const, + outputPrecision: 1, + showSuffix: true, + useShortSuffix: true, +}; + export const syntheticsFieldFormats: FieldFormat[] = [ { field: 'monitor.duration.us', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 76b9a6c2ade41..4fd95e30230b1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -24,9 +24,11 @@ import { STEP_DURATION_LABEL, UP_LABEL, PAGE_LOAD_TIME_LABEL, + NETWORK_TIMINGS_LABEL, } from '../constants/labels'; import { MONITOR_DURATION_US, + NETWORK_TIMINGS_FIELDS, SYNTHETICS_CLS, SYNTHETICS_DCL, SYNTHETICS_DOCUMENT_ONLOAD, @@ -66,7 +68,7 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig operationType: 'median', }, ], - hasOperationType: false, + hasOperationType: true, filterFields: ['observer.geo.name', 'monitor.type', 'tags', 'url.full'], breakdownFields: [ 'observer.geo.name', @@ -163,16 +165,31 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig columnType: OPERATION_COLUMN, columnFilters: getStepMetricColumnFilter(SYNTHETICS_CLS), }, + { + label: NETWORK_TIMINGS_LABEL, + id: 'network_timings', + columnType: OPERATION_COLUMN, + items: NETWORK_TIMINGS_FIELDS.map((field) => ({ + label: FieldLabels[field] ?? field, + field, + id: field, + columnType: OPERATION_COLUMN, + columnFilters: getStepMetricColumnFilter(field, 'journey/network_info'), + })), + }, ], labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL }, }; } -const getStepMetricColumnFilter = (field: string): ColumnFilter[] => { +const getStepMetricColumnFilter = ( + field: string, + stepType: 'step/metrics' | 'step/end' | 'journey/network_info' = 'step/metrics' +): ColumnFilter[] => { return [ { language: 'kuery', - query: `synthetics.type: step/metrics and ${field}: *`, + query: `synthetics.type: ${stepType} and ${field}: * and ${field} > 0`, }, ]; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/runtime_fields.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/runtime_fields.ts new file mode 100644 index 0000000000000..16be09ed474bd --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/runtime_fields.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuntimeField } from '@kbn/data-views-plugin/public'; +import { MS_TO_HUMANIZE_PRECISE } from './field_formats'; +import { + SYNTHETICS_DNS_TIMINGS, + SYNTHETICS_BLOCKED_TIMINGS, + SYNTHETICS_CONNECT_TIMINGS, + SYNTHETICS_TOTAL_TIMINGS, + SYNTHETICS_RECEIVE_TIMINGS, + SYNTHETICS_SEND_TIMINGS, + SYNTHETICS_WAIT_TIMINGS, + SYNTHETICS_SSL_TIMINGS, +} from '../constants/field_names/synthetics'; + +const LONG_FIELD = { + type: 'long' as const, + format: { + id: 'duration', + params: MS_TO_HUMANIZE_PRECISE, + }, +}; + +export const syntheticsRuntimeFields: Array<{ name: string; field: RuntimeField }> = [ + { + name: SYNTHETICS_DNS_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_BLOCKED_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_CONNECT_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_TOTAL_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_RECEIVE_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_SEND_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_WAIT_TIMINGS, + field: LONG_FIELD, + }, + { + name: SYNTHETICS_SSL_TIMINGS, + field: LONG_FIELD, + }, +]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts index b9efbd865b3ed..c67857ae7c0a4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts @@ -6,7 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import { SYNTHETICS_STEP_NAME } from '../constants/field_names/synthetics'; +import { + SYNTHETICS_STEP_DURATION, + SYNTHETICS_STEP_NAME, +} from '../constants/field_names/synthetics'; import { ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, FORMULA_COLUMN } from '../constants'; import { buildExistsFilter } from '../utils'; @@ -29,7 +32,7 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri { field: 'url.full', filters: buildExistsFilter('summary.up', dataView) }, ], reportType: 'single-metric', - baseFilters: [...buildExistsFilter('summary.up', dataView)], + baseFilters: [], metricOptions: [ { id: 'monitor_availability', @@ -65,6 +68,7 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri }, titlePosition: 'bottom', }, + columnFilter: { language: 'kuery', query: 'summary.up: *' }, }, { id: 'monitor_duration', @@ -75,6 +79,17 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri metricStateOptions: { titlePosition: 'bottom', }, + columnFilter: { language: 'kuery', query: 'summary.up: *' }, + }, + { + id: 'step_duration', + field: SYNTHETICS_STEP_DURATION, + label: i18n.translate('xpack.observability.expView.stepDuration', { + defaultMessage: 'Total step duration', + }), + metricStateOptions: { + titlePosition: 'bottom', + }, }, { id: 'monitor_errors', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts index 1c296f9cdc1fd..85fd0c9f601b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/mobile_test_attribute.ts @@ -26,7 +26,7 @@ export const testMobileKPIAttr = { formBased: { layers: { layer0: { - columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0-0'], columns: { 'x-axis-column-layer0': { sourceField: '@timestamp', @@ -37,7 +37,7 @@ export const testMobileKPIAttr = { params: { interval: 'auto' }, scale: 'interval', }, - 'y-axis-column-layer0': { + 'y-axis-column-layer0-0': { isBucketed: false, label: 'Median of System memory usage', operationType: 'median', @@ -68,12 +68,12 @@ export const testMobileKPIAttr = { preferredSeriesType: 'line', layers: [ { - accessors: ['y-axis-column-layer0'], + accessors: ['y-axis-column-layer0-0'], layerId: 'layer0', layerType: 'data', palette: undefined, seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column-layer0', color: 'green', axisMode: 'left' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0-0', color: 'green', axisMode: 'left' }], xAccessor: 'x-axis-column-layer0', }, ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 5302078d372ce..6c6424a0362d1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -32,7 +32,7 @@ export const sampleAttribute = { layer0: { columnOrder: [ 'x-axis-column-layer0', - 'y-axis-column-layer0', + 'y-axis-column-layer0-0', 'y-axis-column-layer0X0', 'y-axis-column-layer0X1', 'y-axis-column-layer0X2', @@ -58,7 +58,7 @@ export const sampleAttribute = { scale: 'interval', sourceField: 'transaction.duration.us', }, - 'y-axis-column-layer0': { + 'y-axis-column-layer0-0': { customLabel: true, dataType: 'number', filter: { @@ -250,7 +250,7 @@ export const sampleAttribute = { }, layers: [ { - accessors: ['y-axis-column-layer0'], + accessors: ['y-axis-column-layer0-0'], layerId: 'layer0', layerType: 'data', palette: undefined, @@ -259,7 +259,7 @@ export const sampleAttribute = { yConfig: [ { color: 'green', - forAccessor: 'y-axis-column-layer0', + forAccessor: 'y-axis-column-layer0-0', axisMode: 'left', }, ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 108112e43ae35..1cf945c4456a5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -27,7 +27,7 @@ export const sampleAttributeCoreWebVital = { layer0: { columnOrder: [ 'x-axis-column-layer0', - 'y-axis-column-layer0', + 'y-axis-column-layer0-0', 'y-axis-column-1', 'y-axis-column-2', ], @@ -40,7 +40,7 @@ export const sampleAttributeCoreWebVital = { params: { missingBucket: false, orderBy: { - columnId: 'y-axis-column-layer0', + columnId: 'y-axis-column-layer0-0', type: 'column', }, orderDirection: 'desc', @@ -75,7 +75,7 @@ export const sampleAttributeCoreWebVital = { scale: 'ratio', sourceField: RECORDS_FIELD, }, - 'y-axis-column-layer0': { + 'y-axis-column-layer0-0': { dataType: 'number', filter: { language: 'kuery', @@ -115,7 +115,7 @@ export const sampleAttributeCoreWebVital = { }, layers: [ { - accessors: ['y-axis-column-layer0', 'y-axis-column-1', 'y-axis-column-2'], + accessors: ['y-axis-column-layer0-0', 'y-axis-column-1', 'y-axis-column-2'], layerId: 'layer0', layerType: 'data', palette: undefined, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index c1bd53c85b760..280438737b5da 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -25,7 +25,7 @@ export const sampleAttributeKpi = { formBased: { layers: { layer0: { - columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0-0'], columns: { 'x-axis-column-layer0': { dataType: 'date', @@ -38,7 +38,7 @@ export const sampleAttributeKpi = { scale: 'interval', sourceField: '@timestamp', }, - 'y-axis-column-layer0': { + 'y-axis-column-layer0-0': { dataType: 'number', filter: { language: 'kuery', @@ -76,7 +76,7 @@ export const sampleAttributeKpi = { }, layers: [ { - accessors: ['y-axis-column-layer0'], + accessors: ['y-axis-column-layer0-0'], layerId: 'layer0', layerType: 'data', palette: undefined, @@ -85,7 +85,7 @@ export const sampleAttributeKpi = { yConfig: [ { color: 'green', - forAccessor: 'y-axis-column-layer0', + forAccessor: 'y-axis-column-layer0-0', axisMode: 'left', }, ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_with_reference_lines.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_with_reference_lines.ts index 2cf6cdc8a6054..5d51b1c193401 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_with_reference_lines.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_with_reference_lines.ts @@ -32,7 +32,7 @@ export const sampleAttributeWithReferenceLines = { layer0: { columnOrder: [ 'x-axis-column-layer0', - 'y-axis-column-layer0', + 'y-axis-column-layer0-0', 'y-axis-column-layer0X0', 'y-axis-column-layer0X1', 'y-axis-column-layer0X2', @@ -58,7 +58,7 @@ export const sampleAttributeWithReferenceLines = { scale: 'interval', sourceField: 'transaction.duration.us', }, - 'y-axis-column-layer0': { + 'y-axis-column-layer0-0': { customLabel: true, dataType: 'number', filter: { @@ -250,7 +250,7 @@ export const sampleAttributeWithReferenceLines = { }, layers: [ { - accessors: ['y-axis-column-layer0'], + accessors: ['y-axis-column-layer0-0'], layerId: 'layer0', layerType: 'data', palette: undefined, @@ -260,7 +260,7 @@ export const sampleAttributeWithReferenceLines = { { axisMode: 'left', color: 'green', - forAccessor: 'y-axis-column-layer0', + forAccessor: 'y-axis-column-layer0-0', }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 2d1a8bba8e5ea..e5a41739f7dd3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -61,7 +61,11 @@ export function getQueryFilter(field: string, value: string[], dataView?: DataVi return []; } -export function buildPhrasesFilter(field: string, value: string[], dataView?: DataView) { +export function buildPhrasesFilter( + field: string, + value: Array, + dataView?: DataView +) { const fieldMeta = dataView?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta && dataView) { if (value.length === 1) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index 0cce7d17cf2fd..253db8a42dd7c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -11,7 +11,7 @@ import { SeriesUrl, UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string | string[]; + value: string | Array; negate?: boolean; wildcards?: string[]; isWildcard?: boolean; @@ -30,8 +30,8 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie notWildcards, }: { field: string; - values: string[]; - notValues: string[]; + values: Array; + notValues: Array; wildcards?: string[]; notWildcards?: string[]; }) => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 5ad043a6b3b96..4b4387d57acc6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -18,7 +18,7 @@ import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; field: string; - allSelectedValues?: string[]; + allSelectedValues?: Array; negate: boolean; nestedField?: string; seriesId: number; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx index 85f15b03c051b..9c910879a1716 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx @@ -20,9 +20,11 @@ import { Breakdowns } from './breakdown/breakdowns'; import { LabelsBreakdown } from './breakdown/label_breakdown'; function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + const metricOption = parseCustomFieldName(seriesConfig, selectedMetricField); - return columnType; + if (!Array.isArray(metricOption)) { + return metricOption?.columnType; + } } interface Props { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 11f6ad624fcbe..075feba5467ec 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -91,7 +91,10 @@ export interface SeriesConfig { } >; textDefinitionFields?: string[]; - metricOptions?: MetricOption[]; + metricOptions?: Array< + | MetricOption + | { id: string; field?: string; label: string; items: MetricOption[]; columnType?: string } + >; labels: Record; hasOperationType: boolean; palette?: PaletteOutput; @@ -124,8 +127,8 @@ export interface SeriesUrl { export interface UrlFilter { field: string; - values?: string[]; - notValues?: string[]; + values?: Array; + notValues?: Array; wildcards?: string[]; notWildcards?: string[]; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts index 0e044bc1e2a27..aee60118bc7e4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts @@ -20,7 +20,7 @@ const buildOrCondition = (values: string[]) => { return `(${values.join(' or ')})`; }; -function addSlashes(str: string) { +function addSlashes(str: string | number) { return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0'); } diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 5d6577dad8c9c..2d9b1aa43660b 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -20,7 +20,7 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string | string[]; + value: string | Array; negate: boolean; field: string; dataView: DataView; @@ -46,10 +46,14 @@ export function buildFilterLabel({ export interface FilterValueLabelProps { field: string; label: string; - value: string | string[]; + value: string | Array; negate: boolean; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; - invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; + removeFilter: (field: string, value: string | Array, notVal: boolean) => void; + invertFilter: (val: { + field: string; + value: string | Array; + negate: boolean; + }) => void; dataView: DataView; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts index 37661c696ea88..8285e1d66e285 100644 --- a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts +++ b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts @@ -12,6 +12,8 @@ import type { DataView, DataViewSpec, } from '@kbn/data-views-plugin/public'; +import { RuntimeField } from '@kbn/data-views-plugin/public'; +import { syntheticsRuntimeFields } from '../../components/shared/exploratory_view/configurations/synthetics/runtime_fields'; import { rumFieldFormats } from '../../components/shared/exploratory_view/configurations/rum/field_formats'; import { syntheticsFieldFormats } from '../../components/shared/exploratory_view/configurations/synthetics/field_formats'; import { @@ -32,8 +34,17 @@ const appFieldFormats: Record = { mobile: apmFieldFormats, }; +const appRuntimeFields: Record | null> = { + infra_logs: null, + infra_metrics: null, + ux: null, + apm: null, + synthetics: syntheticsRuntimeFields, + mobile: null, +}; + function getFieldFormatsForApp(app: AppDataType) { - return appFieldFormats[app]; + return { runtimeFields: appRuntimeFields[app], formats: appFieldFormats[app] }; } export const dataViewList: Record = { @@ -101,7 +112,7 @@ export class ObservabilityDataViews { } // we want to make sure field formats remain same async validateFieldFormats(app: AppDataType, dataView: DataView) { - const defaultFieldFormats = getFieldFormatsForApp(app); + const { formats: defaultFieldFormats, runtimeFields } = getFieldFormatsForApp(app); if (defaultFieldFormats && defaultFieldFormats.length > 0) { let isParamsDifferent = false; defaultFieldFormats.forEach(({ field, format }) => { @@ -115,7 +126,12 @@ export class ObservabilityDataViews { } } }); - if (isParamsDifferent) { + if (runtimeFields !== null) { + runtimeFields.forEach(({ name, field }) => { + dataView.addRuntimeField(name, field); + }); + } + if (isParamsDifferent || runtimeFields !== null) { await this.dataViews?.updateSavedObject(dataView); } } From 8cb51bda55747fc89a3845062e1ed491ad098285 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 18 Oct 2022 13:10:35 -0400 Subject: [PATCH 48/74] chore(slo): Remove context in aggregated data (#143318) --- .../apm_transaction_duration.test.ts.snap | 20 ------------------- .../apm_transaction_error_rate.test.ts.snap | 20 ------------------- .../apm_transaction_duration.ts | 20 ------------------- .../apm_transaction_error_rate.ts | 20 ------------------- 4 files changed, 80 deletions(-) diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index d9a9c5852192b..61fadab240ae7 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -57,26 +57,6 @@ Object { "field": "@timestamp", }, }, - "slo.context.service.environment": Object { - "terms": Object { - "field": "service.environment", - }, - }, - "slo.context.service.name": Object { - "terms": Object { - "field": "service.name", - }, - }, - "slo.context.transaction.name": Object { - "terms": Object { - "field": "transaction.name", - }, - }, - "slo.context.transaction.type": Object { - "terms": Object { - "field": "transaction.type", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index d59faee05b930..51a0ee31581a2 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -62,26 +62,6 @@ Object { "field": "@timestamp", }, }, - "slo.context.service.environment": Object { - "terms": Object { - "field": "service.environment", - }, - }, - "slo.context.service.name": Object { - "terms": Object { - "field": "service.name", - }, - }, - "slo.context.transaction.name": Object { - "terms": Object { - "field": "transaction.name", - }, - }, - "slo.context.transaction.type": Object { - "terms": Object { - "field": "transaction.type", - }, - }, "slo.id": Object { "terms": Object { "field": "slo.id", diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts index 061f020bab2f9..05ef2ac431a83 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts @@ -131,26 +131,6 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera calendar_interval: '1m' as AggregationsCalendarInterval, }, }, - 'slo.context.transaction.name': { - terms: { - field: 'transaction.name', - }, - }, - 'slo.context.transaction.type': { - terms: { - field: 'transaction.type', - }, - }, - 'slo.context.service.name': { - terms: { - field: 'service.name', - }, - }, - 'slo.context.service.environment': { - terms: { - field: 'service.environment', - }, - }, }; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts index e9a796d67e36f..3e69a4e371342 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -133,26 +133,6 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener calendar_interval: '1m' as AggregationsCalendarInterval, }, }, - 'slo.context.transaction.name': { - terms: { - field: 'transaction.name', - }, - }, - 'slo.context.transaction.type': { - terms: { - field: 'transaction.type', - }, - }, - 'slo.context.service.name': { - terms: { - field: 'service.name', - }, - }, - 'slo.context.service.environment': { - terms: { - field: 'service.environment', - }, - }, }; } From 253ed9c3980f24ccdf6ef688770fdc54a77fd4d8 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 18 Oct 2022 11:43:40 -0600 Subject: [PATCH 49/74] [ML] Explain Log Rate Spikes: fix chart showing as empty when filter matches field/value pair in hovered row (#142693) * fix chart showing as empty when filter matches field/value pair in hovered row * use both overall and split buckets to get timerange * always pass along split stats as they are up to date Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../document_count_chart.tsx | 43 +++++++++++-------- .../document_count_content.tsx | 7 ++- .../explain_log_rate_spikes_page.tsx | 6 +-- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx index ad317fbf222ec..4a51957bb934a 100644 --- a/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -127,9 +127,17 @@ export const DocumentCountChart: FC = ({ // TODO Let user choose between ZOOM and BRUSH mode. const [viewMode] = useState(VIEW_MODE.BRUSH); + const hasNoData = useMemo( + () => + (chartPoints === undefined || chartPoints.length < 1) && + (chartPointsSplit === undefined || + (Array.isArray(chartPointsSplit) && chartPointsSplit.length < 1)), + [chartPoints, chartPointsSplit] + ); + const adjustedChartPoints = useMemo(() => { - // Display empty chart when no data in range - if (chartPoints.length < 1) return [{ time: timeRangeEarliest, value: 0 }]; + // Display empty chart when no data in range and no split data to show + if (hasNoData) return [{ time: timeRangeEarliest, value: 0 }]; // If chart has only one bucket // it won't show up correctly unless we add an extra data point @@ -145,12 +153,11 @@ export const DocumentCountChart: FC = ({ const adjustedChartPointsSplit = useMemo(() => { // Display empty chart when no data in range - if (!Array.isArray(chartPointsSplit) || chartPointsSplit.length < 1) - return [{ time: timeRangeEarliest, value: 0 }]; + if (hasNoData) return [{ time: timeRangeEarliest, value: 0 }]; // If chart has only one bucket // it won't show up correctly unless we add an extra data point - if (chartPointsSplit.length === 1) { + if (Array.isArray(chartPointsSplit) && chartPointsSplit.length === 1) { return [ ...chartPointsSplit, { @@ -349,18 +356,20 @@ export const DocumentCountChart: FC = ({ timeAxisLayerCount={useLegacyTimeAxis ? 0 : 2} style={useLegacyTimeAxis ? {} : MULTILAYER_TIME_AXIS_STYLE} /> - - {chartPointsSplit && ( + {adjustedChartPoints?.length && ( + + )} + {adjustedChartPointsSplit?.length && ( = ({ }, [windowParameters]); const bucketTimestamps = Object.keys(documentCountStats?.buckets ?? {}).map((time) => +time); - const timeRangeEarliest = Math.min(...bucketTimestamps); - const timeRangeLatest = Math.max(...bucketTimestamps); + const splitBucketTimestamps = Object.keys(documentCountStatsSplit?.buckets ?? {}).map( + (time) => +time + ); + const timeRangeEarliest = Math.min(...[...bucketTimestamps, ...splitBucketTimestamps]); + const timeRangeLatest = Math.max(...[...bucketTimestamps, ...splitBucketTimestamps]); if ( documentCountStats === undefined || diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx index 63ccb20e4e6b0..6e0201af97a3c 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx @@ -237,11 +237,7 @@ export const ExplainLogRateSpikesPage: FC = ({ brushSelectionUpdateHandler={setWindowParameters} clearSelectionHandler={clearSelection} documentCountStats={documentCountStats} - documentCountStatsSplit={ - currentSelectedChangePoint || currentSelectedGroup - ? documentCountStatsCompare - : undefined - } + documentCountStatsSplit={documentCountStatsCompare} documentCountStatsSplitLabel={getDocumentCountStatsSplitLabel( currentSelectedChangePoint, currentSelectedGroup From 8741acef54639a96d5138c545c7a8fa85dd3abdb Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:58:22 -0700 Subject: [PATCH 50/74] [DOCS] Edits load balancing section of production docs (#143481) --- .../production-considerations/production.asciidoc | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 6396e0c98300c..84727e536cfe9 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -27,16 +27,11 @@ you can configure {kib} to use a list of {es} hosts. To serve multiple {kib} installations behind a load balancer, you must change the configuration. See {kibana-ref}/settings.html[Configuring {kib}] for details on each setting. -Settings unique across each {kib} instance: +These settings must be unique across each {kib} instance: [source,js] -------- -server.uuid +server.uuid // if not provided, this is autogenerated server.name --------- - -Settings unique across each host (for example, running multiple installations on the same virtual machine): -[source,js] --------- path.data pid.file server.port @@ -106,7 +101,7 @@ These can be used to automatically update the list of hosts as a cluster is resi [[memory]] === Memory -Kibana has a default memory limit that scales based on total memory available. In some scenarios, such as large reporting jobs, +Kibana has a default memory limit that scales based on total memory available. In some scenarios, such as large reporting jobs, it may make sense to tweak limits to meet more specific requirements. A limit can be defined by setting `--max-old-space-size` in the `node.options` config file found inside the `kibana/config` folder or any other folder configured with the environment variable `KBN_PATH_CONF`. For example, in the Debian-based system, the folder is `/etc/kibana`. From c9bb23ad8d8ba5abf1d0c0575560c6944260ebdc Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Tue, 18 Oct 2022 11:18:25 -0700 Subject: [PATCH 51/74] [DOCS] Clarify why Fleet settings might be grayed out in UI (#143146) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/fleet-settings.asciidoc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index ddce9feb3e640..38f042284218f 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -38,6 +38,10 @@ Refer to {fleet-guide}/air-gapped.html[Air-gapped environments] for details. `xpack.fleet.agents.fleet_server.hosts`:: Hostnames used by {agent} for accessing {fleet-server}. ++ +If configured in your `kibana.yml`, this setting is grayed out and unavailable +in the {fleet} UI. To make this setting editable in the UI, do not configure it +in the configuration file. `xpack.fleet.agents.elasticsearch.hosts`:: Hostnames used by {agent} for accessing {es}. @@ -52,6 +56,9 @@ Hash pin used for certificate verification. The pin is a base64-encoded string o Use these settings to pre-define integrations and agent policies that you want {fleet} to load up by default. +NOTE: These settings are not supported to pre-configure the Endpoint and Cloud +Security integration. + `xpack.fleet.packages`:: List of integrations that are installed when the {fleet} app starts up for the first time. + @@ -126,7 +133,11 @@ List of agent policies that are configured when the {fleet} app starts. ===== `xpack.fleet.outputs`:: -List of outputs that are configured when the {fleet} app starts. +List of outputs that are configured when the {fleet} app starts. ++ +If configured in your `kibana.yml`, output settings are grayed out and +unavailable in the {fleet} UI. To make these settings editable in the UI, do not +configure them in the configuration file. + .Required properties of `xpack.fleet.outputs` [%collapsible%open] From eb666e20b5e21ea76986ef2f5a66b28c4ae9362d Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 18 Oct 2022 13:10:30 -0600 Subject: [PATCH 52/74] [ML] Anomaly Detection datafeed chart: updates tooltip copy for clarity (#143331) * add clearer tooltip messaging to datafeed chart * update tooltip copy to be consistent Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../datafeed_chart_flyout/datafeed_chart_flyout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx index 53ca34886b9ca..0af5fdd05fcae 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx @@ -510,7 +510,7 @@ export const DatafeedChartFlyout: FC = ({ key={'source-results'} color={euiTheme.euiColorPrimary} id={i18n.translate('xpack.ml.jobsList.datafeedChart.sourceSeriesId', { - defaultMessage: 'Source indices', + defaultMessage: 'Source indices document count', })} xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} @@ -523,7 +523,7 @@ export const DatafeedChartFlyout: FC = ({ key={'job-results'} color={euiTheme.euiColorAccentText} id={i18n.translate('xpack.ml.jobsList.datafeedChart.bucketSeriesId', { - defaultMessage: 'Job results', + defaultMessage: 'Datafeed document count', })} xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} From 836b60da22299c51966fbda0fb9bfb47cb4ecc81 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 18 Oct 2022 12:26:11 -0700 Subject: [PATCH 53/74] Migrates core test_utils to package (#143483) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 2 + packages/BUILD.bazel | 2 + .../core-test-helpers-test-utils/BUILD.bazel | 126 ++++++++++++++++++ .../core-test-helpers-test-utils/README.md | 3 + .../core-test-helpers-test-utils/index.ts | 8 +- .../jest.config.js | 13 ++ .../core-test-helpers-test-utils/kibana.jsonc | 7 + .../core-test-helpers-test-utils/package.json | 7 + .../src/create_exportable_type.ts | 22 +++ .../src/setup_server.ts | 45 ++++--- .../tsconfig.json | 17 +++ .../saved_objects/routes/bulk_create.test.ts | 3 +- .../saved_objects/routes/bulk_delete.test.ts | 2 +- .../saved_objects/routes/bulk_get.test.ts | 2 +- .../saved_objects/routes/bulk_resolve.test.ts | 2 +- .../saved_objects/routes/bulk_update.test.ts | 2 +- .../saved_objects/routes/create.test.ts | 2 +- .../saved_objects/routes/delete.test.ts | 2 +- .../routes/delete_unknown_types.test.ts | 2 +- .../saved_objects/routes/export.test.ts | 2 +- .../saved_objects/routes/find.test.ts | 2 +- .../saved_objects/routes/import.test.ts | 2 +- .../legacy_import_export/export.test.ts | 2 +- .../legacy_import_export/import.test.ts | 2 +- .../routes/resolve_import_errors.test.ts | 2 +- .../saved_objects/routes/update.test.ts | 2 +- .../routes/integration_tests/stats.test.ts | 2 +- .../create_apm_event_client/index.test.ts | 2 +- .../routes/integration_tests/find.test.ts | 2 +- .../get_searchable_types.test.ts | 2 +- .../integration_tests/deprecations.test.ts | 2 +- .../integration_tests/browser.test.ts | 2 +- .../integration_tests/screenshot.test.ts | 2 +- .../generation_from_jobparams.test.ts | 2 +- .../management/integration_tests/jobs.test.ts | 2 +- yarn.lock | 8 ++ 37 files changed, 263 insertions(+), 47 deletions(-) create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/BUILD.bazel create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/README.md rename src/core/server/test_utils.ts => packages/core/test-helpers/core-test-helpers-test-utils/index.ts (56%) create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/jest.config.js create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/kibana.jsonc create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/package.json create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/src/create_exportable_type.ts rename src/core/server/integration_tests/saved_objects/routes/test_utils.ts => packages/core/test-helpers/core-test-helpers-test-utils/src/setup_server.ts (53%) create mode 100644 packages/core/test-helpers/core-test-helpers-test-utils/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 91e4463aa9905..863edb6925730 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -835,6 +835,7 @@ packages/core/status/core-status-server-mocks @elastic/kibana-core packages/core/test-helpers/core-test-helpers-deprecations-getters @elastic/kibana-core packages/core/test-helpers/core-test-helpers-http-setup-browser @elastic/kibana-core packages/core/test-helpers/core-test-helpers-so-type-serializer @elastic/kibana-core +packages/core/test-helpers/core-test-helpers-test-utils @elastic/kibana-core packages/core/theme/core-theme-browser @elastic/kibana-core packages/core/theme/core-theme-browser-internal @elastic/kibana-core packages/core/theme/core-theme-browser-mocks @elastic/kibana-core diff --git a/package.json b/package.json index d996dc2756a4d..e65e82fa8cef2 100644 --- a/package.json +++ b/package.json @@ -297,6 +297,7 @@ "@kbn/core-test-helpers-deprecations-getters": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-deprecations-getters", "@kbn/core-test-helpers-http-setup-browser": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-http-setup-browser", "@kbn/core-test-helpers-so-type-serializer": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-so-type-serializer", + "@kbn/core-test-helpers-test-utils": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-test-utils", "@kbn/core-theme-browser": "link:bazel-bin/packages/core/theme/core-theme-browser", "@kbn/core-theme-browser-internal": "link:bazel-bin/packages/core/theme/core-theme-browser-internal", "@kbn/core-theme-browser-mocks": "link:bazel-bin/packages/core/theme/core-theme-browser-mocks", @@ -1030,6 +1031,7 @@ "@types/kbn__core-test-helpers-deprecations-getters": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-deprecations-getters/npm_module_types", "@types/kbn__core-test-helpers-http-setup-browser": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-http-setup-browser/npm_module_types", "@types/kbn__core-test-helpers-so-type-serializer": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-so-type-serializer/npm_module_types", + "@types/kbn__core-test-helpers-test-utils": "link:bazel-bin/packages/core/test-helpers/core-test-helpers-test-utils/npm_module_types", "@types/kbn__core-theme-browser": "link:bazel-bin/packages/core/theme/core-theme-browser/npm_module_types", "@types/kbn__core-theme-browser-internal": "link:bazel-bin/packages/core/theme/core-theme-browser-internal/npm_module_types", "@types/kbn__core-theme-browser-mocks": "link:bazel-bin/packages/core/theme/core-theme-browser-mocks/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 265e5fe75591d..3eb7a3f694954 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -161,6 +161,7 @@ filegroup( "//packages/core/test-helpers/core-test-helpers-deprecations-getters:build", "//packages/core/test-helpers/core-test-helpers-http-setup-browser:build", "//packages/core/test-helpers/core-test-helpers-so-type-serializer:build", + "//packages/core/test-helpers/core-test-helpers-test-utils:build", "//packages/core/theme/core-theme-browser:build", "//packages/core/theme/core-theme-browser-internal:build", "//packages/core/theme/core-theme-browser-mocks:build", @@ -503,6 +504,7 @@ filegroup( "//packages/core/test-helpers/core-test-helpers-deprecations-getters:build_types", "//packages/core/test-helpers/core-test-helpers-http-setup-browser:build_types", "//packages/core/test-helpers/core-test-helpers-so-type-serializer:build_types", + "//packages/core/test-helpers/core-test-helpers-test-utils:build_types", "//packages/core/theme/core-theme-browser:build_types", "//packages/core/theme/core-theme-browser-internal:build_types", "//packages/core/theme/core-theme-browser-mocks:build_types", diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/BUILD.bazel b/packages/core/test-helpers/core-test-helpers-test-utils/BUILD.bazel new file mode 100644 index 0000000000000..9338058a06c7c --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/BUILD.bazel @@ -0,0 +1,126 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-test-helpers-test-utils" +PKG_REQUIRE_NAME = "@kbn/core-test-helpers-test-utils" + +SOURCE_FILES = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "//packages/core/deprecations/core-deprecations-server-mocks", + "//packages/core/execution-context/core-execution-context-server-mocks", + "//packages/core/elasticsearch/core-elasticsearch-server-mocks", + "//packages/core/http/core-http-context-server-internal", + "//packages/core/http/core-http-context-server-mocks", + "//packages/core/http/core-http-server-mocks", + "//packages/core/saved-objects/core-saved-objects-api-server-mocks", + "//packages/core/saved-objects/core-saved-objects-base-server-mocks", + "//packages/core/saved-objects/core-saved-objects-server-mocks", + "//packages/core/ui-settings/core-ui-settings-server-mocks", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "//packages/core/deprecations/core-deprecations-server-mocks:npm_module_types", + "//packages/core/execution-context/core-execution-context-server-mocks:npm_module_types", + "//packages/core/elasticsearch/core-elasticsearch-server-mocks:npm_module_types", + "//packages/core/http/core-http-context-server-internal:npm_module_types", + "//packages/core/http/core-http-context-server-mocks:npm_module_types", + "//packages/core/http/core-http-server-mocks:npm_module_types", + "//packages/core/saved-objects/core-saved-objects-api-server-mocks:npm_module_types", + "//packages/core/saved-objects/core-saved-objects-base-server-mocks:npm_module_types", + "//packages/core/saved-objects/core-saved-objects-server:npm_module_types", + "//packages/core/saved-objects/core-saved-objects-server-mocks:npm_module_types", + "//packages/core/ui-settings/core-ui-settings-server-mocks:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/README.md b/packages/core/test-helpers/core-test-helpers-test-utils/README.md new file mode 100644 index 0000000000000..0404c8ddcbd41 --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/README.md @@ -0,0 +1,3 @@ +# @kbn/core-test-helpers-test-utils + +This package contains Core's server-side test utils. diff --git a/src/core/server/test_utils.ts b/packages/core/test-helpers/core-test-helpers-test-utils/index.ts similarity index 56% rename from src/core/server/test_utils.ts rename to packages/core/test-helpers/core-test-helpers-test-utils/index.ts index 95a05aca5957b..1ffa5814389be 100644 --- a/src/core/server/test_utils.ts +++ b/packages/core/test-helpers/core-test-helpers-test-utils/index.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -export { createHttpServer } from '@kbn/core-http-server-mocks'; -export { setupServer } from './integration_tests/saved_objects/routes/test_utils'; -export { - getDeprecationsFor, - getDeprecationsForGlobalSettings, -} from '@kbn/core-test-helpers-deprecations-getters'; +export { setupServer } from './src/setup_server'; +export { createExportableType } from './src/create_exportable_type'; diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/jest.config.js b/packages/core/test-helpers/core-test-helpers-test-utils/jest.config.js new file mode 100644 index 0000000000000..c5813c1c35c46 --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/packages/core/test-helpers/core-test-helpers-test-utils'], +}; diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/kibana.jsonc b/packages/core/test-helpers/core-test-helpers-test-utils/kibana.jsonc new file mode 100644 index 0000000000000..a587c1fd8566f --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-test-helpers-test-utils", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/package.json b/packages/core/test-helpers/core-test-helpers-test-utils/package.json new file mode 100644 index 0000000000000..cae81416edded --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/core-test-helpers-test-utils", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/src/create_exportable_type.ts b/packages/core/test-helpers/core-test-helpers-test-utils/src/create_exportable_type.ts new file mode 100644 index 0000000000000..f3653bb621ac6 --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/src/create_exportable_type.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsType } from '@kbn/core-saved-objects-server'; +export const createExportableType = (name: string): SavedObjectsType => { + return { + name, + hidden: false, + namespaceType: 'single', + mappings: { + properties: {}, + }, + management: { + importableAndExportable: true, + }, + }; +}; diff --git a/src/core/server/integration_tests/saved_objects/routes/test_utils.ts b/packages/core/test-helpers/core-test-helpers-test-utils/src/setup_server.ts similarity index 53% rename from src/core/server/integration_tests/saved_objects/routes/test_utils.ts rename to packages/core/test-helpers/core-test-helpers-test-utils/src/setup_server.ts index 89fb671aae377..a4365a883fde4 100644 --- a/src/core/server/integration_tests/saved_objects/routes/test_utils.ts +++ b/packages/core/test-helpers/core-test-helpers-test-utils/src/setup_server.ts @@ -9,11 +9,36 @@ import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks'; import { ContextService } from '@kbn/core-http-context-server-internal'; import { createHttpServer, createCoreContext } from '@kbn/core-http-server-mocks'; -import type { SavedObjectsType } from '@kbn/core-saved-objects-server'; -import { contextServiceMock, coreMock } from '../../../mocks'; +import { contextServiceMock } from '@kbn/core-http-context-server-mocks'; +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; +import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks'; +import { deprecationsServiceMock } from '@kbn/core-deprecations-server-mocks'; const defaultCoreId = Symbol('core'); +function createCoreServerRequestHandlerContextMock() { + return { + savedObjects: { + client: savedObjectsClientMock.create(), + typeRegistry: typeRegistryMock.create(), + getClient: savedObjectsClientMock.create, + getExporter: savedObjectsServiceMock.createExporter, + getImporter: savedObjectsServiceMock.createImporter, + }, + elasticsearch: { + client: elasticsearchServiceMock.createScopedClusterClient(), + }, + uiSettings: { + client: uiSettingsServiceMock.createClient(), + }, + deprecations: { + client: deprecationsServiceMock.createClient(), + }, + }; +} export const setupServer = async (coreId: symbol = defaultCoreId) => { const coreContext = createCoreContext({ coreId }); const contextService = new ContextService(coreContext); @@ -24,7 +49,7 @@ export const setupServer = async (coreId: symbol = defaultCoreId) => { context: contextService.setup({ pluginDependencies: new Map() }), executionContext: executionContextServiceMock.createInternalSetupContract(), }); - const handlerContext = coreMock.createRequestHandlerContext(); + const handlerContext = createCoreServerRequestHandlerContextMock(); httpSetup.registerRouteHandlerContext(coreId, 'core', (ctx, req, res) => { return handlerContext; @@ -36,17 +61,3 @@ export const setupServer = async (coreId: symbol = defaultCoreId) => { handlerContext, }; }; - -export const createExportableType = (name: string): SavedObjectsType => { - return { - name, - hidden: false, - namespaceType: 'single', - mappings: { - properties: {}, - }, - management: { - importableAndExportable: true, - }, - }; -}; diff --git a/packages/core/test-helpers/core-test-helpers-test-utils/tsconfig.json b/packages/core/test-helpers/core-test-helpers-test-utils/tsconfig.json new file mode 100644 index 0000000000000..71bb40fe57f3f --- /dev/null +++ b/packages/core/test-helpers/core-test-helpers-test-utils/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts index 91033a02ee134..073025be64bce 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts @@ -13,11 +13,12 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; + import { registerBulkCreateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; type SetupServerReturn = Awaited>; diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts index 2536915f6f068..5b6aae242da88 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerBulkDeleteRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts index 217c750775048..80a2480a31773 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerBulkGetRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts index 8c2069086fdbd..d284f2d937ad0 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerBulkResolveRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts index 94d9a2f8ebbe7..36129d4c50551 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerBulkUpdateRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/create.test.ts b/src/core/server/integration_tests/saved_objects/routes/create.test.ts index 926ff333a29a5..8bc97b44591a9 100644 --- a/src/core/server/integration_tests/saved_objects/routes/create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/create.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerCreateRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts index a8e5d97bf0709..372d9997614b8 100644 --- a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerDeleteRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/delete_unknown_types.test.ts b/src/core/server/integration_tests/saved_objects/routes/delete_unknown_types.test.ts index 7ff8cfbcaf492..9e2671c1aafa4 100644 --- a/src/core/server/integration_tests/saved_objects/routes/delete_unknown_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/delete_unknown_types.test.ts @@ -9,7 +9,7 @@ import supertest from 'supertest'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { SavedObjectsType } from '../../..'; import { registerDeleteUnknownTypesRoute, diff --git a/src/core/server/integration_tests/saved_objects/routes/export.test.ts b/src/core/server/integration_tests/saved_objects/routes/export.test.ts index 3b0400de07b69..3fa9368f37ed0 100644 --- a/src/core/server/integration_tests/saved_objects/routes/export.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/export.test.ts @@ -15,7 +15,7 @@ import { } from '@kbn/core-usage-data-server-mocks'; import { savedObjectsExporterMock } from '@kbn/core-saved-objects-import-export-server-mocks'; import type { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; -import { setupServer, createExportableType } from './test_utils'; +import { setupServer, createExportableType } from '@kbn/core-test-helpers-test-utils'; import { registerExportRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/find.test.ts index 2c7b1c9838b50..e35432d07353b 100644 --- a/src/core/server/integration_tests/saved_objects/routes/find.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/find.test.ts @@ -15,7 +15,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerFindRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/core/server/integration_tests/saved_objects/routes/import.test.ts b/src/core/server/integration_tests/saved_objects/routes/import.test.ts index fd03a8e5b0d09..f6098df7dd75b 100644 --- a/src/core/server/integration_tests/saved_objects/routes/import.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/import.test.ts @@ -22,7 +22,7 @@ import { registerImportRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; -import { setupServer, createExportableType } from './test_utils'; +import { setupServer, createExportableType } from '@kbn/core-test-helpers-test-utils'; type SetupServerReturn = Awaited>; diff --git a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts index c5a12dbb0128f..0f2176ab7e1a8 100644 --- a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/export.test.ts @@ -33,7 +33,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from '../test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; import { SavedObjectsBulkResponse } from '@kbn/core-saved-objects-api-server'; import { diff --git a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts index 85433600e6293..6341278dfcd0c 100644 --- a/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/legacy_import_export/import.test.ts @@ -33,7 +33,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from '../test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { loggerMock } from '@kbn/logging-mocks'; import { SavedObjectsBulkResponse } from '@kbn/core-saved-objects-api-server'; import { diff --git a/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts b/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts index a8d167e41d0d9..8bcf9e1d6fc5f 100644 --- a/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/resolve_import_errors.test.ts @@ -15,7 +15,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer, createExportableType } from './test_utils'; +import { setupServer, createExportableType } from '@kbn/core-test-helpers-test-utils'; import { SavedObjectConfig } from '@kbn/core-saved-objects-base-server-internal'; import { SavedObjectsImporter } from '@kbn/core-saved-objects-import-export-server-internal'; import { diff --git a/src/core/server/integration_tests/saved_objects/routes/update.test.ts b/src/core/server/integration_tests/saved_objects/routes/update.test.ts index 5be1cb1f597e6..0b03b1ceffdb0 100644 --- a/src/core/server/integration_tests/saved_objects/routes/update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/update.test.ts @@ -13,7 +13,7 @@ import { coreUsageStatsClientMock, coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; -import { setupServer } from './test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { registerUpdateRoute, type InternalSavedObjectsRequestHandlerContext, diff --git a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts index 4d35c42626dfe..670749c52c0c4 100644 --- a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts +++ b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts @@ -20,7 +20,7 @@ import { metricsServiceMock, executionContextServiceMock, } from '@kbn/core/server/mocks'; -import { createHttpServer } from '@kbn/core/server/test_utils'; +import { createHttpServer } from '@kbn/core-http-server-mocks'; import { registerStatsRoute } from '../stats'; import supertest from 'supertest'; import { CollectorSet } from '../../collector'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts index b4b7ca3a9b406..d5068669bf27c 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -9,7 +9,7 @@ import { contextServiceMock, executionContextServiceMock, } from '@kbn/core/server/mocks'; -import { createHttpServer } from '@kbn/core/server/test_utils'; +import { createHttpServer } from '@kbn/core-http-server-mocks'; import supertest from 'supertest'; import { APMEventClient } from '.'; diff --git a/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts index 56cd4edaef253..fc53c8d01ebfa 100644 --- a/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts +++ b/x-pack/plugins/global_search/server/routes/integration_tests/find.test.ts @@ -7,7 +7,7 @@ import { of, throwError } from 'rxjs'; import supertest from 'supertest'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { GlobalSearchResult, GlobalSearchBatchedResults } from '../../../common/types'; import { GlobalSearchFindError } from '../../../common/errors'; import { globalSearchPluginMock } from '../../mocks'; diff --git a/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts index a72cb9975db19..7a061b6151f54 100644 --- a/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts +++ b/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts @@ -6,7 +6,7 @@ */ import supertest from 'supertest'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { globalSearchPluginMock } from '../../mocks'; import { registerInternalSearchableTypesRoute } from '../get_searchable_types'; diff --git a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts index 1a13a3d1efa67..5bff863f3f482 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts @@ -6,7 +6,7 @@ */ import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import supertest from 'supertest'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks'; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts index b623c9581c2aa..bb835fb8a039c 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts @@ -7,7 +7,7 @@ import * as Rx from 'rxjs'; import { docLinksServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import supertest from 'supertest'; import { ReportingCore } from '../../..'; import type { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts index fd514302ecae5..e53efe67ebf44 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts @@ -6,7 +6,7 @@ */ import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import supertest from 'supertest'; import { ReportingCore } from '../../..'; import { generatePngObservable } from '../../../export_types/common'; diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts index 2054f76f67ae2..4c3f5d4d851b0 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts @@ -8,7 +8,7 @@ import rison from 'rison-node'; import { BehaviorSubject } from 'rxjs'; import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import supertest from 'supertest'; import { ReportingCore } from '../../..'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; diff --git a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts index 1e92f3ebb538d..f46ce228fa826 100644 --- a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts @@ -10,7 +10,7 @@ jest.mock('../../../lib/content_stream', () => ({ })); import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { BehaviorSubject } from 'rxjs'; -import { setupServer } from '@kbn/core/server/test_utils'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { Readable } from 'stream'; import supertest from 'supertest'; import { ReportingCore } from '../../..'; diff --git a/yarn.lock b/yarn.lock index 35cc88efd5eb4..b6a091aeec06e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3317,6 +3317,10 @@ version "0.0.0" uid "" +"@kbn/core-test-helpers-test-utils@link:bazel-bin/packages/core/test-helpers/core-test-helpers-test-utils": + version "0.0.0" + uid "" + "@kbn/core-theme-browser-internal@link:bazel-bin/packages/core/theme/core-theme-browser-internal": version "0.0.0" uid "" @@ -7485,6 +7489,10 @@ version "0.0.0" uid "" +"@types/kbn__core-test-helpers-test-utils@link:bazel-bin/packages/core/test-helpers/core-test-helpers-test-utils/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__core-theme-browser-internal@link:bazel-bin/packages/core/theme/core-theme-browser-internal/npm_module_types": version "0.0.0" uid "" From 66041ca2c26c2318f0e084619d4bef9b90b9772e Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Tue, 18 Oct 2022 14:50:51 -0500 Subject: [PATCH 54/74] [Lens] Support metric trendlines (#141851) --- packages/kbn-optimizer/limits.yml | 2 +- .../expression_metric/common/constants.ts | 3 + .../metric_trendline_function.test.ts.snap | 353 ++++++++ .../metric_trendline_function.test.ts | 397 +++++++++ .../metric_trendline_function.ts | 144 ++++ .../metric_vis_function.ts | 26 +- .../expression_metric/common/index.ts | 2 +- .../common/types/expression_functions.ts | 30 +- .../common/types/expression_renderers.ts | 2 + .../public/components/metric_vis.test.tsx | 143 ++++ .../public/components/metric_vis.tsx | 49 +- .../expression_metric/public/index.ts | 1 + .../expression_metric/public/plugin.ts | 2 + .../configurations/goal.test.ts | 7 +- .../convert_to_lens/configurations/goal.ts | 15 +- .../configurations/index.test.ts | 3 +- .../convert_to_lens/configurations/index.ts | 14 +- .../configurations/index.test.ts | 3 +- .../convert_to_lens/configurations/index.ts | 15 +- .../convert_to_lens/lib/series/series_agg.ts | 3 +- .../xy/public/convert_to_lens/index.ts | 12 +- .../common/convert_to_lens/constants.ts | 2 + .../convert_to_lens/types/configurations.ts | 12 +- .../common/convert_to_lens/utils.ts | 6 +- .../public/convert_to_lens/utils.ts | 17 +- test/functional/services/inspector.ts | 21 + .../lens/common/expressions/collapse/index.ts | 2 +- .../expressions/datatable/datatable_column.ts | 3 +- x-pack/plugins/lens/common/layer_types.ts | 13 + x-pack/plugins/lens/common/types.ts | 7 +- .../app_plugin/show_underlying_data.test.ts | 5 + .../droppable/on_drop_handler.test.ts | 105 +-- .../droppable/on_drop_handler.ts | 149 ++-- .../datasources/form_based/form_based.test.ts | 309 ++++++- .../datasources/form_based/form_based.tsx | 167 +++- .../form_based_suggestions.test.tsx | 25 + .../datasources/form_based/loader.test.ts | 8 +- .../public/datasources/form_based/loader.ts | 21 +- .../form_based/operations/layer_helpers.ts | 30 +- .../public/datasources/form_based/types.ts | 1 + .../text_based/text_based_languages.test.ts | 1 + .../text_based/text_based_languages.tsx | 8 + .../editor_frame/config_panel/add_layer.tsx | 6 +- .../buttons/drop_targets_utils.tsx | 30 +- .../config_panel/config_panel.test.tsx | 20 + .../config_panel/config_panel.tsx | 235 ++--- .../config_panel/layer_panel.test.tsx | 204 ++++- .../editor_frame/config_panel/layer_panel.tsx | 106 +-- .../editor_frame/data_panel_wrapper.test.tsx | 1 + .../editor_frame/editor_frame.test.tsx | 7 +- .../editor_frame/suggestion_helpers.test.ts | 3 + x-pack/plugins/lens/public/index.ts | 6 +- .../lens/public/mocks/datasource_mock.ts | 2 + .../shared_components/collapse_setting.tsx | 5 +- .../lens/public/state_management/index.ts | 1 + .../state_management/lens_slice.test.ts | 287 ++++++- .../public/state_management/lens_slice.ts | 262 +++++- x-pack/plugins/lens/public/types.ts | 41 +- .../components/dimension_editor.test.tsx | 3 + .../datatable/components/dimension_editor.tsx | 2 +- ...mension_editor_additional_section.test.tsx | 9 +- .../datatable/visualization.test.tsx | 4 + .../legacy_metric/dimension_editor.test.tsx | 4 + .../legacy_metric/visualization.test.ts | 1 + .../__snapshots__/visualization.test.ts.snap | 77 +- .../public/visualizations/metric/constants.ts | 4 + .../metric/dimension_editor.test.tsx | 310 +++++-- .../metric/dimension_editor.tsx | 329 ++++--- .../visualizations/metric/to_expression.ts | 172 ++++ .../visualizations/metric/toolbar.test.tsx | 7 + .../metric/visualization.test.ts | 810 ++++++++++++++---- .../visualizations/metric/visualization.tsx | 620 ++++++++------ .../visualizations/partition/toolbar.tsx | 2 +- .../partition/visualization.test.ts | 3 +- .../lens/public/visualizations/xy/types.ts | 3 +- .../visualizations/xy/visualization.test.ts | 7 + .../visualizations/xy/visualization.tsx | 4 + .../xy/visualization_helpers.tsx | 42 +- .../annotations_config_panel/index.test.tsx | 28 + .../xy_config_panel/xy_config_panel.test.tsx | 15 + .../functional/apps/lens/group3/metric.ts | 45 + .../group3/open_in_lens/agg_based/goal.ts | 1 + .../group3/open_in_lens/agg_based/metric.ts | 9 + .../test/functional/page_objects/lens_page.ts | 3 + 84 files changed, 4691 insertions(+), 1177 deletions(-) create mode 100644 src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_trendline_function.test.ts.snap create mode 100644 src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts create mode 100644 src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.ts create mode 100644 x-pack/plugins/lens/common/layer_types.ts create mode 100644 x-pack/plugins/lens/public/visualizations/metric/to_expression.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 5b96ef213bf3e..93c33b5954eb9 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -74,7 +74,7 @@ pageLoadAssetSize: kibanaUsageCollection: 16463 kibanaUtils: 79713 kubernetesSecurity: 77234 - lens: 36500 + lens: 37000 licenseManagement: 41817 licensing: 29004 lists: 22900 diff --git a/src/plugins/chart_expressions/expression_metric/common/constants.ts b/src/plugins/chart_expressions/expression_metric/common/constants.ts index 03e8852f8155a..7e81bc1dddbda 100644 --- a/src/plugins/chart_expressions/expression_metric/common/constants.ts +++ b/src/plugins/chart_expressions/expression_metric/common/constants.ts @@ -7,6 +7,9 @@ */ export const EXPRESSION_METRIC_NAME = 'metricVis'; +export const EXPRESSION_METRIC_TRENDLINE_NAME = 'metricTrendline'; + +export const DEFAULT_TRENDLINE_NAME = 'default'; export const LabelPosition = { BOTTOM: 'bottom', diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_trendline_function.test.ts.snap b/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_trendline_function.test.ts.snap new file mode 100644 index 0000000000000..f72375e83644e --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_trendline_function.test.ts.snap @@ -0,0 +1,353 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`metric trendline function builds trends with breakdown 1`] = ` +Object { + "css": Array [ + Object { + "x": 1664121600000, + "y": 3264, + }, + Object { + "x": 1664123400000, + "y": 7215, + }, + Object { + "x": 1664125200000, + "y": 9601, + }, + Object { + "x": 1664127000000, + "y": 8458, + }, + ], + "deb": Array [ + Object { + "x": 1664121600000, + "y": NaN, + }, + Object { + "x": 1664123400000, + "y": 9680, + }, + Object { + "x": 1664125200000, + "y": NaN, + }, + Object { + "x": 1664127000000, + "y": NaN, + }, + ], + "gz": Array [ + Object { + "x": 1664121600000, + "y": 3116.5, + }, + Object { + "x": 1664123400000, + "y": NaN, + }, + Object { + "x": 1664125200000, + "y": 4148, + }, + Object { + "x": 1664127000000, + "y": NaN, + }, + ], + "rpm": Array [ + Object { + "x": 1664121600000, + "y": NaN, + }, + Object { + "x": 1664123400000, + "y": NaN, + }, + Object { + "x": 1664125200000, + "y": NaN, + }, + Object { + "x": 1664127000000, + "y": NaN, + }, + ], + "zip": Array [ + Object { + "x": 1664121600000, + "y": NaN, + }, + Object { + "x": 1664123400000, + "y": NaN, + }, + Object { + "x": 1664125200000, + "y": 5037, + }, + Object { + "x": 1664127000000, + "y": NaN, + }, + Object { + "x": 1664128800000, + "y": NaN, + }, + ], +} +`; + +exports[`metric trendline function builds trends without breakdown 1`] = ` +Object { + "default": Array [ + Object { + "x": 1664121600000, + "y": null, + }, + Object { + "x": 1664123400000, + "y": null, + }, + Object { + "x": 1664125200000, + "y": null, + }, + Object { + "x": 1664127000000, + "y": null, + }, + ], +} +`; + +exports[`metric trendline function creates inspector information 1`] = ` +Object { + "columns": Array [ + Object { + "id": "breakdown", + "meta": Object { + "dimensionName": "Split group", + "field": "extension.keyword", + "index": "kibana_sample_data_logs", + "params": Object { + "id": "terms", + "params": Object { + "id": "string", + "missingBucketLabel": "(missing value)", + "otherBucketLabel": "Other", + }, + }, + "source": "esaggs", + "sourceParams": Object { + "enabled": true, + "hasPrecisionError": false, + "id": "0", + "indexPatternId": "90943e30-9a47-11e8-b64d-95841ca0b247", + "params": Object { + "excludeIsRegex": false, + "field": "extension.keyword", + "includeIsRegex": false, + "missingBucket": false, + "missingBucketLabel": "(missing value)", + "order": "desc", + "orderBy": "2", + "otherBucket": true, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": "segment", + "type": "terms", + }, + "type": "string", + }, + "name": "Top 5 values of extension.keyword", + }, + Object { + "id": "time", + "meta": Object { + "dimensionName": "Time field", + "field": "timestamp", + "index": "kibana_sample_data_logs", + "params": Object { + "id": "date", + "params": Object { + "pattern": "HH:mm", + }, + }, + "source": "esaggs", + "sourceParams": Object { + "appliedTimeRange": Object { + "from": "2022-09-25T16:00:00.000Z", + "to": "2022-09-26T16:12:41.742Z", + }, + "enabled": true, + "hasPrecisionError": false, + "id": "1", + "indexPatternId": "90943e30-9a47-11e8-b64d-95841ca0b247", + "params": Object { + "drop_partials": false, + "extendToTimeRange": true, + "extended_bounds": Object {}, + "field": "timestamp", + "interval": "auto", + "min_doc_count": 0, + "scaleMetricValues": false, + "timeRange": Object { + "from": "2022-09-25T16:00:00.000Z", + "to": "2022-09-26T16:12:41.742Z", + }, + "useNormalizedEsInterval": true, + "used_interval": "30m", + }, + "schema": "segment", + "type": "date_histogram", + }, + "type": "date", + }, + "name": "timestamp per 30 minutes", + }, + Object { + "id": "metric", + "meta": Object { + "dimensionName": "Metric", + "field": "bytes", + "index": "kibana_sample_data_logs", + "params": Object { + "id": "number", + }, + "source": "esaggs", + "sourceParams": Object { + "enabled": true, + "hasPrecisionError": false, + "id": "2", + "indexPatternId": "90943e30-9a47-11e8-b64d-95841ca0b247", + "params": Object { + "field": "bytes", + }, + "schema": "metric", + "type": "median", + }, + "type": "number", + }, + "name": "Median of byts", + }, + ], + "meta": Object { + "source": "90943e30-9a47-11e8-b64d-95841ca0b247", + "statistics": Object { + "totalCount": 236, + }, + "type": "esaggs", + }, + "rows": Array [ + Object { + "breakdown": "rpm", + "metric": null, + "time": 1664121600000, + }, + Object { + "breakdown": "rpm", + "metric": null, + "time": 1664123400000, + }, + Object { + "breakdown": "rpm", + "metric": null, + "time": 1664125200000, + }, + Object { + "breakdown": "rpm", + "metric": null, + "time": 1664127000000, + }, + Object { + "breakdown": "deb", + "metric": null, + "time": 1664121600000, + }, + Object { + "breakdown": "deb", + "metric": 9680, + "time": 1664123400000, + }, + Object { + "breakdown": "deb", + "metric": null, + "time": 1664125200000, + }, + Object { + "breakdown": "deb", + "metric": null, + "time": 1664127000000, + }, + Object { + "breakdown": "zip", + "metric": null, + "time": 1664121600000, + }, + Object { + "breakdown": "zip", + "metric": null, + "time": 1664123400000, + }, + Object { + "breakdown": "zip", + "metric": 5037, + "time": 1664125200000, + }, + Object { + "breakdown": "zip", + "metric": null, + "time": 1664127000000, + }, + Object { + "breakdown": "zip", + "metric": null, + "time": 1664128800000, + }, + Object { + "breakdown": "css", + "metric": 3264, + "time": 1664121600000, + }, + Object { + "breakdown": "css", + "metric": 7215, + "time": 1664123400000, + }, + Object { + "breakdown": "css", + "metric": 9601, + "time": 1664125200000, + }, + Object { + "breakdown": "css", + "metric": 8458, + "time": 1664127000000, + }, + Object { + "breakdown": "gz", + "metric": 3116.5, + "time": 1664121600000, + }, + Object { + "breakdown": "gz", + "metric": null, + "time": 1664123400000, + }, + Object { + "breakdown": "gz", + "metric": 4148, + "time": 1664125200000, + }, + Object { + "breakdown": "gz", + "metric": null, + "time": 1664127000000, + }, + ], + "type": "datatable", +} +`; diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts new file mode 100644 index 0000000000000..6dcfe4c79147a --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts @@ -0,0 +1,397 @@ +/* + * 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 { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common'; +import { Adapters } from '@kbn/inspector-plugin/common'; +import { SerializableRecord } from '@kbn/utility-types'; +import { TrendlineArguments } from '../types'; +import { metricTrendlineFunction } from './metric_trendline_function'; + +const fakeContext = {} as ExecutionContext; +const fakeInput = {} as Datatable; +const metricTrendline = (args: TrendlineArguments) => + metricTrendlineFunction().fn(fakeInput, args, fakeContext); + +describe('metric trendline function', () => { + const tableWithBreakdown: Datatable = { + type: 'datatable', + columns: [ + { + id: 'breakdown', + name: 'Top 5 values of extension.keyword', + meta: { + type: 'string', + field: 'extension.keyword', + index: 'kibana_sample_data_logs', + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: '(missing value)', + }, + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + id: '0', + enabled: true, + type: 'terms', + params: { + field: 'extension.keyword', + orderBy: '2', + order: 'desc', + size: 5, + otherBucket: true, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: '(missing value)', + includeIsRegex: false, + excludeIsRegex: false, + }, + schema: 'segment', + }, + }, + }, + { + id: 'time', + name: 'timestamp per 30 minutes', + meta: { + type: 'date', + field: 'timestamp', + index: 'kibana_sample_data_logs', + params: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + appliedTimeRange: { + from: '2022-09-25T16:00:00.000Z', + to: '2022-09-26T16:12:41.742Z', + }, + id: '1', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: '2022-09-25T16:00:00.000Z', + to: '2022-09-26T16:12:41.742Z', + }, + useNormalizedEsInterval: true, + extendToTimeRange: true, + scaleMetricValues: false, + interval: 'auto', + used_interval: '30m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + schema: 'segment', + }, + }, + }, + { + id: 'metric', + name: 'Median of byts', + meta: { + type: 'number', + field: 'bytes', + index: 'kibana_sample_data_logs', + params: { + id: 'number', + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + id: '2', + enabled: true, + type: 'median', + params: { + field: 'bytes', + }, + schema: 'metric', + }, + }, + }, + ], + rows: [ + { + breakdown: 'rpm', + time: 1664121600000, + metric: null, + }, + { + breakdown: 'rpm', + time: 1664123400000, + metric: null, + }, + { + breakdown: 'rpm', + time: 1664125200000, + metric: null, + }, + { + breakdown: 'rpm', + time: 1664127000000, + metric: null, + }, + { + breakdown: 'deb', + time: 1664121600000, + metric: null, + }, + { + breakdown: 'deb', + time: 1664123400000, + metric: 9680, + }, + { + breakdown: 'deb', + time: 1664125200000, + metric: null, + }, + { + breakdown: 'deb', + time: 1664127000000, + metric: null, + }, + { + breakdown: 'zip', + time: 1664121600000, + metric: null, + }, + { + breakdown: 'zip', + time: 1664123400000, + metric: null, + }, + { + breakdown: 'zip', + time: 1664125200000, + metric: 5037, + }, + { + breakdown: 'zip', + time: 1664127000000, + metric: null, + }, + { + breakdown: 'zip', + time: 1664128800000, + metric: null, + }, + { + breakdown: 'css', + time: 1664121600000, + metric: 3264, + }, + { + breakdown: 'css', + time: 1664123400000, + metric: 7215, + }, + { + breakdown: 'css', + time: 1664125200000, + metric: 9601, + }, + { + breakdown: 'css', + time: 1664127000000, + metric: 8458, + }, + { + breakdown: 'gz', + time: 1664121600000, + metric: 3116.5, + }, + { + breakdown: 'gz', + time: 1664123400000, + metric: null, + }, + { + breakdown: 'gz', + time: 1664125200000, + metric: 4148, + }, + { + breakdown: 'gz', + time: 1664127000000, + metric: null, + }, + ], + meta: { + type: 'esaggs', + source: '90943e30-9a47-11e8-b64d-95841ca0b247', + statistics: { + totalCount: 236, + }, + }, + }; + + const tableWithoutBreakdown: Datatable = { + type: 'datatable', + columns: [ + { + id: 'time', + name: 'timestamp per 30 minutes', + meta: { + type: 'date', + field: 'timestamp', + index: 'kibana_sample_data_logs', + params: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + appliedTimeRange: { + from: '2022-09-25T16:00:00.000Z', + to: '2022-09-26T16:12:41.742Z', + }, + id: '1', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: '2022-09-25T16:00:00.000Z', + to: '2022-09-26T16:12:41.742Z', + }, + useNormalizedEsInterval: true, + extendToTimeRange: true, + scaleMetricValues: false, + interval: 'auto', + used_interval: '30m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + schema: 'segment', + }, + }, + }, + { + id: 'metric', + name: 'Median of byts', + meta: { + type: 'number', + field: 'bytes', + index: 'kibana_sample_data_logs', + params: { + id: 'number', + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + id: '2', + enabled: true, + type: 'median', + params: { + field: 'bytes', + }, + schema: 'metric', + }, + }, + }, + ], + rows: [ + { + time: 1664121600000, + metric: null, + }, + { + time: 1664123400000, + metric: null, + }, + { + time: 1664125200000, + metric: null, + }, + { + time: 1664127000000, + metric: null, + }, + ], + meta: { + type: 'esaggs', + source: '90943e30-9a47-11e8-b64d-95841ca0b247', + statistics: { + totalCount: 236, + }, + }, + }; + + it.each(['metric', 'time', 'breakdown'])('checks %s accessor', (accessor) => { + const table = { + ...tableWithBreakdown, + columns: tableWithBreakdown.columns.filter((column) => column.id !== accessor), + }; + const args = { + table, + metric: 'metric', + timeField: 'time', + breakdownBy: 'breakdown', + inspectorTableId: '', + }; + + expect(() => metricTrendline(args)).toThrow(); + }); + + it('checks accessors', () => {}); + + it('builds trends with breakdown', () => { + const { trends } = metricTrendline({ + table: tableWithBreakdown, + metric: 'metric', + timeField: 'time', + breakdownBy: 'breakdown', + inspectorTableId: '', + }); + expect(trends).toMatchSnapshot(); + }); + + it('builds trends without breakdown', () => { + const { trends } = metricTrendline({ + table: tableWithoutBreakdown, + metric: 'metric', + timeField: 'time', + inspectorTableId: '', + }); + expect(trends).toMatchSnapshot(); + }); + + it('creates inspector information', () => { + const tableId = 'my-id'; + + const { inspectorTable, inspectorTableId } = metricTrendline({ + table: tableWithBreakdown, + metric: 'metric', + timeField: 'time', + breakdownBy: 'breakdown', + inspectorTableId: tableId, + }); + + expect(inspectorTableId).toBe(tableId); + expect(inspectorTable).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.ts new file mode 100644 index 0000000000000..4d980b85cf059 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.ts @@ -0,0 +1,144 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { + validateAccessor, + getColumnByAccessor, + prepareLogTable, + Dimension, +} from '@kbn/visualizations-plugin/common/utils'; +import { DatatableRow } from '@kbn/expressions-plugin/common'; +import { MetricWTrend } from '@elastic/charts'; +import type { TrendlineExpressionFunctionDefinition } from '../types'; +import { DEFAULT_TRENDLINE_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants'; + +export const metricTrendlineFunction = (): TrendlineExpressionFunctionDefinition => ({ + name: EXPRESSION_METRIC_TRENDLINE_NAME, + inputTypes: ['datatable'], + type: EXPRESSION_METRIC_TRENDLINE_NAME, + help: i18n.translate('expressionMetricVis.trendline.function.help', { + defaultMessage: 'Metric visualization', + }), + args: { + metric: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.trendline.function.metric.help', { + defaultMessage: 'The primary metric.', + }), + required: true, + }, + timeField: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.trendline.function.timeField.help', { + defaultMessage: 'The time field for the trend line', + }), + required: true, + }, + breakdownBy: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.trendline.function.breakdownBy.help', { + defaultMessage: 'The dimension containing the labels for sub-categories.', + }), + }, + table: { + types: ['datatable'], + help: i18n.translate('expressionMetricVis.trendline.function.table.help', { + defaultMessage: 'A data table', + }), + multi: false, + }, + inspectorTableId: { + types: ['string'], + help: i18n.translate('expressionMetricVis.trendline.function.inspectorTableId.help', { + defaultMessage: 'An ID for the inspector table', + }), + multi: false, + default: 'trendline', + }, + }, + fn(input, args, handlers) { + const table = args.table; + validateAccessor(args.metric, table.columns); + validateAccessor(args.timeField, table.columns); + validateAccessor(args.breakdownBy, table.columns); + + const argsTable: Dimension[] = [ + [ + [args.metric], + i18n.translate('expressionMetricVis.function.dimension.metric', { + defaultMessage: 'Metric', + }), + ], + [ + [args.timeField], + i18n.translate('expressionMetricVis.function.dimension.timeField', { + defaultMessage: 'Time field', + }), + ], + ]; + + if (args.breakdownBy) { + argsTable.push([ + [args.breakdownBy], + i18n.translate('expressionMetricVis.function.dimension.splitGroup', { + defaultMessage: 'Split group', + }), + ]); + } + + const inspectorTable = prepareLogTable(table, argsTable, true); + + const metricColId = getColumnByAccessor(args.metric, table.columns)?.id; + const timeColId = getColumnByAccessor(args.timeField, table.columns)?.id; + + if (!metricColId || !timeColId) { + throw new Error("Metric trendline - couldn't find metric or time column!"); + } + + const trends: Record = {}; + + if (!args.breakdownBy) { + trends[DEFAULT_TRENDLINE_NAME] = table.rows.map((row) => ({ + x: row[timeColId], + y: row[metricColId], + })); + } else { + const breakdownByColId = getColumnByAccessor(args.breakdownBy, table.columns)?.id; + + if (!breakdownByColId) { + throw new Error("Metric trendline - couldn't find breakdown column!"); + } + + const rowsByBreakdown: Record = {}; + table.rows.forEach((row) => { + const breakdownTerm = row[breakdownByColId]; + if (!(breakdownTerm in rowsByBreakdown)) { + rowsByBreakdown[breakdownTerm] = []; + } + rowsByBreakdown[breakdownTerm].push(row); + }); + + for (const breakdownTerm in rowsByBreakdown) { + if (!rowsByBreakdown.hasOwnProperty(breakdownTerm)) continue; + trends[breakdownTerm] = rowsByBreakdown[breakdownTerm].map((row) => ({ + x: row[timeColId] !== null ? row[timeColId] : NaN, + y: row[metricColId] !== null ? row[metricColId] : NaN, + })); + } + } + + return { + type: EXPRESSION_METRIC_TRENDLINE_NAME, + trends, + inspectorTable, + inspectorTableId: args.inspectorTableId, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts index 10067f702d906..04a1284c1cf34 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts @@ -16,7 +16,7 @@ import { import { LayoutDirection } from '@elastic/charts'; import { visType } from '../types'; import { MetricVisExpressionFunctionDefinition } from '../types'; -import { EXPRESSION_METRIC_NAME } from '../constants'; +import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants'; export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ name: EXPRESSION_METRIC_NAME, @@ -51,6 +51,12 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ defaultMessage: 'The dimension containing the labels for sub-categories.', }), }, + trendline: { + types: [EXPRESSION_METRIC_TRENDLINE_NAME], + help: i18n.translate('expressionMetricVis.function.trendline.help', { + defaultMessage: 'An optional trendline configuration', + }), + }, subtitle: { types: ['string'], help: i18n.translate('expressionMetricVis.function.subtitle.help', { @@ -98,6 +104,14 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ 'Specifies the minimum number of tiles in the metric grid regardless of the input data.', }), }, + inspectorTableId: { + types: ['string'], + help: i18n.translate('expressionMetricVis.function.inspectorTableId.help', { + defaultMessage: 'An ID for the inspector table', + }), + multi: false, + default: 'default', + }, }, fn(input, args, handlers) { validateAccessor(args.metric, input.columns); @@ -146,7 +160,14 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ } const logTable = prepareLogTable(input, argsTable, true); - handlers.inspectorAdapters.tables.logDatatable('default', logTable); + handlers.inspectorAdapters.tables.logDatatable(args.inspectorTableId, logTable); + + if (args.trendline?.inspectorTable && args.trendline.inspectorTableId) { + handlers.inspectorAdapters.tables.logDatatable( + args.trendline?.inspectorTableId, + args.trendline?.inspectorTable + ); + } } return { @@ -164,6 +185,7 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ progressDirection: args.progressDirection, maxCols: args.maxCols, minTiles: args.minTiles, + trends: args.trendline?.trends, }, dimensions: { metric: args.metric, diff --git a/src/plugins/chart_expressions/expression_metric/common/index.ts b/src/plugins/chart_expressions/expression_metric/common/index.ts index ee023dca2f4ff..163c153efa9ee 100755 --- a/src/plugins/chart_expressions/expression_metric/common/index.ts +++ b/src/plugins/chart_expressions/expression_metric/common/index.ts @@ -22,4 +22,4 @@ export type { export { metricVisFunction } from './expression_functions'; -export { EXPRESSION_METRIC_NAME } from './constants'; +export { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from './constants'; diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts index d44b34fa736d1..9aa67b0df2ee5 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts @@ -7,22 +7,23 @@ */ import type { PaletteOutput } from '@kbn/coloring'; -import { LayoutDirection } from '@elastic/charts'; +import { LayoutDirection, MetricWTrend } from '@elastic/charts'; import { Datatable, ExpressionFunctionDefinition, ExpressionValueRender, } from '@kbn/expressions-plugin/common'; -import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; +import { ExpressionValueVisDimension, prepareLogTable } from '@kbn/visualizations-plugin/common'; import { CustomPaletteState } from '@kbn/charts-plugin/common'; import { VisParams, visType } from './expression_renderers'; -import { EXPRESSION_METRIC_NAME } from '../constants'; +import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants'; export interface MetricArguments { metric: ExpressionValueVisDimension | string; secondaryMetric?: ExpressionValueVisDimension | string; max?: ExpressionValueVisDimension | string; breakdownBy?: ExpressionValueVisDimension | string; + trendline?: TrendlineResult; subtitle?: string; secondaryPrefix?: string; progressDirection: LayoutDirection; @@ -30,6 +31,7 @@ export interface MetricArguments { palette?: PaletteOutput; maxCols: number; minTiles?: number; + inspectorTableId: string; } export type MetricInput = Datatable; @@ -46,3 +48,25 @@ export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition MetricArguments, ExpressionValueRender >; + +export interface TrendlineArguments { + metric: ExpressionValueVisDimension | string; + timeField: ExpressionValueVisDimension | string; + breakdownBy?: ExpressionValueVisDimension | string; + table: Datatable; + inspectorTableId: string; +} + +export interface TrendlineResult { + type: typeof EXPRESSION_METRIC_TRENDLINE_NAME; + trends: Record; + inspectorTable: ReturnType; + inspectorTableId: string; +} + +export type TrendlineExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof EXPRESSION_METRIC_TRENDLINE_NAME, + Datatable, + TrendlineArguments, + TrendlineResult +>; diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts index 75144d1cf5525..48b4b4ce0f524 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts @@ -9,6 +9,7 @@ import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { CustomPaletteState } from '@kbn/charts-plugin/common'; import { LayoutDirection } from '@elastic/charts'; +import { TrendlineResult } from './expression_functions'; export const visType = 'metric'; @@ -27,6 +28,7 @@ export interface MetricVisParam { progressDirection: LayoutDirection; maxCols: number; minTiles?: number; + trends?: TrendlineResult['trends']; } export interface VisParams { diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx index ab5b340012125..f3c1792c913c1 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx @@ -16,6 +16,7 @@ import { MetricElementEvent, MetricWNumber, MetricWProgress, + MetricWTrend, Settings, } from '@elastic/charts'; import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; @@ -25,6 +26,8 @@ import { HtmlAttributes } from 'csstype'; import { CustomPaletteState } from '@kbn/charts-plugin/common/expressions/palette/types'; import { DimensionsVisParam } from '../../common'; import { euiThemeVars } from '@kbn/ui-theme'; +import { DEFAULT_TRENDLINE_NAME } from '../../common/constants'; +import faker from 'faker'; const mockDeserialize = jest.fn((params) => { const converter = @@ -367,6 +370,40 @@ describe('MetricVisComponent', function () { (getConfig(basePriceColumnId, 'horizontal') as MetricWProgress).progressBarDirection ).toBe('horizontal'); }); + + it('should configure trendline if provided', () => { + const trends = { + [DEFAULT_TRENDLINE_NAME]: [ + { x: 1, y: 2 }, + { x: 3, y: 4 }, + { x: 5, y: 6 }, + { x: 7, y: 8 }, + ], + }; + + const tileConfig = shallow( + + ) + .find(Metric) + .props().data![0][0]! as MetricWTrend; + + expect(tileConfig.trend).toEqual(trends[DEFAULT_TRENDLINE_NAME]); + expect(tileConfig.trendShape).toEqual('area'); + }); }); describe('metric grid', () => { @@ -701,6 +738,74 @@ describe('MetricVisComponent', function () { ] `); }); + it('should configure trendlines if provided', () => { + const trends: Record = { + Friday: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + Wednesday: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + Saturday: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + Sunday: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + Thursday: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + Other: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + // this one shouldn't show up! + [DEFAULT_TRENDLINE_NAME]: [ + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + { x: faker.random.number(), y: faker.random.number() }, + ], + }; + + const data = shallow( + + ) + .find(Metric) + .props().data![0] as MetricWTrend[]; + + data?.forEach((tileConfig) => { + expect(tileConfig.trend).toEqual(trends[tileConfig.title!]); + expect(tileConfig.trendShape).toEqual('area'); + }); + }); it('renders with no data', () => { const component = shallow( @@ -816,6 +921,44 @@ describe('MetricVisComponent', function () { expect(renderCompleteSpy).toHaveBeenCalledTimes(1); }); + it('should convert null values to NaN', () => { + const metricId = faker.random.word(); + + const tableWNull: Datatable = { + type: 'datatable', + columns: [ + { + id: metricId, + name: metricId, + meta: { + type: 'number', + }, + }, + ], + rows: [{ [metricId]: null }], + }; + + const metricConfig = shallow( + + ) + .find(Metric) + .props().data![0][0]! as MetricWNumber; + + expect(metricConfig.value).toBeNaN(); + }); + describe('filter events', () => { const fireEventSpy = jest.fn(); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx index f3e7a2864ec86..e8883ad16935b 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx @@ -18,13 +18,14 @@ import { isMetricElementEvent, RenderChangeListener, Settings, + MetricWTrend, + MetricWNumber, } from '@elastic/charts'; import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import type { Datatable, DatatableColumn, - DatatableRow, IInterpreterRenderHandlers, RenderMode, } from '@kbn/expressions-plugin/common'; @@ -35,6 +36,7 @@ import { CUSTOM_PALETTE } from '@kbn/coloring'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { useResizeObserver } from '@elastic/eui'; +import { DEFAULT_TRENDLINE_NAME } from '../../common/constants'; import { VisParams } from '../../common'; import { getPaletteService, @@ -222,28 +224,20 @@ export const MetricVis = ({ .getConverterFor('text'); } - let getProgressBarConfig = (_row: DatatableRow): Partial => ({}); - const maxColId = config.dimensions.max ? getColumnByAccessor(config.dimensions.max, data.columns)?.id : undefined; - if (maxColId) { - getProgressBarConfig = (_row: DatatableRow): Partial => ({ - domainMax: _row[maxColId], - progressBarDirection: config.metric.progressDirection, - }); - } const metricConfigs: MetricSpec['data'][number] = ( breakdownByColumn ? data.rows : data.rows.slice(0, 1) ).map((row, rowIdx) => { - const value = row[primaryMetricColumn.id]; + const value: number = row[primaryMetricColumn.id] !== null ? row[primaryMetricColumn.id] : NaN; const title = breakdownByColumn ? formatBreakdownValue(row[breakdownByColumn.id]) : primaryMetricColumn.name; const subtitle = breakdownByColumn ? primaryMetricColumn.name : config.metric.subtitle; const secondaryPrefix = config.metric.secondaryPrefix ?? secondaryMetricColumn?.name; - return { + const baseMetric: MetricWNumber = { value, valueFormatter: formatPrimaryMetric, title, @@ -272,8 +266,39 @@ export const MetricVis = ({ rowIdx ) ?? defaultColor : config.metric.color ?? defaultColor, - ...getProgressBarConfig(row), }; + + const trendId = breakdownByColumn ? row[breakdownByColumn.id] : DEFAULT_TRENDLINE_NAME; + if (config.metric.trends && config.metric.trends[trendId]) { + const metricWTrend: MetricWTrend = { + ...baseMetric, + trend: config.metric.trends[trendId], + trendShape: 'area', + trendA11yTitle: i18n.translate('expressionMetricVis.trendA11yTitle', { + defaultMessage: '{dataTitle} over time.', + values: { + dataTitle: primaryMetricColumn.name, + }, + }), + trendA11yDescription: i18n.translate('expressionMetricVis.trendA11yDescription', { + defaultMessage: 'A line chart showing the trend of the primary metric over time.', + }), + }; + + return metricWTrend; + } + + if (maxColId && config.metric.progressDirection) { + const metricWProgress: MetricWProgress = { + ...baseMetric, + domainMax: row[maxColId], + progressBarDirection: config.metric.progressDirection, + }; + + return metricWProgress; + } + + return baseMetric; }); if (config.metric.minTiles) { diff --git a/src/plugins/chart_expressions/expression_metric/public/index.ts b/src/plugins/chart_expressions/expression_metric/public/index.ts index c8d5d080bd4e6..765c0738924b3 100644 --- a/src/plugins/chart_expressions/expression_metric/public/index.ts +++ b/src/plugins/chart_expressions/expression_metric/public/index.ts @@ -13,3 +13,4 @@ export function plugin() { } export { getDataBoundsForPalette } from './utils'; +export { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../common'; diff --git a/src/plugins/chart_expressions/expression_metric/public/plugin.ts b/src/plugins/chart_expressions/expression_metric/public/plugin.ts index f1820ee8d36de..4b13497596754 100644 --- a/src/plugins/chart_expressions/expression_metric/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_metric/public/plugin.ts @@ -17,6 +17,7 @@ import { setFormatService, setPaletteService } from './services'; import { getMetricVisRenderer } from './expression_renderers'; import { setThemeService } from './services/theme_service'; import { setUiSettingsService } from './services/ui_settings'; +import { metricTrendlineFunction } from '../common/expression_functions/metric_trendline_function'; /** @internal */ export interface ExpressionMetricPluginSetup { @@ -45,6 +46,7 @@ export class ExpressionMetricPlugin implements Plugin { }); expressions.registerFunction(metricVisFunction); + expressions.registerFunction(metricTrendlineFunction); expressions.registerRenderer(getMetricVisRenderer({ getStartDeps })); setUiSettingsService(core.uiSettings); diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts index 6f472ca29af46..6d88ae3f3b0d5 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts @@ -8,6 +8,7 @@ import { ColorSchemas } from '@kbn/charts-plugin/common'; import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { CollapseFunction } from '@kbn/visualizations-plugin/common'; import { GaugeVisParams } from '../../types'; import { getConfiguration } from './goal'; @@ -69,7 +70,10 @@ describe('getConfiguration', () => { buckets, maxAccessor, columnsWithoutReferenced: [], - bucketCollapseFn: { [collapseFn]: [breakdownByAccessor] }, + bucketCollapseFn: { [collapseFn]: [breakdownByAccessor] } as Record< + CollapseFunction, + string[] + >, }) ).toEqual({ breakdownByAccessor, @@ -78,6 +82,7 @@ describe('getConfiguration', () => { layerType: 'data', maxAccessor, metricAccessor, + showBar: true, palette, }); }); diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts index ec56280fa25cc..46e1f5f05399e 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts @@ -7,7 +7,11 @@ */ import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; -import { Column, MetricVisConfiguration } from '@kbn/visualizations-plugin/common'; +import { + CollapseFunction, + Column, + MetricVisConfiguration, +} from '@kbn/visualizations-plugin/common'; import { GaugeVisParams } from '../../types'; export const getConfiguration = ( @@ -28,15 +32,15 @@ export const getConfiguration = ( }; maxAccessor: string; columnsWithoutReferenced: Column[]; - bucketCollapseFn?: Record; + bucketCollapseFn?: Record; } ): MetricVisConfiguration => { const [metricAccessor] = metrics; const [breakdownByAccessor] = buckets.all; const collapseFn = bucketCollapseFn - ? Object.keys(bucketCollapseFn).find((key) => - bucketCollapseFn[key].includes(breakdownByAccessor) - ) + ? (Object.keys(bucketCollapseFn).find((key) => + bucketCollapseFn[key as CollapseFunction].includes(breakdownByAccessor) + ) as CollapseFunction) : undefined; return { layerId, @@ -45,6 +49,7 @@ export const getConfiguration = ( metricAccessor, breakdownByAccessor, maxAccessor, + showBar: Boolean(maxAccessor), collapseFn, subtitle: gauge.labels.show && gauge.style.subText ? gauge.style.subText : undefined, }; diff --git a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts index 5ccfb169c91ae..f3c80eb87d5e7 100644 --- a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts @@ -8,6 +8,7 @@ import { ColorSchemas } from '@kbn/charts-plugin/common'; import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { CollapseFunction } from '@kbn/visualizations-plugin/common'; import { getConfiguration } from '.'; import { VisParams } from '../../types'; @@ -50,7 +51,7 @@ describe('getConfiguration', () => { metrics: [metric], buckets: { all: [bucket], customBuckets: { metric: bucket } }, columnsWithoutReferenced: [], - bucketCollapseFn: { [collapseFn]: [bucket] }, + bucketCollapseFn: { [collapseFn]: [bucket] } as Record, }) ).toEqual({ breakdownByAccessor: bucket, diff --git a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts index 7b1b42a0211f5..04969e0c99f65 100644 --- a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts @@ -7,7 +7,11 @@ */ import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; -import { Column, MetricVisConfiguration } from '@kbn/visualizations-plugin/common'; +import { + CollapseFunction, + Column, + MetricVisConfiguration, +} from '@kbn/visualizations-plugin/common'; import { VisParams } from '../../types'; export const getConfiguration = ( @@ -26,15 +30,15 @@ export const getConfiguration = ( customBuckets: Record; }; columnsWithoutReferenced: Column[]; - bucketCollapseFn?: Record; + bucketCollapseFn?: Record; } ): MetricVisConfiguration => { const [metricAccessor] = metrics; const [breakdownByAccessor] = buckets.all; const collapseFn = bucketCollapseFn - ? Object.keys(bucketCollapseFn).find((key) => - bucketCollapseFn[key].includes(breakdownByAccessor) - ) + ? (Object.keys(bucketCollapseFn).find((key) => + bucketCollapseFn[key as CollapseFunction].includes(breakdownByAccessor) + ) as CollapseFunction) : undefined; return { layerId, diff --git a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts index d2e68436700d3..19e4a004bd6fc 100644 --- a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts @@ -8,6 +8,7 @@ import { AggTypes } from '../../../common'; import { getConfiguration } from '.'; +import { CollapseFunction } from '@kbn/visualizations-plugin/common'; const params = { perPage: 20, @@ -48,7 +49,7 @@ describe('getConfiguration', () => { }, }, ], - bucketCollapseFn: { sum: ['bucket-1'] }, + bucketCollapseFn: { sum: ['bucket-1'] } as Record, }) ).toEqual({ columns: [ diff --git a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts index 5079b25258a75..b532a79df7d6b 100644 --- a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts @@ -6,19 +6,26 @@ * Side Public License, v 1. */ -import { Column, PagingState, TableVisConfiguration } from '@kbn/visualizations-plugin/common'; +import { + CollapseFunction, + Column, + PagingState, + TableVisConfiguration, +} from '@kbn/visualizations-plugin/common'; import { TableVisParams } from '../../../common'; const getColumns = ( params: TableVisParams, metrics: string[], columns: Column[], - bucketCollapseFn?: Record + bucketCollapseFn?: Record ) => { const { showTotal, totalFunc } = params; return columns.map(({ columnId }) => { const collapseFn = bucketCollapseFn - ? Object.keys(bucketCollapseFn).find((key) => bucketCollapseFn[key].includes(columnId)) + ? (Object.keys(bucketCollapseFn).find((key) => + bucketCollapseFn[key as CollapseFunction].includes(columnId) + ) as CollapseFunction) : undefined; return { columnId, @@ -61,7 +68,7 @@ export const getConfiguration = ( customBuckets: Record; }; columnsWithoutReferenced: Column[]; - bucketCollapseFn?: Record; + bucketCollapseFn?: Record; } ): TableVisConfiguration => { return { diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/series_agg.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/series_agg.ts index 030bcd887f9cb..92ad627c55970 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/series_agg.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/series_agg.ts @@ -5,9 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { CollapseFunction } from '@kbn/visualizations-plugin/common'; import type { Metric } from '../../../../common/types'; -const functionMap: Partial> = { +const functionMap: Partial> = { mean: 'avg', min: 'min', max: 'max', diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/index.ts b/src/plugins/vis_types/xy/public/convert_to_lens/index.ts index 3b4339828c6d5..88965cc77f118 100644 --- a/src/plugins/vis_types/xy/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/xy/public/convert_to_lens/index.ts @@ -7,7 +7,7 @@ */ import { METRIC_TYPES } from '@kbn/data-plugin/public'; -import { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common'; +import { CollapseFunction, Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common'; import { convertToLensModule, getVisSchemas, @@ -25,7 +25,7 @@ export interface Layer { columnOrder: never[]; seriesIdsMap: Record; isReferenceLineLayer: boolean; - collapseFn?: string; + collapseFn?: CollapseFunction; } const SIBBLING_PIPELINE_AGGS: string[] = [ @@ -175,9 +175,11 @@ export const convertToLens: ConvertXYToLensVisualization = async (vis, timefilte } }); const collapseFn = l.bucketCollapseFn - ? Object.keys(l.bucketCollapseFn).find((key) => - l.bucketCollapseFn[key].includes(l.buckets.customBuckets[l.metrics[0]]) - ) + ? (Object.keys(l.bucketCollapseFn).find((key) => + l.bucketCollapseFn[key as CollapseFunction].includes( + l.buckets.customBuckets[l.metrics[0]] + ) + ) as CollapseFunction) : undefined; return { indexPatternId, diff --git a/src/plugins/visualizations/common/convert_to_lens/constants.ts b/src/plugins/visualizations/common/convert_to_lens/constants.ts index b3af9b9394d67..00890884fd722 100644 --- a/src/plugins/visualizations/common/convert_to_lens/constants.ts +++ b/src/plugins/visualizations/common/convert_to_lens/constants.ts @@ -134,3 +134,5 @@ export const GaugeColorModes = { PALETTE: 'palette', NONE: 'none', } as const; + +export const CollapseFunctions = ['sum', 'avg', 'min', 'max'] as const; diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts index 72d0323ac3525..4f7a5ad715215 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -26,8 +26,11 @@ import { GaugeLabelMajorModes, GaugeColorModes, GaugeCentralMajorModes, + CollapseFunctions, } from '../constants'; +export type CollapseFunction = typeof CollapseFunctions[number]; + export type FillType = $Values; export type SeriesType = $Values; export type YAxisMode = $Values; @@ -72,7 +75,7 @@ export interface XYDataLayerConfig { yConfig?: YConfig[]; splitAccessor?: string; palette?: PaletteOutput; - collapseFn?: string; + collapseFn?: CollapseFunction; xScaleType?: 'time' | 'linear' | 'ordinal'; isHistogram?: boolean; columnToLabel?: string; @@ -179,7 +182,7 @@ export interface ColumnState { columnId: string; summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max'; alignment?: 'left' | 'right' | 'center'; - collapseFn?: string; + collapseFn?: CollapseFunction; } export interface TableVisConfiguration { @@ -203,10 +206,11 @@ export interface MetricVisConfiguration { breakdownByAccessor?: string; // the dimensions can optionally be single numbers // computed by collapsing all rows - collapseFn?: string; + collapseFn?: CollapseFunction; subtitle?: string; secondaryPrefix?: string; progressDirection?: LayoutDirection; + showBar?: boolean; color?: string; palette?: PaletteOutput; maxCols?: number; @@ -218,7 +222,7 @@ export interface PartitionLayerState { primaryGroups: string[]; secondaryGroups?: string[]; metric?: string; - collapseFns?: Record; + collapseFns?: Record; numberDisplay: NumberDisplayType; categoryDisplay: CategoryDisplayType; legendDisplay: LegendDisplayType; diff --git a/src/plugins/visualizations/common/convert_to_lens/utils.ts b/src/plugins/visualizations/common/convert_to_lens/utils.ts index 51c7e8239a439..6a875bf63bea4 100644 --- a/src/plugins/visualizations/common/convert_to_lens/utils.ts +++ b/src/plugins/visualizations/common/convert_to_lens/utils.ts @@ -7,8 +7,9 @@ */ import type { DataViewField } from '@kbn/data-views-plugin/common'; +import { CollapseFunctions } from './constants'; import type { SupportedMetric } from './lib/convert/supported_metrics'; -import type { Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types'; +import type { CollapseFunction, Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types'; export const isAnnotationsLayer = ( layer: Pick @@ -31,3 +32,6 @@ export const isFieldValid = ( return true; }; + +export const isCollapseFunction = (candidate: string | undefined): candidate is CollapseFunction => + Boolean(candidate && CollapseFunctions.includes(candidate as CollapseFunction)); diff --git a/src/plugins/visualizations/public/convert_to_lens/utils.ts b/src/plugins/visualizations/public/convert_to_lens/utils.ts index 0cab4f698fb2f..ba05d29cdeea9 100644 --- a/src/plugins/visualizations/public/convert_to_lens/utils.ts +++ b/src/plugins/visualizations/public/convert_to_lens/utils.ts @@ -8,7 +8,13 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/public'; -import { AggBasedColumn, SchemaConfig, SupportedAggregation } from '../../common'; +import { + AggBasedColumn, + CollapseFunction, + isCollapseFunction, + SchemaConfig, + SupportedAggregation, +} from '../../common'; import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets'; import { isSiblingPipeline } from '../../common/convert_to_lens/lib/utils'; import { BucketColumn } from '../../common/convert_to_lens/lib'; @@ -30,7 +36,7 @@ export const getBucketCollapseFn = ( customBucketsMap: Record, metricColumns: AggBasedColumn[] ) => { - const collapseFnMap: Record = { + const collapseFnMap: Record = { min: [], max: [], sum: [], @@ -45,8 +51,11 @@ export const getBucketCollapseFn = ( const collapseFn = metrics .find((m) => m.aggId === metricColumn.meta.aggId) ?.aggType.split('_')[0]; - if (collapseFn) { - collapseFnMap[collapseFn].push(bucket.columnId); + + if (isCollapseFunction(collapseFn)) { + if (collapseFn) { + collapseFnMap[collapseFn].push(bucket.columnId); + } } }); }); diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index 0a7468a5b9be4..c3f357ea3875b 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -217,6 +217,27 @@ export class InspectorService extends FtrService { await this.openInspectorView('Requests'); } + /** + * Check how many tables are being shown in the inspector. + * @returns + */ + public async getNumberOfTables(): Promise { + const chooserDataTestId = 'inspectorTableChooser'; + const menuDataTestId = 'inspectorTableChooserMenuPanel'; + + if (!(await this.testSubjects.exists(chooserDataTestId))) { + return 1; + } + + return await this.retry.try(async () => { + await this.testSubjects.click(chooserDataTestId); + const menu = await this.testSubjects.find(menuDataTestId); + return ( + await menu.findAllByCssSelector(`[data-test-subj="${menuDataTestId}"] .euiContextMenuItem`) + ).length; + }); + } + /** * Returns the selected option value from combobox */ diff --git a/x-pack/plugins/lens/common/expressions/collapse/index.ts b/x-pack/plugins/lens/common/expressions/collapse/index.ts index bd8df507c95e8..43874859411fc 100644 --- a/x-pack/plugins/lens/common/expressions/collapse/index.ts +++ b/x-pack/plugins/lens/common/expressions/collapse/index.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { CollapseExpressionFunction } from './types'; -type CollapseFunction = 'sum' | 'avg' | 'min' | 'max'; +export type CollapseFunction = 'sum' | 'avg' | 'min' | 'max'; export interface CollapseArgs { by?: string[]; metric?: string[]; diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts index a7592f3f179b3..f955cc1dfa2cb 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts @@ -10,6 +10,7 @@ import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring'; import type { CustomPaletteState } from '@kbn/charts-plugin/common'; import type { ExpressionFunctionDefinition, DatatableColumn } from '@kbn/expressions-plugin/common'; import type { SortingHint } from '../..'; +import { CollapseFunction } from '../collapse'; export type LensGridDirection = 'none' | Direction; @@ -43,7 +44,7 @@ export interface ColumnState { colorMode?: 'none' | 'cell' | 'text'; summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max'; summaryLabel?: string; - collapseFn?: string; + collapseFn?: CollapseFunction; } export type DatatableColumnResult = ColumnState & { type: 'lens_datatable_column' }; diff --git a/x-pack/plugins/lens/common/layer_types.ts b/x-pack/plugins/lens/common/layer_types.ts new file mode 100644 index 0000000000000..18f392481241d --- /dev/null +++ b/x-pack/plugins/lens/common/layer_types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const layerTypes = { + DATA: 'data', + REFERENCELINE: 'referenceLine', + ANNOTATIONS: 'annotations', + METRIC_TRENDLINE: 'metricTrendline', +} as const; diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index c3410f9860173..9056e58eef1eb 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -11,9 +11,10 @@ import type { $Values } from '@kbn/utility-types'; import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type { ColorMode } from '@kbn/charts-plugin/common'; -import { LayerTypes } from '@kbn/expression-xy-plugin/common'; import type { LegendSize } from '@kbn/visualizations-plugin/common'; import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; +import { layerTypes } from './layer_types'; +import { CollapseFunction } from './expressions'; export type { OriginalColumn } from './expressions/map_to_columns'; @@ -39,7 +40,7 @@ export interface PersistableFilter extends Filter { export type SortingHint = 'version'; -export type LayerType = typeof LayerTypes[keyof typeof LayerTypes]; +export type LayerType = typeof layerTypes[keyof typeof layerTypes]; export type ValueLabelConfig = 'hide' | 'show'; @@ -59,7 +60,7 @@ export interface SharedPieLayerState { primaryGroups: string[]; secondaryGroups?: string[]; metric?: string; - collapseFns?: Record; + collapseFns?: Record; numberDisplay: NumberDisplayType; categoryDisplay: CategoryDisplayType; legendDisplay: LegendDisplayType; diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts index b185697021105..c6f97c9670031 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts @@ -75,6 +75,7 @@ describe('getLayerMetaInfo', () => { isStaticValue: false, sortingHint: undefined, hasTimeShift: true, + hasReducedTimeRange: true, })), getTableSpec: jest.fn(), getVisualDefaults: jest.fn(), @@ -82,6 +83,7 @@ describe('getLayerMetaInfo', () => { getMaxPossibleNumValues: jest.fn(), getFilters: jest.fn(), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); expect( @@ -101,6 +103,7 @@ describe('getLayerMetaInfo', () => { getMaxPossibleNumValues: jest.fn(), getFilters: jest.fn(() => ({ error: 'filters error' })), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); expect( @@ -128,6 +131,7 @@ describe('getLayerMetaInfo', () => { getMaxPossibleNumValues: jest.fn(), getFilters: jest.fn(() => ({ error: 'filters error' })), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); // both capabilities should be enabled to enable discover @@ -180,6 +184,7 @@ describe('getLayerMetaInfo', () => { }, disabled: { kuery: [], lucene: [] }, })), + hasDefaultTimeField: jest.fn(() => true), }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); const { error, meta } = getLayerMetaInfo( diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts index 1bff858194a36..82c0a153668a4 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.test.ts @@ -88,7 +88,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { target: mockedDndOperations.notFiltering, state, setState, - dimensionGroups: [], + targetLayerDimensionGroups: [], indexPatterns: mockDataViews(), }; @@ -285,7 +285,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { onDrop({ ...defaultProps, source: mockedDraggedField, - dimensionGroups, + targetLayerDimensionGroups: dimensionGroups, dropType: 'field_add', target: { ...defaultProps.target, @@ -768,8 +768,9 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...state.layers, first: { ...state.layers.first, - columnOrder: ['col2'], + columnOrder: ['col1', 'col2'], columns: { + col1: state.layers.first.columns.col1, col2: state.layers.first.columns.col1, }, }, @@ -803,10 +804,10 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - incompleteColumns: {}, - columnOrder: ['col1', 'col3'], + columnOrder: ['col1', 'col2', 'col3'], columns: { col1: testState.layers.first.columns.col2, + col2: testState.layers.first.columns.col2, col3: testState.layers.first.columns.col3, }, }, @@ -879,7 +880,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, source: mockedDndOperations.bucket, state: testState, - dimensionGroups, + targetLayerDimensionGroups: dimensionGroups, dropType: 'move_compatible', }); @@ -890,11 +891,11 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - incompleteColumns: {}, - columnOrder: ['newCol', 'col1', 'col3', 'col4'], + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], columns: { newCol: testState.layers.first.columns.col2, col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, col3: testState.layers.first.columns.col3, col4: testState.layers.first.columns.col4, }, @@ -918,7 +919,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, source: mockedDndOperations.bucket, state: testState, - dimensionGroups, + targetLayerDimensionGroups: dimensionGroups, dropType: 'duplicate_compatible', }); @@ -957,7 +958,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, source: mockedDndOperations.bucket2, state: testState, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -972,11 +973,11 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - incompleteColumns: {}, - columnOrder: ['col1', 'col2', 'col4'], + columnOrder: ['col1', 'col2', 'col3', 'col4'], columns: { col1: testState.layers.first.columns.col3, col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, col4: testState.layers.first.columns.col4, }, }, @@ -1028,7 +1029,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, }, }, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1043,11 +1044,11 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - incompleteColumns: {}, - columnOrder: ['col1', 'col2', 'col4', 'col5', 'col6'], + columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'], columns: { col1: testState.layers.first.columns.col3, col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, col4: testState.layers.first.columns.col4, col5: expect.objectContaining({ dataType: 'number', @@ -1085,7 +1086,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { columnId: 'col1', groupId: 'a', }, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1128,7 +1129,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { columnId: 'newCol', groupId: 'b', }, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1142,9 +1143,9 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - incompleteColumns: {}, - columnOrder: ['col2', 'col3', 'newCol', 'col4'], + columnOrder: ['col1', 'col2', 'col3', 'newCol', 'col4'], columns: { + col1: testState.layers.first.columns.col1, newCol: testState.layers.first.columns.col1, col2: testState.layers.first.columns.col2, col3: testState.layers.first.columns.col3, @@ -1171,7 +1172,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, source: mockedDndOperations.metric, state: testState, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1214,7 +1215,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { groupId: 'a', filterOperations: (op: OperationMetadata) => op.dataType === 'number', }, - dimensionGroups: [ + targetLayerDimensionGroups: [ // a and b are ordered in reverse visually, but nesting order keeps them in place for column order { ...dimensionGroups[1], nestingOrder: 1 }, { ...dimensionGroups[0], nestingOrder: 0 }, @@ -1264,7 +1265,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { columnId: 'newCol', groupId: 'a', }, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1278,7 +1279,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3'], + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], columns: { col1: testState.layers.first.columns.col1, newCol: expect.objectContaining({ @@ -1287,6 +1288,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }), col2: testState.layers.first.columns.col2, col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, }, incompleteColumns: {}, }, @@ -1311,7 +1313,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { columnId: 'newCol', groupId: 'a', }, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1359,7 +1361,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { columnId: 'col2', groupId: 'b', }, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1373,7 +1375,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...testState.layers, first: { ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3'], + columnOrder: ['col1', 'col2', 'col3', 'col4'], columns: { col1: testState.layers.first.columns.col1, col2: { @@ -1392,6 +1394,13 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, }, col3: testState.layers.first.columns.col3, + col4: { + dataType: 'number', + isBucketed: false, + label: 'Median of bytes', + operationType: 'median', + sourceField: 'bytes', + }, }, incompleteColumns: {}, }, @@ -1416,7 +1425,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { source: mockedDndOperations.metricC, dropType: 'swap_compatible', state: testState, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1459,7 +1468,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { dropType: 'swap_incompatible', source: mockedDndOperations.metricC, state: testState, - dimensionGroups: [ + targetLayerDimensionGroups: [ { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, { ...dimensionGroups[2] }, @@ -1575,7 +1584,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { layerId: 'second', indexPatternId: 'indexPattern1', }, - dimensionGroups: defaultDimensionGroups, + targetLayerDimensionGroups: defaultDimensionGroups, dropType: 'move_compatible', }; jest.clearAllMocks(); @@ -1592,10 +1601,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...props.state, layers: { ...props.state.layers, - first: { - ...mockedLayers.emptyLayer(), - incompleteColumns: {}, - }, second: { columnOrder: ['col2', 'col3', 'col4', 'newCol', 'col5'], columns: { @@ -1677,10 +1682,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...props.state, layers: { ...props.state.layers, - first: { - ...mockedLayers.emptyLayer(), - incompleteColumns: {}, - }, second: { columnOrder: ['col2', 'col3', 'col4', 'col5'], columns: { @@ -1781,10 +1782,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...props.state, layers: { ...props.state.layers, - first: { - ...mockedLayers.emptyLayer(), - incompleteColumns: {}, - }, second: { columnOrder: ['col2', 'col3', 'col4', 'col5'], columns: { @@ -1826,10 +1823,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...props.state, layers: { ...props.state.layers, - first: { - ...mockedLayers.emptyLayer(), - incompleteColumns: {}, - }, second: { columnOrder: ['col2', 'col3', 'col4', 'col5', 'newCol'], columns: { @@ -1993,9 +1986,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { expect(props.setState).toBeCalledTimes(1); expect(props.setState).toHaveBeenCalledWith({ ...props.state, - layers: { - ...props.state.layers, - first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} }, + layers: expect.objectContaining({ second: { ...props.state.layers.second, incompleteColumns: {}, @@ -2021,7 +2012,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, }, }, - }, + }), }); }); it('combine_incompatible: allows dropping to combine to multiterms', () => { @@ -2058,9 +2049,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { expect(props.setState).toBeCalledTimes(1); expect(props.setState).toHaveBeenCalledWith({ ...props.state, - layers: { - ...props.state.layers, - first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} }, + layers: expect.objectContaining({ second: { ...props.state.layers.second, incompleteColumns: {}, @@ -2086,7 +2075,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { }, }, }, - }, + }), }); }); }); @@ -2094,7 +2083,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { let props: DatasourceDimensionDropHandlerProps; beforeEach(() => { props = { - dimensionGroups: defaultDimensionGroups, + targetLayerDimensionGroups: defaultDimensionGroups, setState: jest.fn(), dropType: 'move_compatible', @@ -2181,10 +2170,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...props.state, layers: { ...props.state.layers, - first: { - ...mockedLayers.emptyLayer(), - incompleteColumns: {}, - }, second: { columnOrder: ['second', 'secondX0', 'newColumnX0', 'newColumn'], columns: { @@ -2240,10 +2225,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => { ...props.state, layers: { ...props.state.layers, - first: { - ...mockedLayers.emptyLayer(), - incompleteColumns: {}, - }, second: { columnOrder: ['second', 'secondX0'], columns: { diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.ts index ad575d3c53947..48a24405c470d 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/droppable/on_drop_handler.ts @@ -17,14 +17,12 @@ import { } from '../../../../types'; import { insertOrReplaceColumn, - deleteColumn, getColumnOrder, reorderByGroups, copyColumn, hasOperationSupportForMultipleFields, getOperationHelperForMultipleFields, replaceColumn, - deleteColumnInLayers, } from '../../operations'; import { mergeLayer, mergeLayers } from '../../state_helpers'; import { getNewOperation, getField } from './get_drop_props'; @@ -39,7 +37,7 @@ interface DropHandlerProps { forceRender?: boolean; } >; - dimensionGroups: VisualizationDimensionGroupConfig[]; + targetLayerDimensionGroups: VisualizationDimensionGroupConfig[]; dropType?: DropType; source: T; target: DataViewDragDropOperation; @@ -89,16 +87,24 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps ['field_add', 'field_replace', 'field_combine'].includes(dropType); function onFieldDrop(props: DropHandlerProps, shouldAddField?: boolean) { - const { setState, state, source, target, dimensionGroups, indexPatterns } = props; + const { setState, state, source, target, targetLayerDimensionGroups, indexPatterns } = props; - const prioritizedOperation = dimensionGroups.find( + const prioritizedOperation = targetLayerDimensionGroups.find( (g) => g.groupId === target.groupId )?.prioritizedOperation; @@ -168,7 +174,7 @@ function onFieldDrop(props: DropHandlerProps, shouldAddField?: boo indexPattern, op: newOperation, field, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, targetGroup: target.groupId, shouldCombineField: shouldAddField, initialParams, @@ -177,45 +183,42 @@ function onFieldDrop(props: DropHandlerProps, shouldAddField?: boo return true; } -function onMoveCompatible( - { setState, state, source, target, dimensionGroups }: DropHandlerProps, - shouldDeleteSource?: boolean -) { - const modifiedLayers = copyColumn({ +function onMoveCompatible({ + setState, + state, + source, + target, + targetLayerDimensionGroups, +}: DropHandlerProps) { + let modifiedLayers = copyColumn({ layers: state.layers, target, source, - shouldDeleteSource, }); - if (target.layerId === source.layerId) { - const updatedColumnOrder = reorderByGroups( - dimensionGroups, - getColumnOrder(modifiedLayers[target.layerId]), - target.groupId, - target.columnId - ); + const updatedColumnOrder = reorderByGroups( + targetLayerDimensionGroups, + getColumnOrder(modifiedLayers[target.layerId]), + target.groupId, + target.columnId + ); - const newLayer = { + modifiedLayers = { + ...modifiedLayers, + [target.layerId]: { ...modifiedLayers[target.layerId], columnOrder: updatedColumnOrder, columns: modifiedLayers[target.layerId].columns, - }; - - // Time to replace - setState( - mergeLayer({ - state, - layerId: target.layerId, - newLayer, - }) - ); - return true; - } else { - setState(mergeLayers({ state, newLayers: modifiedLayers })); + }, + }; - return true; - } + setState( + mergeLayers({ + state, + newLayers: modifiedLayers, + }) + ); + return true; } function onReorder({ @@ -250,17 +253,14 @@ function onReorder({ return true; } -function onMoveIncompatible( - { - setState, - state, - source, - dimensionGroups, - target, - indexPatterns, - }: DropHandlerProps, - shouldDeleteSource?: boolean -) { +function onMoveIncompatible({ + setState, + state, + source, + targetLayerDimensionGroups, + target, + indexPatterns, +}: DropHandlerProps) { const targetLayer = state.layers[target.layerId]; const targetColumn = targetLayer.columns[target.columnId] || null; const sourceLayer = state.layers[source.layerId]; @@ -272,22 +272,14 @@ function onMoveIncompatible( return false; } - const outputSourceLayer = shouldDeleteSource - ? deleteColumn({ - layer: sourceLayer, - columnId: source.columnId, - indexPattern, - }) - : sourceLayer; - if (target.layerId === source.layerId) { const newLayer = insertOrReplaceColumn({ - layer: outputSourceLayer, + layer: sourceLayer, columnId: target.columnId, indexPattern, op: newOperation, field: sourceField, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, targetGroup: target.groupId, shouldResetLabel: true, }); @@ -306,7 +298,7 @@ function onMoveIncompatible( indexPattern, op: newOperation, field: sourceField, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, targetGroup: target.groupId, shouldResetLabel: true, }); @@ -314,7 +306,7 @@ function onMoveIncompatible( mergeLayers({ state, newLayers: { - [source.layerId]: outputSourceLayer, + [source.layerId]: sourceLayer, [target.layerId]: outputTargetLayer, }, }) @@ -327,7 +319,7 @@ function onSwapIncompatible({ setState, state, source, - dimensionGroups, + targetLayerDimensionGroups, target, indexPatterns, }: DropHandlerProps) { @@ -354,7 +346,7 @@ function onSwapIncompatible({ indexPattern, op: newOperationForSource, field: sourceField, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, shouldResetLabel: true, }); @@ -365,7 +357,7 @@ function onSwapIncompatible({ indexPattern, op: newOperationForTarget, field: targetField, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, targetGroup: source.groupId, shouldResetLabel: true, }); @@ -384,7 +376,7 @@ function onSwapIncompatible({ indexPattern, op: newOperationForTarget, field: targetField, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, targetGroup: source.groupId, shouldResetLabel: true, }); @@ -413,7 +405,7 @@ function onSwapCompatible({ setState, state, source, - dimensionGroups, + targetLayerDimensionGroups, target, }: DropHandlerProps) { if (target.layerId === source.layerId) { @@ -426,7 +418,7 @@ function onSwapCompatible({ let updatedColumnOrder = swapColumnOrder(layer.columnOrder, source.columnId, target.columnId); updatedColumnOrder = reorderByGroups( - dimensionGroups, + targetLayerDimensionGroups, updatedColumnOrder, target.groupId, target.columnId @@ -445,6 +437,7 @@ function onSwapCompatible({ return true; } else { + // TODO why not reorderByGroups for both columns? Are they already in that order? const newTargetLayer = copyColumn({ layers: state.layers, target, @@ -478,7 +471,7 @@ function onCombine({ setState, source, target, - dimensionGroups, + targetLayerDimensionGroups, indexPatterns, }: DropHandlerProps) { const targetLayer = state.layers[target.layerId]; @@ -509,16 +502,14 @@ function onCombine({ indexPattern, op: targetColumn.operationType, field: targetField, - visualizationGroups: dimensionGroups, + visualizationGroups: targetLayerDimensionGroups, targetGroup: target.groupId, initialParams, shouldCombineField: true, }); - const newLayers = deleteColumnInLayers({ - layers: { ...state.layers, [target.layerId]: outputTargetLayer }, - source, - }); - setState(mergeLayers({ state, newLayers })); + setState( + mergeLayers({ state, newLayers: { ...state.layers, [target.layerId]: outputTargetLayer } }) + ); return true; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts index 6021cdc1e821e..e8ac68460beb9 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts @@ -1632,7 +1632,7 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; - expect(FormBasedDatasource.insertLayer(state, 'newLayer')).toEqual({ + expect(FormBasedDatasource.insertLayer(state, 'newLayer', ['link-to-id'])).toEqual({ ...state, layers: { ...state.layers, @@ -1640,6 +1640,7 @@ describe('IndexPattern Data Source', () => { indexPatternId: '1', columnOrder: [], columns: {}, + linkToLayers: ['link-to-id'], }, }, }); @@ -1867,6 +1868,7 @@ describe('IndexPattern Data Source', () => { isBucketed: true, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, } as OperationDescriptor); }); @@ -2722,6 +2724,43 @@ describe('IndexPattern Data Source', () => { expect(publicAPI.getMaxPossibleNumValues('non-existant')).toEqual(null); }); }); + + test('hasDefaultTimeField', () => { + const indexPatternWithDefaultTimeField = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), + spec: {}, + isPersisted: true, + }; + + const indexPatternWithoutDefaultTimeField = { + ...indexPatternWithDefaultTimeField, + timeFieldName: '', + }; + + expect( + FormBasedDatasource.getPublicAPI({ + state: baseState, + layerId: 'first', + indexPatterns: { + 1: indexPatternWithDefaultTimeField, + }, + }).hasDefaultTimeField() + ).toBe(true); + expect( + FormBasedDatasource.getPublicAPI({ + state: baseState, + layerId: 'first', + indexPatterns: { + 1: indexPatternWithoutDefaultTimeField, + }, + }).hasDefaultTimeField() + ).toBe(false); + }); }); describe('#getErrorMessages', () => { @@ -3218,6 +3257,7 @@ describe('IndexPattern Data Source', () => { FormBasedDatasource.initializeDimension!(state, 'first', indexPatterns, { columnId: 'newStatic', groupId: 'a', + visualizationGroups: [], }) ).toBe(state); }); @@ -3246,6 +3286,7 @@ describe('IndexPattern Data Source', () => { columnId: 'newStatic', groupId: 'a', staticValue: 0, // use a falsy value to check also this corner case + visualizationGroups: [], }) ).toEqual({ ...state, @@ -3272,6 +3313,272 @@ describe('IndexPattern Data Source', () => { }, }); }); + + it('should add a new date histogram column if autoTimeField is passed', () => { + const state = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + }, + }, + }, + } as FormBasedPrivateState; + expect( + FormBasedDatasource.initializeDimension!(state, 'first', indexPatterns, { + columnId: 'newTime', + groupId: 'a', + autoTimeField: true, + visualizationGroups: [], + }) + ).toEqual({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + incompleteColumns: {}, + columnOrder: ['newTime', 'metric'], + columns: { + ...state.layers.first.columns, + newTime: { + dataType: 'date', + isBucketed: true, + label: 'timestampLabel', + operationType: 'date_histogram', + params: { dropPartials: false, includeEmptyRows: true, interval: 'auto' }, + scale: 'interval', + sourceField: 'timestamp', + }, + }, + }, + }, + }); + }); + }); + + describe('#syncColumns', () => { + it('copies linked columns', () => { + const links: Parameters[0]['links'] = [ + { + from: { + columnId: 'col1', + layerId: 'first', + groupId: 'foo', + }, + to: { + columnId: 'col1', + layerId: 'second', + groupId: 'foo', + }, + }, + { + from: { + columnId: 'col2', + layerId: 'first', + groupId: 'foo', + }, + to: { + columnId: 'new-col', + layerId: 'second', + groupId: 'foo', + }, + }, + ]; + + const newState = FormBasedDatasource.syncColumns({ + state: { + currentIndexPatternId: 'foo', + layers: { + first: { + indexPatternId: 'foo', + columnOrder: [], + columns: { + col1: { + operationType: 'sum', + label: '', + dataType: 'number', + isBucketed: false, + sourceField: 'field1', + customLabel: false, + timeScale: 'd', + } as SumIndexPatternColumn, + col2: { + sourceField: 'field2', + operationType: 'count', + customLabel: false, + timeScale: 'h', + } as CountIndexPatternColumn, + }, + }, + second: { + indexPatternId: 'foo', + columnOrder: [], + columns: { + col1: { + sourceField: 'field1', + operationType: 'count', + customLabel: false, + timeScale: 'd', + } as CountIndexPatternColumn, + }, + }, + }, + }, + links, + indexPatterns, + getDimensionGroups: () => [], + }); + + expect(newState).toMatchInlineSnapshot(` + Object { + "currentIndexPatternId": "foo", + "layers": Object { + "first": Object { + "columnOrder": Array [], + "columns": Object { + "col1": Object { + "customLabel": false, + "dataType": "number", + "isBucketed": false, + "label": "", + "operationType": "sum", + "sourceField": "field1", + "timeScale": "d", + }, + "col2": Object { + "customLabel": false, + "operationType": "count", + "sourceField": "field2", + "timeScale": "h", + }, + }, + "indexPatternId": "foo", + }, + "second": Object { + "columnOrder": Array [ + "col1", + "new-col", + ], + "columns": Object { + "col1": Object { + "customLabel": false, + "dataType": "number", + "isBucketed": false, + "label": "", + "operationType": "sum", + "sourceField": "field1", + "timeScale": "d", + }, + "new-col": Object { + "customLabel": false, + "operationType": "count", + "sourceField": "field2", + "timeScale": "h", + }, + }, + "indexPatternId": "foo", + }, + }, + } + `); + }); + + it('updates terms order by references', () => { + const links: Parameters[0]['links'] = [ + { + from: { + columnId: 'col1FirstLayer', + layerId: 'first', + groupId: 'foo', + }, + to: { + columnId: 'col1SecondLayer', + layerId: 'second', + groupId: 'foo', + }, + }, + { + from: { + columnId: 'col2', + layerId: 'first', + groupId: 'foo', + }, + to: { + columnId: 'new-col', + layerId: 'second', + groupId: 'foo', + }, + }, + ]; + + const newState = FormBasedDatasource.syncColumns({ + state: { + currentIndexPatternId: 'foo', + layers: { + first: { + indexPatternId: 'foo', + columnOrder: [], + columns: { + col1FirstLayer: { + operationType: 'sum', + label: '', + dataType: 'number', + isBucketed: false, + sourceField: 'field1', + customLabel: false, + timeScale: 'd', + } as SumIndexPatternColumn, + col2: { + operationType: 'terms', + sourceField: 'field2', + label: '', + dataType: 'number', + isBucketed: false, + params: { + orderBy: { + columnId: 'col1FirstLayer', + type: 'column', + }, + }, + } as TermsIndexPatternColumn, + }, + }, + second: { + indexPatternId: 'foo', + columnOrder: [], + columns: { + col1SecondLayer: { + sourceField: 'field1', + operationType: 'count', + customLabel: false, + timeScale: 'd', + } as CountIndexPatternColumn, + }, + }, + }, + }, + links, + indexPatterns, + getDimensionGroups: () => [], + }); + + expect( + (newState.layers.second.columns['new-col'] as TermsIndexPatternColumn).params.orderBy + ).toEqual({ + type: 'column', + columnId: 'col1SecondLayer', + }); + }); }); describe('#isEqual', () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 67d3acda068c9..d41218f785d9b 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -80,9 +80,14 @@ import { operationDefinitionMap, TermsIndexPatternColumn, } from './operations'; -import { getReferenceRoot } from './operations/layer_helpers'; -import { FormBasedPrivateState, FormBasedPersistedState } from './types'; -import { mergeLayer } from './state_helpers'; +import { + copyColumn, + getColumnOrder, + getReferenceRoot, + reorderByGroups, +} from './operations/layer_helpers'; +import { FormBasedPrivateState, FormBasedPersistedState, DataViewDragDropOperation } from './types'; +import { mergeLayer, mergeLayers } from './state_helpers'; import { Datasource, VisualizeEditorContext } from '../../types'; import { deleteColumn, isReferenced } from './operations'; import { GeoFieldWorkspacePanel } from '../../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; @@ -91,6 +96,7 @@ import { getStateTimeShiftWarningMessages } from './time_shift_utils'; import { getPrecisionErrorWarningMessages } from './utils'; import { DOCUMENT_FIELD_NAME } from '../../../common/constants'; import { isColumnOfType } from './operations/definitions/helpers'; +import { FormBasedLayer } from '../..'; export type { OperationType, GenericIndexPatternColumn } from './operations'; export { deleteColumn } from './operations'; @@ -99,7 +105,7 @@ export function columnToOperation( uniqueLabel?: string, dataView?: IndexPattern ): OperationDescriptor { - const { dataType, label, isBucketed, scale, operationType, timeShift } = column; + const { dataType, label, isBucketed, scale, operationType, timeShift, reducedTimeRange } = column; const fieldTypes = 'sourceField' in column ? dataView?.getFieldByName(column.sourceField)?.esTypes : undefined; return { @@ -113,6 +119,7 @@ export function columnToOperation( ? 'version' : undefined, hasTimeShift: Boolean(timeShift), + hasReducedTimeRange: Boolean(reducedTimeRange), interval: isColumnOfType('date_histogram', column) ? column.params.interval : undefined, @@ -180,12 +187,16 @@ export function getFormBasedDatasource({ return extractReferences(state); }, - insertLayer(state: FormBasedPrivateState, newLayerId: string) { + insertLayer( + state: FormBasedPrivateState, + newLayerId: string, + linkToLayers: string[] | undefined + ) { return { ...state, layers: { ...state.layers, - [newLayerId]: blankLayer(state.currentIndexPatternId), + [newLayerId]: blankLayer(state.currentIndexPatternId, linkToLayers), }, }; }, @@ -219,7 +230,7 @@ export function getFormBasedDatasource({ ...state, layers: { ...state.layers, - [layerId]: blankLayer(state.currentIndexPatternId), + [layerId]: blankLayer(state.currentIndexPatternId, state.layers[layerId].linkToLayers), }, }; }, @@ -241,26 +252,123 @@ export function getFormBasedDatasource({ }); }, - initializeDimension(state, layerId, indexPatterns, { columnId, groupId, staticValue }) { + initializeDimension( + state, + layerId, + indexPatterns, + { columnId, groupId, staticValue, autoTimeField, visualizationGroups } + ) { const indexPattern = indexPatterns[state.layers[layerId]?.indexPatternId]; - if (staticValue == null) { - return state; + let ret = state; + + if (staticValue != null) { + ret = mergeLayer({ + state, + layerId, + newLayer: insertNewColumn({ + layer: state.layers[layerId], + op: 'static_value', + columnId, + field: undefined, + indexPattern, + visualizationGroups, + initialParams: { params: { value: staticValue } }, + targetGroup: groupId, + }), + }); } - return mergeLayer({ + if (autoTimeField && indexPattern.timeFieldName) { + ret = mergeLayer({ + state, + layerId, + newLayer: insertNewColumn({ + layer: state.layers[layerId], + op: 'date_histogram', + columnId, + field: indexPattern.fields.find((field) => field.name === indexPattern.timeFieldName), + indexPattern, + visualizationGroups, + targetGroup: groupId, + }), + }); + } + + return ret; + }, + + syncColumns({ state, links, indexPatterns, getDimensionGroups }) { + let modifiedLayers: Record = state.layers; + + links.forEach((link) => { + const source: DataViewDragDropOperation = { + ...link.from, + dataView: indexPatterns[modifiedLayers[link.from.layerId]?.indexPatternId], + filterOperations: () => true, + }; + + const target: DataViewDragDropOperation = { + ...link.to, + dataView: indexPatterns[modifiedLayers[link.to.layerId]?.indexPatternId], + filterOperations: () => true, + }; + + modifiedLayers = copyColumn({ + layers: modifiedLayers, + target, + source, + }); + + const updatedColumnOrder = reorderByGroups( + getDimensionGroups(target.layerId), + getColumnOrder(modifiedLayers[target.layerId]), + target.groupId, + target.columnId + ); + + modifiedLayers = { + ...modifiedLayers, + [target.layerId]: { + ...modifiedLayers[target.layerId], + columnOrder: updatedColumnOrder, + columns: modifiedLayers[target.layerId].columns, + }, + }; + }); + + const newState = mergeLayers({ state, - layerId, - newLayer: insertNewColumn({ - layer: state.layers[layerId], - op: 'static_value', - columnId, - field: undefined, - indexPattern, - visualizationGroups: [], - initialParams: { params: { value: staticValue } }, - targetGroup: groupId, - }), + newLayers: modifiedLayers, }); + + links + .filter((link) => + isColumnOfType( + 'terms', + newState.layers[link.from.layerId].columns[link.from.columnId] + ) + ) + .forEach(({ from, to }) => { + const fromColumn = newState.layers[from.layerId].columns[ + from.columnId + ] as TermsIndexPatternColumn; + if (fromColumn.params.orderBy.type === 'column') { + const fromOrderByColumnId = fromColumn.params.orderBy.columnId; + const orderByColumnLink = links.find( + ({ from: { columnId } }) => columnId === fromOrderByColumnId + ); + + if (orderByColumnLink) { + // order the synced column by the dimension which is linked to the column that the original column was ordered by + const toColumn = newState.layers[to.layerId].columns[ + to.columnId + ] as TermsIndexPatternColumn; + toColumn.params.orderBy = { type: 'column', columnId: orderByColumnLink.to.columnId }; + } + } + }); + + return newState; }, getSelectedFields(state) { @@ -497,9 +605,18 @@ export function getFormBasedDatasource({ onRefreshIndexPattern, onIndexPatternChange(state, indexPatterns, indexPatternId, layerId) { if (layerId) { + const layersToChange = [ + layerId, + ...Object.entries(state.layers) + .map(([possiblyLinkedId, layer]) => + layer.linkToLayers?.includes(layerId) ? possiblyLinkedId : '' + ) + .filter(Boolean), + ]; + return changeLayerIndexPattern({ indexPatternId, - layerId, + layerIds: layersToChange, state, replaceIfPossible: true, storage, @@ -620,6 +737,7 @@ export function getFormBasedDatasource({ } return null; }, + hasDefaultTimeField: () => Boolean(indexPatterns[layer.indexPatternId].timeFieldName), }; }, getDatasourceSuggestionsForField(state, draggedField, filterLayers, indexPatterns) { @@ -799,9 +917,10 @@ export function getFormBasedDatasource({ return formBasedDatasource; } -function blankLayer(indexPatternId: string) { +function blankLayer(indexPatternId: string, linkToLayers?: string[]): FormBasedLayer { return { indexPatternId, + linkToLayers, columns: {}, columnOrder: [], }; diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx index d7a544a723e04..2489659f0da55 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx @@ -1283,6 +1283,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, { @@ -1294,6 +1295,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ratio', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, ], @@ -1373,6 +1375,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, { @@ -1384,6 +1387,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ratio', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, ], @@ -2198,6 +2202,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, ], @@ -2222,6 +2227,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, ], @@ -2272,6 +2278,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'interval', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: 'auto', }, }, @@ -2284,6 +2291,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ratio', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -2349,6 +2357,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ordinal', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -2361,6 +2370,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'interval', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: 'auto', }, }, @@ -2373,6 +2383,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ratio', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -2459,6 +2470,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ordinal', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -2471,6 +2483,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'interval', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: 'auto', }, }, @@ -2483,6 +2496,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ratio', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -2592,6 +2606,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'ordinal', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -2604,6 +2619,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'interval', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: 'auto', }, }, @@ -2616,6 +2632,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -3123,6 +3140,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, { @@ -3134,6 +3152,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }, }, ], @@ -3201,6 +3220,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: 'interval', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: 'auto', }, }, @@ -3213,6 +3233,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -3225,6 +3246,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -3291,6 +3313,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: 'auto', }, }, @@ -3303,6 +3326,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, @@ -3315,6 +3339,7 @@ describe('IndexPattern Data Source suggestions', () => { scale: undefined, isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, interval: undefined, }, }, diff --git a/x-pack/plugins/lens/public/datasources/form_based/loader.test.ts b/x-pack/plugins/lens/public/datasources/form_based/loader.test.ts index 896fbd0e6404f..924110080329c 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/loader.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/loader.test.ts @@ -401,7 +401,7 @@ describe('loader', () => { }); describe('changeLayerIndexPattern', () => { - it('loads the index pattern and then changes the specified layer', async () => { + it('loads the index pattern and then changes the specified layers', async () => { const state: FormBasedPrivateState = { currentIndexPatternId: '1', layers: { @@ -434,7 +434,7 @@ describe('loader', () => { const newState = changeLayerIndexPattern({ state, indexPatternId: '2', - layerId: 'l1', + layerIds: ['l0', 'l1'], storage, indexPatterns: sampleIndexPatterns, replaceIfPossible: true, @@ -444,9 +444,9 @@ describe('loader', () => { currentIndexPatternId: '2', layers: { l0: { - columnOrder: ['col1'], + columnOrder: [], columns: {}, - indexPatternId: '1', + indexPatternId: '2', }, l1: { columnOrder: ['col2'], diff --git a/x-pack/plugins/lens/public/datasources/form_based/loader.ts b/x-pack/plugins/lens/public/datasources/form_based/loader.ts index 6aa021a6f363f..54c9efe6f1041 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/loader.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/loader.ts @@ -253,25 +253,34 @@ export function triggerActionOnIndexPatternChange({ export function changeLayerIndexPattern({ indexPatternId, indexPatterns, - layerId, + layerIds, state, replaceIfPossible, storage, }: { indexPatternId: string; - layerId: string; + layerIds: string[]; state: FormBasedPrivateState; replaceIfPossible?: boolean; storage: IStorageWrapper; indexPatterns: Record; }) { setLastUsedIndexPatternId(storage, indexPatternId); + + const newLayers = { + ...state.layers, + }; + + layerIds.forEach((layerId) => { + newLayers[layerId] = updateLayerIndexPattern( + state.layers[layerId], + indexPatterns[indexPatternId] + ); + }); + return { ...state, - layers: { - ...state.layers, - [layerId]: updateLayerIndexPattern(state.layers[layerId], indexPatterns[indexPatternId]), - }, + layers: newLayers, currentIndexPatternId: replaceIfPossible ? indexPatternId : state.currentIndexPatternId, }; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/layer_helpers.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/layer_helpers.ts index db9b744cd43b2..9c2b41ce94798 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/layer_helpers.ts @@ -74,34 +74,8 @@ interface ColumnCopy { shouldDeleteSource?: boolean; } -export const deleteColumnInLayers = ({ - layers, - source, -}: { - layers: Record; - source: DataViewDragDropOperation; -}) => ({ - ...layers, - [source.layerId]: deleteColumn({ - layer: layers[source.layerId], - columnId: source.columnId, - indexPattern: source.dataView, - }), -}); - -export function copyColumn({ - layers, - source, - target, - shouldDeleteSource, -}: ColumnCopy): Record { - const outputLayers = createCopiedColumn(layers, target, source); - return shouldDeleteSource - ? deleteColumnInLayers({ - layers: outputLayers, - source, - }) - : outputLayers; +export function copyColumn({ layers, source, target }: ColumnCopy): Record { + return createCopiedColumn(layers, target, source); } function createCopiedColumn( diff --git a/x-pack/plugins/lens/public/datasources/form_based/types.ts b/x-pack/plugins/lens/public/datasources/form_based/types.ts index 2d33ed0bc1451..3c695d5064e3f 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/types.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/types.ts @@ -51,6 +51,7 @@ export interface FormBasedLayer { columns: Record; // Each layer is tied to the index pattern that created it indexPatternId: string; + linkToLayers?: string[]; // Partial columns represent the temporary invalid states incompleteColumns?: Record; } diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts index 968a1ac1e496c..cef1bfa96b8a5 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts @@ -728,6 +728,7 @@ describe('IndexPattern Data Source', () => { dataType: 'number', isBucketed: false, hasTimeShift: false, + hasReducedTimeRange: false, }); }); diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index f7e78e830669a..afe6368477cc9 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -209,6 +209,12 @@ export function getTextBasedDatasource({ initialContext: context, }; }, + + syncColumns({ state }) { + // TODO implement this for real + return state; + }, + onRefreshIndexPattern() {}, getUsedDataViews: (state) => { @@ -581,6 +587,7 @@ export function getTextBasedDatasource({ label: columnLabelMap[columnId] ?? column?.fieldName, isBucketed: Boolean(column?.meta?.type !== 'number'), hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -606,6 +613,7 @@ export function getTextBasedDatasource({ }, }; }, + hasDefaultTimeField: () => false, }; }, getDatasourceSuggestionsForField(state, draggedField) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx index dee24edcad17e..edb4bd5b9d252 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx @@ -44,10 +44,12 @@ export function AddLayerButton({ if (!visualization.appendLayer || !visualizationState) { return null; } - return visualization.getSupportedLayers?.(visualizationState, layersMeta); + return visualization + .getSupportedLayers?.(visualizationState, layersMeta) + ?.filter(({ canAddViaMenu: hideFromMenu }) => !hideFromMenu); }, [visualization, visualizationState, layersMeta]); - if (supportedLayers == null) { + if (supportedLayers == null || !supportedLayers.length) { return null; } if (supportedLayers.length === 1) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx index 3094a07cf3290..454fe3b191755 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -147,11 +147,23 @@ export interface OnVisDropProps { group?: VisualizationDimensionGroupConfig; } +export function shouldRemoveSource(source: DragDropIdentifier, dropType: DropType) { + return ( + isOperation(source) && + (dropType === 'move_compatible' || + dropType === 'move_incompatible' || + dropType === 'combine_incompatible' || + dropType === 'combine_compatible' || + dropType === 'replace_compatible' || + dropType === 'replace_incompatible') + ); +} + export function onDropForVisualization( props: OnVisDropProps, activeVisualization: Visualization ) { - const { prevState, target, frame, dropType, source, group } = props; + const { prevState, target, frame, source, group } = props; const { layerId, columnId, groupId } = target; const previousColumn = @@ -166,21 +178,5 @@ export function onDropForVisualization( frame, }); - if ( - isOperation(source) && - (dropType === 'move_compatible' || - dropType === 'move_incompatible' || - dropType === 'combine_incompatible' || - dropType === 'combine_compatible' || - dropType === 'replace_compatible' || - dropType === 'replace_incompatible') - ) { - return activeVisualization.removeDimension({ - columnId: source?.columnId, - layerId: source?.layerId, - prevState: newVisState, - frame, - }); - } return newVisState; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 5fbfb0b86e11d..ecab05e70b87d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -374,6 +374,16 @@ describe('ConfigPanel', () => { columnId: 'myColumn', groupId: 'testGroup', staticValue: 100, + visualizationGroups: [ + expect.objectContaining({ + accessors: [], + dataTestSubj: 'mockVisA', + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + }), + ], } ); }); @@ -410,6 +420,16 @@ describe('ConfigPanel', () => { groupId: 'a', columnId: 'newId', staticValue: 100, + visualizationGroups: [ + expect.objectContaining({ + accessors: [], + dataTestSubj: 'mockVisA', + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + }), + ], } ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 6c1c7680d8bd3..41ac0e3359ef6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -13,7 +13,8 @@ import { UPDATE_FILTER_REFERENCES_ACTION, UPDATE_FILTER_REFERENCES_TRIGGER, } from '@kbn/unified-search-plugin/public'; -import { changeIndexPattern } from '../../../state_management/lens_slice'; +import { LayerType } from '../../../../common'; +import { changeIndexPattern, removeDimension } from '../../../state_management/lens_slice'; import { Visualization } from '../../../types'; import { LayerPanel } from './layer_panel'; import { generateId } from '../../../id_generator'; @@ -24,7 +25,7 @@ import { useLensDispatch, removeOrClearLayer, cloneLayer, - addLayer, + addLayer as addLayerAction, updateState, updateDatasourceState, updateVisualizationState, @@ -78,18 +79,20 @@ export function LayerPanels( [activeVisualization, dispatchLens] ); const updateDatasource = useMemo( - () => (datasourceId: string | undefined, newState: unknown) => { - if (datasourceId) { - dispatchLens( - updateDatasourceState({ - updater: (prevState: unknown) => - typeof newState === 'function' ? newState(prevState) : newState, - datasourceId, - clearStagedPreview: false, - }) - ); - } - }, + () => + (datasourceId: string | undefined, newState: unknown, dontSyncLinkedDimensions?: boolean) => { + if (datasourceId) { + dispatchLens( + updateDatasourceState({ + updater: (prevState: unknown) => + typeof newState === 'function' ? newState(prevState) : newState, + datasourceId, + clearStagedPreview: false, + dontSyncLinkedDimensions, + }) + ); + } + }, [dispatchLens] ); const updateDatasourceAsync = useMemo( @@ -157,6 +160,48 @@ export function LayerPanels( [dispatchLens] ); + const onRemoveLayer = useCallback( + (layerToRemoveId: string) => { + const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerToRemoveId]; + const datasourceId = datasourcePublicAPI?.datasourceId; + + if (datasourceId) { + const layerDatasource = datasourceMap[datasourceId]; + const layerDatasourceState = datasourceStates?.[datasourceId]?.state; + const trigger = props.uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER); + const action = props.uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION); + + action?.execute({ + trigger, + fromDataView: layerDatasource.getUsedDataView(layerDatasourceState, layerToRemoveId), + usedDataViews: layerDatasource + .getLayers(layerDatasourceState) + .map((layer) => layerDatasource.getUsedDataView(layerDatasourceState, layer)), + defaultDataView: layerDatasource.getUsedDataView(layerDatasourceState), + } as ActionExecutionContext); + } + + dispatchLens( + removeOrClearLayer({ + visualizationId: activeVisualization.id, + layerId: layerToRemoveId, + layerIds, + }) + ); + removeLayerRef(layerToRemoveId); + }, + [ + activeVisualization.id, + datasourceMap, + datasourceStates, + dispatchLens, + layerIds, + props.framePublicAPI.datasourceLayers, + props.uiActions, + removeLayerRef, + ] + ); + const onChangeIndexPattern = useCallback( async ({ indexPatternId, @@ -183,100 +228,98 @@ export function LayerPanels( }) ); }, - [dispatchLens, props.framePublicAPI.dataViews, props.indexPatternService] + [dispatchLens, props.framePublicAPI.dataViews.indexPatterns, props.indexPatternService] ); + const addLayer = (layerType: LayerType) => { + const layerId = generateId(); + dispatchLens(addLayerAction({ layerId, layerType })); + setNextFocusedLayerId(layerId); + }; + const hideAddLayerButton = query && isOfAggregateQueryType(query); return ( - {layerIds.map((layerId, layerIndex) => ( - { - // avoid state update if the datasource does not support initializeDimension - if ( - activeDatasourceId != null && - datasourceMap[activeDatasourceId]?.initializeDimension - ) { - dispatchLens( - setLayerDefaultDimension({ - layerId, - columnId, - groupId, - }) - ); - } - }} - onCloneLayer={() => { - dispatchLens( - cloneLayer({ - layerId, - }) - ); - }} - onRemoveLayer={() => { - const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerId]; - const datasourceId = datasourcePublicAPI?.datasourceId; - - if (datasourceId) { - const layerDatasource = datasourceMap[datasourceId]; - const layerDatasourceState = datasourceStates?.[datasourceId]?.state; - const trigger = props.uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER); - const action = props.uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION); - - action?.execute({ - trigger, - fromDataView: layerDatasource.getUsedDataView(layerDatasourceState, layerId), - usedDataViews: layerDatasource - .getLayers(layerDatasourceState) - .map((layer) => layerDatasource.getUsedDataView(layerDatasourceState, layer)), - defaultDataView: layerDatasource.getUsedDataView(layerDatasourceState), - } as ActionExecutionContext); - } + {layerIds.map((layerId, layerIndex) => { + const { hidden, groups } = activeVisualization.getConfiguration({ + layerId, + frame: props.framePublicAPI, + state: visualization.state, + }); - dispatchLens( - removeOrClearLayer({ - visualizationId: activeVisualization.id, - layerId, - layerIds, - }) - ); - removeLayerRef(layerId); - }} - toggleFullscreen={toggleFullscreen} - indexPatternService={indexPatternService} - /> - ))} + return ( + !hidden && ( + { + onChangeIndexPattern(args); + const layersToRemove = + activeVisualization.getLayersToRemoveOnIndexPatternChange?.( + visualization.state + ) ?? []; + layersToRemove.forEach((id) => onRemoveLayer(id)); + }} + updateAll={updateAll} + addLayer={addLayer} + isOnlyLayer={ + getRemoveOperation( + activeVisualization, + visualization.state, + layerId, + layerIds.length + ) === 'clear' + } + onEmptyDimensionAdd={(columnId, { groupId }) => { + // avoid state update if the datasource does not support initializeDimension + if ( + activeDatasourceId != null && + datasourceMap[activeDatasourceId]?.initializeDimension + ) { + dispatchLens( + setLayerDefaultDimension({ + layerId, + columnId, + groupId, + }) + ); + } + }} + onCloneLayer={() => { + dispatchLens( + cloneLayer({ + layerId, + }) + ); + }} + onRemoveLayer={onRemoveLayer} + onRemoveDimension={(dimensionProps) => { + const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerId]; + const datasourceId = datasourcePublicAPI?.datasourceId; + dispatchLens(removeDimension({ ...dimensionProps, datasourceId })); + }} + toggleFullscreen={toggleFullscreen} + indexPatternService={indexPatternService} + /> + ) + ); + })} {!hideAddLayerButton && ( { - const layerId = generateId(); - dispatchLens(addLayer({ layerId, layerType })); - setNextFocusedLayerId(layerId); - }} + onAddLayerClick={(layerType) => addLayer(layerType)} /> )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 74394e89f0d63..173378c6fb9ef 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { EuiFormRow } from '@elastic/eui'; -import { FramePublicAPI, Visualization } from '../../../types'; +import { FramePublicAPI, Visualization, VisualizationConfigProps } from '../../../types'; import { LayerPanel } from './layer_panel'; import { ChildDragDropProvider, DragDrop } from '../../../drag_drop'; import { coreMock } from '@kbn/core/public/mocks'; @@ -89,6 +89,7 @@ describe('LayerPanel', () => { return { layerId: 'first', activeVisualization: mockVisualization, + dimensionGroups: mockVisualization.getConfiguration({} as VisualizationConfigProps).groups, datasourceMap: { testDatasource: mockDatasource, }, @@ -99,8 +100,10 @@ describe('LayerPanel', () => { updateAll: jest.fn(), framePublicAPI: frame, isOnlyLayer: true, + addLayer: jest.fn(), onRemoveLayer: jest.fn(), onCloneLayer: jest.fn(), + onRemoveDimension: jest.fn(), dispatch: jest.fn(), core: coreMock.createStart(), layerIndex: 0, @@ -473,9 +476,6 @@ describe('LayerPanel', () => { }); it('should remove the dimension when the datasource marks it as removed', async () => { - const updateAll = jest.fn(); - const updateDatasource = jest.fn(); - mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -489,37 +489,31 @@ describe('LayerPanel', () => { ], }); - const { instance } = await mountWithProvider( - , - { - preloadedState: { - datasourceStates: { - testDatasource: { - isLoading: false, - state: { - layers: [ - { - indexPatternId: '1', - columns: { - y: { - operationType: 'moving_average', - references: ['ref'], - }, + const props = getDefaultProps(); + const { instance } = await mountWithProvider(, { + preloadedState: { + datasourceStates: { + testDatasource: { + isLoading: false, + state: { + layers: [ + { + indexPatternId: '1', + columns: { + y: { + operationType: 'moving_average', + references: ['ref'], }, - columnOrder: ['y'], - incompleteColumns: {}, }, - ], - }, + columnOrder: ['y'], + incompleteColumns: {}, + }, + ], }, }, }, - } - ); + }, + }); act(() => { instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').last().simulate('click'); @@ -548,12 +542,10 @@ describe('LayerPanel', () => { } ); }); - expect(updateAll).toHaveBeenCalled(); - expect(mockVisualization.removeDimension).toHaveBeenCalledWith( - expect.objectContaining({ - columnId: 'y', - }) - ); + expect(props.onRemoveDimension).toHaveBeenCalledWith({ + layerId: props.layerId, + columnId: 'y', + }); }); it('should keep the DimensionContainer open when configuring a new dimension', async () => { @@ -985,7 +977,6 @@ describe('LayerPanel', () => { it('should call onDrop and update visualization when replacing between compatible groups', async () => { const mockVis = { ...mockVisualization, - removeDimension: jest.fn(), setDimension: jest.fn(() => 'modifiedState'), }; mockVis.getConfiguration.mockReturnValue({ @@ -1019,11 +1010,13 @@ describe('LayerPanel', () => { mockDatasource.onDrop.mockReturnValue(true); const updateVisualization = jest.fn(); + const mockOnRemoveDimension = jest.fn(); const { instance } = await mountWithProvider( @@ -1047,19 +1040,15 @@ describe('LayerPanel', () => { prevState: 'state', }) ); - expect(mockVis.removeDimension).toHaveBeenCalledWith( - expect.objectContaining({ - columnId: 'a', - layerId: 'first', - prevState: 'modifiedState', - }) - ); + expect(mockOnRemoveDimension).toHaveBeenCalledWith({ + columnId: 'a', + layerId: 'first', + }); expect(updateVisualization).toHaveBeenCalledTimes(1); }); it('should call onDrop and update visualization when replacing between compatible groups2', async () => { const mockVis = { ...mockVisualization, - removeDimension: jest.fn(), setDimension: jest.fn(() => 'modifiedState'), onDrop: jest.fn(() => 'modifiedState'), }; @@ -1096,11 +1085,13 @@ describe('LayerPanel', () => { mockDatasource.onDrop.mockReturnValue(true); const updateVisualization = jest.fn(); + const mockOnRemoveDimension = jest.fn(); const { instance } = await mountWithProvider( @@ -1132,9 +1123,130 @@ describe('LayerPanel', () => { mockVis ); expect(mockVis.setDimension).not.toHaveBeenCalled(); - expect(mockVis.removeDimension).not.toHaveBeenCalled(); + expect(mockOnRemoveDimension).toHaveBeenCalledWith({ + columnId: 'a', + layerId: 'first', + }); expect(updateVisualization).toHaveBeenCalledTimes(1); }); + + it('should not change visualization state if datasource drop failed', async () => { + const mockVis = { + ...mockVisualization, + setDimension: jest.fn(() => 'modifiedState'), + }; + + mockVis.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }, { columnId: 'b' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [{ columnId: 'c' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup2', + }, + ], + }); + + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; + + mockDatasource.onDrop.mockReturnValue(false); + const updateVisualization = jest.fn(); + const mockOnRemoveDimension = jest.fn(); + + const { instance } = await mountWithProvider( + + + + ); + act(() => { + instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); + }); + + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dropType: 'replace_compatible', + source: draggingOperation, + }) + ); + expect(updateVisualization).not.toHaveBeenCalled(); + expect(mockOnRemoveDimension).not.toHaveBeenCalled(); + }); + + it("should not remove source if drop type doesn't require it", async () => { + const mockVis = { + ...mockVisualization, + setDimension: jest.fn(() => 'modifiedState'), + }; + + mockVis.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }, { columnId: 'b' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [{ columnId: 'c' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup2', + }, + ], + }); + + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; + + mockDatasource.onDrop.mockReturnValue(true); + + const mockOnRemoveDimension = jest.fn(); + + const { instance } = await mountWithProvider( + + + + ); + act(() => { + instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'duplicate_compatible'); + }); + + expect(mockOnRemoveDimension).not.toHaveBeenCalled(); + }); }); describe('add a new dimension', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 3d1068ebd521f..c27a09b95ede6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -18,6 +18,7 @@ import { EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { LayerType } from '../../../../common'; import { LayerActions } from './layer_actions'; import { IndexPatternServiceAPI } from '../../../data_views_service/service'; import { NativeRenderer } from '../../../native_renderer'; @@ -27,6 +28,7 @@ import { DragDropOperation, DropType, isOperation, + VisualizationDimensionGroupConfig, } from '../../../types'; import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; @@ -42,7 +44,7 @@ import { selectResolvedDateRange, selectDatasourceStates, } from '../../../state_management'; -import { onDropForVisualization } from './buttons/drop_targets_utils'; +import { onDropForVisualization, shouldRemoveSource } from './buttons/drop_targets_utils'; import { getSharedActions } from './layer_actions/layer_actions'; const initialActiveDimensionState = { @@ -52,19 +54,26 @@ const initialActiveDimensionState = { export function LayerPanel( props: Exclude & { activeVisualization: Visualization; + dimensionGroups: VisualizationDimensionGroupConfig[]; layerId: string; layerIndex: number; isOnlyLayer: boolean; + addLayer: (layerType: LayerType) => void; updateVisualization: StateSetter; - updateDatasource: (datasourceId: string | undefined, newState: unknown) => void; + updateDatasource: ( + datasourceId: string | undefined, + newState: unknown, + dontSyncLinkedDimensions?: boolean + ) => void; updateDatasourceAsync: (datasourceId: string | undefined, newState: unknown) => void; updateAll: ( datasourceId: string | undefined, newDatasourcestate: unknown, newVisualizationState: unknown ) => void; - onRemoveLayer: () => void; + onRemoveLayer: (layerId: string) => void; onCloneLayer: () => void; + onRemoveDimension: (props: { columnId: string; layerId: string }) => void; registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; toggleFullscreen: () => void; onEmptyDimensionAdd: (columnId: string, group: { groupId: string }) => void; @@ -86,6 +95,7 @@ export function LayerPanel( framePublicAPI, layerId, isOnlyLayer, + dimensionGroups, onRemoveLayer, onCloneLayer, registerNewLayerRef, @@ -138,26 +148,15 @@ export function LayerPanel( dateRange, }; - const { groups } = useMemo( - () => activeVisualization.getConfiguration(layerVisualizationConfigProps), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - layerVisualizationConfigProps.frame, - layerVisualizationConfigProps.state, - layerId, - activeVisualization, - ] - ); - const columnLabelMap = !layerDatasource && activeVisualization.getUniqueLabels ? activeVisualization.getUniqueLabels(props.visualizationState) : layerDatasource?.uniqueLabels?.(layerDatasourceConfigProps?.state); - const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); + const isEmptyLayer = !dimensionGroups.some((d) => d.accessors.length > 0); const { activeId, activeGroup } = activeDimension; - const allAccessors = groups.flatMap((group) => + const allAccessors = dimensionGroups.flatMap((group) => group.accessors.map((accessor) => accessor.columnId) ); @@ -188,16 +187,17 @@ export function LayerPanel( layerDatasource?.onDrop({ state: layerDatasourceState, setState: (newState: unknown) => { - updateDatasource(datasourceId, newState); + // we don't sync linked dimension here because that would trigger an onDrop routine within an onDrop routine + updateDatasource(datasourceId, newState, true); }, source, target: { ...(target as unknown as DragDropOperation), filterOperations: - groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations || - Boolean, + dimensionGroups.find(({ groupId: gId }) => gId === target.groupId) + ?.filterOperations || Boolean, }, - dimensionGroups: groups, + targetLayerDimensionGroups: dimensionGroups, dropType, indexPatterns: framePublicAPI.dataViews.indexPatterns, }) @@ -214,24 +214,31 @@ export function LayerPanel( target, source, dropType, - group: groups.find(({ groupId: gId }) => gId === target.groupId), + group: dimensionGroups.find(({ groupId: gId }) => gId === target.groupId), }, activeVisualization ) ); + + if (isOperation(source) && shouldRemoveSource(source, dropType)) { + props.onRemoveDimension({ + columnId: source.columnId, + layerId: source.layerId, + }); + } } }; }, [ layerDatasource, setNextFocusedButtonId, layerDatasourceState, - groups, + dimensionGroups, + framePublicAPI, updateDatasource, datasourceId, activeVisualization, updateVisualization, - props.visualizationState, - framePublicAPI, + props, ]); const isDimensionPanelOpen = Boolean(activeId); @@ -260,16 +267,8 @@ export function LayerPanel( // The datasource can indicate that the previously-valid column is no longer // complete, which clears the visualization. This keeps the flyout open and reuses // the previous columnId - updateAll( - datasourceId, - newState, - activeVisualization.removeDimension({ - layerId, - columnId: activeId, - prevState: visualizationState, - frame: framePublicAPI, - }) - ); + props.updateDatasource(datasourceId, newState); + props.onRemoveDimension({ layerId, columnId: activeId }); } } else if (isDimensionComplete) { updateAll( @@ -327,7 +326,7 @@ export function LayerPanel( isOnlyLayer, isTextBasedLanguage, onCloneLayer, - onRemoveLayer, + onRemoveLayer: () => onRemoveLayer(layerId), }), ].filter((i) => i.isCompatible), [ @@ -402,7 +401,7 @@ export function LayerPanel( )} - {groups.map((group, groupIndex) => { + {dimensionGroups.map((group, groupIndex) => { let errorText: string = ''; if (!isEmptyLayer) { @@ -529,32 +528,7 @@ export function LayerPanel( }); }} onRemoveClick={(id: string) => { - if (datasourceId && layerDatasource) { - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - indexPatterns: dataViews.indexPatterns, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - frame: framePublicAPI, - }) - ); - } else { - props.updateVisualization( - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - frame: framePublicAPI, - }) - ); - } + props.onRemoveDimension({ columnId: id, layerId }); removeButtonRef(id); }} invalid={ @@ -694,7 +668,7 @@ export function LayerPanel( groupId: activeGroup.groupId, hideGrouping: activeGroup.hideGrouping, filterOperations: activeGroup.filterOperations, - dimensionGroups: groups, + dimensionGroups, toggleFullscreen, isFullscreen, setState: updateDataLayerState, @@ -723,7 +697,10 @@ export function LayerPanel( ...layerVisualizationConfigProps, groupId: activeGroup.groupId, accessor: activeId, + datasource, setState: props.updateVisualization, + addLayer: props.addLayer, + removeLayer: props.onRemoveLayer, panelRef, }} /> @@ -735,7 +712,10 @@ export function LayerPanel( ...layerVisualizationConfigProps, groupId: activeGroup.groupId, accessor: activeId, + datasource, setState: props.updateVisualization, + addLayer: props.addLayer, + removeLayer: props.onRemoveLayer, panelRef, }} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx index 9370892a2d7fe..c11156b556985 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx @@ -27,6 +27,7 @@ describe('Data Panel Wrapper', () => { activeDatasource: { renderDataPanel, getUsedDataViews: jest.fn(), + getLayers: jest.fn(() => []), } as unknown as Datasource, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 0c5f6ac3fcd9e..2530b8e215ddb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -306,7 +306,7 @@ describe('editor_frame', () => { setDatasourceState(updatedState); }); - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(3); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, @@ -377,6 +377,7 @@ describe('editor_frame', () => { getFilters: jest.fn(), getMaxPossibleNumValues: jest.fn(), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); @@ -386,7 +387,7 @@ describe('editor_frame', () => { setDatasourceState({}); }); - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(3); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ @@ -711,7 +712,7 @@ describe('editor_frame', () => { instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click'); }); - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: suggestionVisState, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index a2f36f0754708..e86f602465584 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -571,6 +571,7 @@ describe('suggestion helpers', () => { getFilters: jest.fn(), getMaxPossibleNumValues: jest.fn(), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }, }, { activeId: 'testVis', state: {} }, @@ -609,6 +610,7 @@ describe('suggestion helpers', () => { getFilters: jest.fn(), getMaxPossibleNumValues: jest.fn(), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }, }; defaultParams[3] = { @@ -672,6 +674,7 @@ describe('suggestion helpers', () => { getFilters: jest.fn(), getMaxPossibleNumValues: jest.fn(), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }, }; mockVisualization1.getSuggestions.mockReturnValue([]); diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 54380bd7eec63..b162161a3a90f 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { LensPlugin } from './plugin'; export type { @@ -38,6 +37,7 @@ export type { PieVisualizationState, PieLayerState, SharedPieLayerState, + LayerType, } from '../common/types'; export type { DatatableVisualizationState } from './visualizations/datatable/visualization'; @@ -80,7 +80,6 @@ export type { export type { XYArgs, XYRender, - LayerType, LineStyle, FillStyle, YScaleType, @@ -105,8 +104,7 @@ export type { export type { LensEmbeddableInput, LensSavedObjectAttributes, Embeddable } from './embeddable'; -/** @deprecated Please use LayerTypes from @kbn/expression-xy-plugin **/ -export const layerTypes = LayerTypes; +export { layerTypes } from '../common/layer_types'; export type { LensPublicStart, LensPublicSetup } from './plugin'; diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index 7d66b705ae0fa..cfb93882559ef 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -21,6 +21,7 @@ export function createMockDatasource(id: string): DatasourceMock { getFilters: jest.fn(), getMaxPossibleNumValues: jest.fn(), isTextBasedLanguage: jest.fn(() => false), + hasDefaultTimeField: jest.fn(() => true), }; return { @@ -53,6 +54,7 @@ export function createMockDatasource(id: string): DatasourceMock { getDropProps: jest.fn(), onDrop: jest.fn(), createEmptyLayer: jest.fn(), + syncColumns: jest.fn(), // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called diff --git a/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx b/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx index f5558850d6af0..b4cf7c03bc92b 100644 --- a/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx +++ b/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx @@ -8,6 +8,7 @@ import { EuiFormRow, EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { CollapseFunction } from '../../common/expressions'; const options = [ { text: i18n.translate('xpack.lens.collapse.none', { defaultMessage: 'None' }), value: '' }, @@ -22,7 +23,7 @@ export function CollapseSetting({ onChange, }: { value: string; - onChange: (value: string) => void; + onChange: (value: CollapseFunction) => void; }) { return ( ) => { - onChange(e.target.value); + onChange(e.target.value as CollapseFunction); }} /> diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index 725c68e7a1429..bfb922397e3da 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -44,6 +44,7 @@ export const { cloneLayer, addLayer, setLayerDefaultDimension, + removeDimension, } = lensActions; export const makeConfigureStore = ( diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts index 559120139894b..934fc0854b6e6 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts @@ -20,12 +20,20 @@ import { LensRootStore, selectTriggerApplyChanges, selectChangesApplied, + removeDimension, } from '.'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { makeLensStore, defaultState, mockStoreDeps } from '../mocks'; -import { DatasourceMap, VisualizationMap } from '../types'; +import { + Datasource, + DatasourceMap, + Visualization, + VisualizationDimensionGroupConfig, + VisualizationMap, +} from '../types'; import { applyChanges, disableAutoApply, enableAutoApply, setChangesApplied } from './lens_slice'; -import { LensAppState } from './types'; +import { DataViewsState, LensAppState } from './types'; +import { layerTypes } from '../../common/layer_types'; describe('lensSlice', () => { let store: EnhancedStore<{ lens: LensAppState }>; @@ -108,7 +116,7 @@ describe('lensSlice', () => { }) ); - expect(store.getState().lens.visualization.state).toBe(newVisState); + expect(store.getState().lens.visualization.state).toEqual(newVisState); }); it('should update the datasource state with passed in reducer', () => { const datasourceUpdater = jest.fn(() => ({ changed: true })); @@ -278,7 +286,12 @@ describe('lensSlice', () => { ), removeLayer: (layerIds: unknown, layerId: string) => (layerIds as string[]).filter((id: string) => id !== layerId), - insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], + insertLayer: (layerIds: unknown, layerId: string, layersToLinkTo: string[]) => [ + ...(layerIds as string[]), + layerId, + ...layersToLinkTo, + ], + getCurrentIndexPatternId: jest.fn(() => 'indexPattern1'), getUsedDataView: jest.fn(() => 'indexPattern1'), }; }; @@ -296,8 +309,10 @@ describe('lensSlice', () => { testDatasource: testDatasource('testDatasource'), testDatasource2: testDatasource('testDatasource2'), }; + + const activeVisId = 'testVis'; const visualizationMap = { - testVis: { + [activeVisId]: { clearLayer: (layerIds: unknown, layerId: string) => (layerIds as string[]).map((id: string) => id === layerId ? `vis_clear_${layerId}` : id @@ -305,9 +320,10 @@ describe('lensSlice', () => { removeLayer: (layerIds: unknown, layerId: string) => (layerIds as string[]).filter((id: string) => id !== layerId), getLayerIds: (layerIds: unknown) => layerIds as string[], + getLayersToLinkTo: (state, newLayerId) => ['linked-layer-id'], appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], - getSupportedLayers: jest.fn(() => [{ type: LayerTypes.DATA, label: 'Data Layer' }]), - }, + getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]), + } as Partial, }; let customStore: LensRootStore; @@ -317,12 +333,12 @@ describe('lensSlice', () => { activeDatasourceId: 'testDatasource', datasourceStates, visualization: { - activeId: 'testVis', + activeId: activeVisId, state: ['layer1', 'layer2'], }, stagedPreview: { visualization: { - activeId: 'testVis', + activeId: activeVisId, state: ['layer1', 'layer2'], }, datasourceStates, @@ -345,11 +361,112 @@ describe('lensSlice', () => { const state = customStore.getState().lens; expect(state.visualization.state).toEqual(['layer1', 'layer2', 'foo']); - expect(state.datasourceStates.testDatasource.state).toEqual(['layer1', 'foo']); + expect(state.datasourceStates.testDatasource.state).toEqual([ + 'layer1', + 'foo', + 'linked-layer-id', + ]); expect(state.datasourceStates.testDatasource2.state).toEqual(['layer2']); expect(state.stagedPreview).not.toBeDefined(); }); + it('addLayer: syncs linked dimensions', () => { + const activeVisualization = visualizationMap[activeVisId]; + + activeVisualization.getLinkedDimensions = jest.fn(() => [ + { + from: { + layerId: 'from-layer', + columnId: 'from-column', + groupId: 'from-group', + }, + to: { + layerId: 'from-layer', + columnId: 'from-column', + groupId: 'from-group', + }, + }, + ]); + activeVisualization.getConfiguration = jest.fn(() => ({ + groups: [{ groupId: 'to-group' } as VisualizationDimensionGroupConfig], + })); + activeVisualization.onDrop = jest.fn(({ prevState }) => prevState); + (datasourceMap.testDatasource as unknown as Datasource).syncColumns = jest.fn( + ({ state }) => state + ); + + customStore.dispatch( + addLayer({ + layerId: 'foo', + layerType: layerTypes.DATA, + }) + ); + + expect( + ( + (datasourceMap.testDatasource as unknown as Datasource).syncColumns as jest.Mock< + Datasource['syncColumns'] + > + ).mock.calls[0][0] + ).toMatchInlineSnapshot(` + Object { + "getDimensionGroups": [Function], + "indexPatterns": Object {}, + "links": Array [ + Object { + "from": Object { + "columnId": "from-column", + "groupId": "from-group", + "layerId": "from-layer", + }, + "to": Object { + "columnId": "from-column", + "groupId": "from-group", + "layerId": "from-layer", + }, + }, + ], + "state": Array [ + "layer1", + "foo", + "linked-layer-id", + ], + } + `); + + expect(activeVisualization.onDrop).toHaveBeenCalledTimes(1); + expect({ + ...(activeVisualization.onDrop as jest.Mock).mock.calls[0][0], + frame: undefined, + }).toMatchInlineSnapshot(` + Object { + "dropType": "duplicate_compatible", + "frame": undefined, + "group": undefined, + "prevState": Array [ + "layer1", + "layer2", + "foo", + ], + "source": Object { + "columnId": "from-column", + "groupId": "from-group", + "humanData": Object { + "label": "", + }, + "id": "from-column", + "layerId": "from-layer", + }, + "target": Object { + "columnId": "from-column", + "filterOperations": [Function], + "groupId": "from-group", + "layerId": "from-layer", + }, + } + `); + }); + it('removeLayer: should remove the layer if it is not the only layer', () => { customStore.dispatch( removeOrClearLayer({ @@ -366,5 +483,155 @@ describe('lensSlice', () => { expect(state.stagedPreview).not.toBeDefined(); }); }); + + describe('removing a dimension', () => { + const colToRemove = 'col-id'; + const otherCol = 'other-col-id'; + const datasourceId = 'testDatasource'; + + interface DatasourceState { + cols: string[]; + } + + const datasourceStates = { + [datasourceId]: { + isLoading: false, + state: { + cols: [colToRemove, otherCol], + } as DatasourceState, + }, + }; + + const datasourceMap = { + [datasourceId]: { + id: datasourceId, + removeColumn: jest.fn(({ prevState: state, columnId }) => ({ + ...(state as DatasourceState), + cols: (state as DatasourceState).cols.filter((id) => id !== columnId), + })), + getLayers: () => [], + } as Partial, + }; + + const activeVisId = 'testVis'; + + const visualizationMap = { + [activeVisId]: { + removeDimension: jest.fn(({ prevState, columnId }) => + (prevState as string[]).filter((id) => id !== columnId) + ), + } as Partial, + }; + + const visualizationState = [colToRemove, otherCol]; + + const dataViews = { indexPatterns: {} } as DataViewsState; + + const layerId = 'some-layer-id'; + + let customStore: LensRootStore; + beforeEach(() => { + customStore = makeLensStore({ + preloadedState: { + activeDatasourceId: datasourceId, + datasourceStates, + visualization: { + activeId: activeVisId, + state: visualizationState, + }, + dataViews, + } as Partial, + storeDeps: mockStoreDeps({ + visualizationMap: visualizationMap as unknown as VisualizationMap, + datasourceMap: datasourceMap as unknown as DatasourceMap, + }), + }).store; + + jest.clearAllMocks(); + }); + + it('removes a dimension', () => { + customStore.dispatch( + removeDimension({ + layerId, + columnId: colToRemove, + datasourceId, + }) + ); + + const state = customStore.getState().lens; + + expect(datasourceMap[datasourceId].removeColumn).toHaveBeenCalledWith({ + layerId, + columnId: colToRemove, + prevState: datasourceStates[datasourceId].state, + indexPatterns: dataViews.indexPatterns, + }); + expect(visualizationMap[activeVisId].removeDimension).toHaveBeenCalledWith( + expect.objectContaining({ + layerId, + columnId: colToRemove, + prevState: visualizationState, + }) + ); + expect(state.visualization.state).toEqual([otherCol]); + expect((state.datasourceStates[datasourceId].state as DatasourceState).cols).toEqual([ + otherCol, + ]); + }); + + it('removes a dimension without touching the datasource', () => { + customStore.dispatch( + removeDimension({ + layerId, + columnId: colToRemove, + datasourceId: undefined, + }) + ); + + const state = customStore.getState().lens; + + expect(datasourceMap[datasourceId].removeColumn).not.toHaveBeenCalled(); + + expect(visualizationMap[activeVisId].removeDimension).toHaveBeenCalledWith( + expect.objectContaining({ + layerId, + columnId: colToRemove, + prevState: visualizationState, + }) + ); + expect(state.visualization.state).toEqual([otherCol]); + }); + + it('removes linked dimensions', () => { + visualizationMap[activeVisId].getLinkedDimensions = jest.fn(() => [ + { + from: { + columnId: colToRemove, + layerId, + groupId: '', + }, + to: { + columnId: otherCol, + layerId, + groupId: '', + }, + }, + ]); + + customStore.dispatch( + removeDimension({ + layerId, + columnId: colToRemove, + datasourceId, + }) + ); + + const state = customStore.getState().lens; + + expect(state.visualization.state).toEqual([]); + expect((state.datasourceStates[datasourceId].state as DatasourceState).cols).toEqual([]); + }); + }); }); }); diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 38aee718a536f..b90c5bc965a6b 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -12,7 +12,13 @@ import { Query } from '@kbn/es-query'; import { History } from 'history'; import { LensEmbeddableInput } from '..'; import { TableInspectorAdapter } from '../editor_frame_service/types'; -import type { VisualizeEditorContext, Suggestion, IndexPattern } from '../types'; +import type { + VisualizeEditorContext, + Suggestion, + IndexPattern, + VisualizationMap, + DatasourceMap, +} from '../types'; import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils'; import type { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; import type { Datasource, Visualization } from '../types'; @@ -21,7 +27,8 @@ import type { LayerType } from '../../common/types'; import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer'; import { getVisualizeFieldSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers'; import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types'; -import { selectFramePublicAPI } from './selectors'; +import { selectDataViews, selectFramePublicAPI } from './selectors'; +import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; export const initialState: LensAppState = { persistedDoc: undefined, @@ -106,6 +113,7 @@ export const updateDatasourceState = createAction<{ updater: unknown | ((prevState: unknown) => unknown); datasourceId: string; clearStagedPreview?: boolean; + dontSyncLinkedDimensions?: boolean; }>('lens/updateDatasourceState'); export const updateVisualizationState = createAction<{ visualizationId: string; @@ -200,6 +208,11 @@ export const changeIndexPattern = createAction<{ layerId?: string; dataViews: Partial; }>('lens/changeIndexPattern'); +export const removeDimension = createAction<{ + layerId: string; + columnId: string; + datasourceId?: string; +}>('lens/removeDimension'); export const lensActions = { setState, @@ -231,6 +244,8 @@ export const lensActions = { updateIndexPatterns, replaceIndexpattern, changeIndexPattern, + removeDimension, + syncLinkedDimensions, }; export const makeLensReducer = (storeDeps: LensStoreDeps) => { @@ -286,7 +301,31 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { }; } ) => { - const newState = updater(current(state) as LensAppState); + let newState: LensAppState = updater(current(state) as LensAppState); + + if (newState.activeDatasourceId) { + const { datasourceState, visualizationState } = syncLinkedDimensions( + newState, + visualizationMap, + datasourceMap + ); + + newState = { + ...newState, + visualization: { + ...newState.visualization, + state: visualizationState, + }, + datasourceStates: { + ...newState.datasourceStates, + [newState.activeDatasourceId]: { + ...newState.datasourceStates[newState.activeDatasourceId], + state: datasourceState, + }, + }, + }; + } + return { ...newState, stagedPreview: undefined, @@ -545,22 +584,49 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { updater: unknown | ((prevState: unknown) => unknown); datasourceId: string; clearStagedPreview?: boolean; + dontSyncLinkedDimensions: boolean; }; } ) => { - return { - ...state, + const currentState = current(state); + + const newAppState: LensAppState = { + ...currentState, datasourceStates: { - ...state.datasourceStates, + ...currentState.datasourceStates, [payload.datasourceId]: { state: typeof payload.updater === 'function' - ? payload.updater(current(state).datasourceStates[payload.datasourceId].state) + ? payload.updater(currentState.datasourceStates[payload.datasourceId].state) : payload.updater, isLoading: false, }, }, - stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview, + stagedPreview: payload.clearStagedPreview ? undefined : currentState.stagedPreview, + }; + + if (payload.dontSyncLinkedDimensions) { + return newAppState; + } + + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions(newAppState, visualizationMap, datasourceMap, payload.datasourceId); + + return { + ...newAppState, + visualization: { + ...newAppState.visualization, + state: syncedVisualizationState, + }, + datasourceStates: { + ...newAppState.datasourceStates, + [payload.datasourceId]: { + state: syncedDatasourceState, + isLoading: false, + }, + }, }; }, [updateVisualizationState.type]: ( @@ -583,13 +649,21 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { if (state.visualization.activeId !== payload.visualizationId) { return state; } - return { - ...state, - visualization: { - ...state.visualization, - state: payload.newState, - }, - }; + + state.visualization.state = payload.newState; + + if (!state.activeDatasourceId) { + return; + } + + // TODO - consolidate into applySyncLinkedDimensions + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); + + state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState; + state.visualization.state = syncedVisualizationState; }, [switchVisualization.type]: ( @@ -945,11 +1019,15 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { .getSupportedLayers(visualizationState, framePublicAPI) .find(({ type }) => type === layerType) || {}; + const layersToLinkTo = + activeVisualization.getLayersToLinkTo?.(visualizationState, layerId) ?? []; + const datasourceState = !noDatasource && activeDatasource ? activeDatasource.insertLayer( state.datasourceStates[state.activeDatasourceId].state, - layerId + layerId, + layersToLinkTo ) : state.datasourceStates[state.activeDatasourceId].state; @@ -966,6 +1044,14 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { state.visualization.state = activeVisualizationState; state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState; state.stagedPreview = undefined; + + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); + + state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState; + state.visualization.state = syncedVisualizationState; }, [setLayerDefaultDimension.type]: ( state, @@ -1001,6 +1087,72 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { state.visualization.state = activeVisualizationState; state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState; }, + [removeDimension.type]: ( + state, + { + payload: { layerId, columnId, datasourceId }, + }: { + payload: { + layerId: string; + columnId: string; + datasourceId?: string; + }; + } + ) => { + if (!state.visualization.activeId) { + return state; + } + + const activeVisualization = visualizationMap[state.visualization.activeId]; + + const links = activeVisualization.getLinkedDimensions?.(state.visualization.state); + + const linkedDimensions = links + ?.filter(({ from: { columnId: fromId } }) => columnId === fromId) + ?.map(({ to }) => to); + + const datasource = datasourceId ? datasourceMap[datasourceId] : undefined; + + const frame = selectFramePublicAPI({ lens: current(state) }, datasourceMap); + + const remove = (dimensionProps: { layerId: string; columnId: string }) => { + if (datasource && datasourceId) { + let datasourceState; + try { + datasourceState = current(state.datasourceStates[datasourceId].state); + } catch { + datasourceState = state.datasourceStates[datasourceId].state; + } + state.datasourceStates[datasourceId].state = datasource?.removeColumn({ + layerId: dimensionProps.layerId, + columnId: dimensionProps.columnId, + prevState: datasourceState, + indexPatterns: frame.dataViews.indexPatterns, + }); + } + + let visualizationState; + try { + visualizationState = current(state.visualization.state); + } catch { + visualizationState = state.visualization.state; + } + state.visualization.state = activeVisualization.removeDimension({ + layerId: dimensionProps.layerId, + columnId: dimensionProps.columnId, + prevState: visualizationState, + frame, + }); + }; + + remove({ layerId, columnId }); + + linkedDimensions?.forEach( + (linkedDimension) => + linkedDimension.columnId && // if there's no columnId, there's no dimension to remove + remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId }) + ); + }, }); }; @@ -1053,6 +1205,11 @@ function addInitialValueIfAvailable({ { ...info, columnId: columnId || info.columnId, + visualizationGroups: activeVisualization.getConfiguration({ + layerId, + frame: framePublicAPI, + state: activeVisualizationState, + }).groups, } ), activeVisualizationState, @@ -1071,3 +1228,76 @@ function addInitialValueIfAvailable({ activeVisualizationState: visualizationState, }; } + +function syncLinkedDimensions( + state: LensAppState, + visualizationMap: VisualizationMap, + datasourceMap: DatasourceMap, + _datasourceId?: string +) { + const datasourceId = _datasourceId ?? state.activeDatasourceId; + + if (!datasourceId) { + return { datasourceState: null, visualizationState: state.visualization.state }; + } + + const indexPatterns = selectDataViews({ lens: state }).indexPatterns; + + let datasourceState: unknown = state.datasourceStates[datasourceId].state; + let visualizationState: unknown = state.visualization.state; + + const activeVisualization = visualizationMap[state.visualization.activeId!]; // TODO - double check the safety of this coercion + const linkedDimensions = activeVisualization.getLinkedDimensions?.(visualizationState); + const frame = selectFramePublicAPI({ lens: state }, datasourceMap); + + const getDimensionGroups = (layerId: string) => + activeVisualization.getConfiguration({ + state: visualizationState, + layerId, + frame, + }).groups; + + if (linkedDimensions) { + const idAssuredLinks = linkedDimensions.map((link) => ({ + ...link, + to: { ...link.to, columnId: link.to.columnId ?? generateId() }, + })); + + datasourceState = datasourceMap[datasourceId].syncColumns({ + state: datasourceState, + links: idAssuredLinks, + getDimensionGroups, + indexPatterns, + }); + + idAssuredLinks.forEach(({ from, to }) => { + const dropSource = { + ...from, + id: from.columnId, + // don't need to worry about accessibility here + humanData: { label: '' }, + }; + + const dropTarget = { + ...to, + filterOperations: () => true, + }; + + visualizationState = (activeVisualization.onDrop || onDropForVisualization)?.( + { + prevState: visualizationState, + frame, + target: dropTarget, + source: dropSource, + dropType: 'duplicate_compatible', + group: getDimensionGroups(to.layerId).find( + ({ groupId }) => groupId === dropTarget.groupId + ), + }, + activeVisualization + ); + }); + } + + return { datasourceState, visualizationState }; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 389e500488c0d..e01c35f3457d9 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -233,6 +233,15 @@ export interface GetDropPropsArgs { indexPatterns: IndexPatternMap; } +interface DimensionLink { + from: { columnId: string; groupId: string; layerId: string }; + to: { + columnId?: string; + groupId: string; + layerId: string; + }; +} + /** * Interface for the datasource registry */ @@ -254,7 +263,7 @@ export interface Datasource { getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; getUnifiedSearchErrors?: (state: T) => Error[]; - insertLayer: (state: T, newLayerId: string) => T; + insertLayer: (state: T, newLayerId: string, linkToLayers?: string[]) => T; createEmptyLayer: (indexPatternId: string) => T; removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; @@ -278,10 +287,18 @@ export interface Datasource { value: { columnId: string; groupId: string; + visualizationGroups: VisualizationDimensionGroupConfig[]; staticValue?: unknown; + autoTimeField?: boolean; } ) => T; + syncColumns: (args: { + state: T; + links: Array; + getDimensionGroups: (layerId: string) => VisualizationDimensionGroupConfig[]; + indexPatterns: IndexPatternMap; + }) => T; getSelectedFields?: (state: T) => string[]; renderDataPanel: ( @@ -489,6 +506,7 @@ export interface DatasourcePublicAPI { * or 6 if the "Other" bucket is enabled) */ getMaxPossibleNumValues: (columnId: string) => number | null; + hasDefaultTimeField: () => boolean; } export interface DatasourceDataPanelProps { @@ -619,7 +637,7 @@ export interface DatasourceDimensionDropProps { forceRender?: boolean; } >; - dimensionGroups: VisualizationDimensionGroupConfig[]; + targetLayerDimensionGroups: VisualizationDimensionGroupConfig[]; } export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { @@ -675,6 +693,7 @@ export interface OperationMetadata { */ export interface OperationDescriptor extends Operation { hasTimeShift: boolean; + hasReducedTimeRange: boolean; } export interface VisualizationConfigProps { @@ -699,7 +718,10 @@ export interface VisualizationToolbarProps { export type VisualizationDimensionEditorProps = VisualizationConfigProps & { groupId: string; accessor: string; + datasource: DatasourcePublicAPI | undefined; setState(newState: T | ((currState: T) => T)): void; + addLayer: (layerType: LayerType) => void; + removeLayer: (layerId: string) => void; panelRef: MutableRefObject; }; @@ -977,7 +999,9 @@ export interface Visualization { columnId: string; groupId: string; staticValue?: unknown; + autoTimeField?: boolean; }>; + canAddViaMenu?: boolean; }>; /** * returns a list of custom actions supported by the visualization layer. @@ -990,6 +1014,17 @@ export interface Visualization { ) => LayerAction[]; /** returns the type string of the given layer */ getLayerType: (layerId: string, state?: T) => LayerType | undefined; + + /** + * Get the layers this one should be linked to (currently that means just keeping the data view in sync) + */ + getLayersToLinkTo?: (state: T, newLayerId: string) => string[]; + + /** + * Returns a set of dimensions that should be kept in sync + */ + getLinkedDimensions?: (state: T) => DimensionLink[]; + /* returns the type of removal operation to perform for the specific layer in the current state */ getRemoveOperation?: (state: T, layerId: string) => 'remove' | 'clear'; @@ -997,6 +1032,7 @@ export interface Visualization { * For consistency across different visualizations, the dimension configuration UI is standardized */ getConfiguration: (props: VisualizationConfigProps) => { + hidden?: boolean; groups: VisualizationDimensionGroupConfig[]; }; @@ -1148,6 +1184,7 @@ export interface Visualization { */ onIndexPatternChange?: (state: T, indexPatternId: string, layerId?: string) => T; onIndexPatternRename?: (state: T, oldIndexPatternId: string, newIndexPatternId: string) => T; + getLayersToRemoveOnIndexPatternChange?: (state: T) => string[]; /** * Gets custom display options for showing the visualization. */ diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx index a1c6dc550d7a9..5bad424f5d208 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx @@ -74,6 +74,9 @@ describe('data table dimension editor', () => { setState, paletteService: chartPluginMock.createPaletteRegistry(), panelRef: React.createRef(), + addLayer: jest.fn(), + removeLayer: jest.fn(), + datasource: {} as DatasourcePublicAPI, }; }); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx index ed0048ca0d409..2c0f9f41cb35f 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx @@ -98,7 +98,7 @@ export function TableDimensionEditor( {props.groupId === 'rows' && ( { + onChange={(collapseFn) => { setState({ ...state, columns: updateColumnWith(state, accessor, { collapseFn }), diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor_additional_section.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor_additional_section.test.tsx index 3262a786c72f6..770e95e54d099 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor_additional_section.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor_additional_section.test.tsx @@ -8,7 +8,11 @@ import React from 'react'; import { EuiComboBox, EuiFieldText } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types'; +import { + DatasourcePublicAPI, + FramePublicAPI, + VisualizationDimensionEditorProps, +} from '../../../types'; import { DatatableVisualizationState } from '../visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; @@ -67,6 +71,9 @@ describe('data table dimension editor additional section', () => { setState, paletteService: chartPluginMock.createPaletteRegistry(), panelRef: React.createRef(), + addLayer: jest.fn(), + removeLayer: jest.fn(), + datasource: {} as DatasourcePublicAPI, }; }); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx index cc4b108a23bda..e6683aee13499 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx @@ -526,6 +526,7 @@ describe('Datatable Visualization', () => { label: 'label', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }); const expression = datatableVisualization.toExpression( @@ -577,6 +578,7 @@ describe('Datatable Visualization', () => { label: 'label', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }); const expression = datatableVisualization.toExpression( @@ -712,6 +714,7 @@ describe('Datatable Visualization', () => { label: 'label', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }); const error = datatableVisualization.getErrorMessages({ @@ -737,6 +740,7 @@ describe('Datatable Visualization', () => { label: 'label', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }); const error = datatableVisualization.getErrorMessages({ diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.test.tsx index 824f70dd68348..ffed81f52a35b 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/dimension_editor.test.tsx @@ -24,6 +24,7 @@ import { act } from 'react-dom/test-utils'; import { PalettePanelContainer } from '../../shared_components'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import type { LegacyMetricState } from '../../../common/types'; +import { DatasourcePublicAPI } from '../..'; // mocking random id generator function jest.mock('@elastic/eui', () => { @@ -93,6 +94,9 @@ describe('metric dimension editor', () => { setState, paletteService: chartPluginMock.createPaletteRegistry(), panelRef: React.createRef(), + addLayer: jest.fn(), + removeLayer: jest.fn(), + datasource: {} as DatasourcePublicAPI, }; // add a div to the ref props.panelRef.current = document.createElement('div'); diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts index 3fa5ea3dd0393..517e331634880 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts @@ -271,6 +271,7 @@ describe('metric_visualization', () => { label: 'shazm', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; }, }; diff --git a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap index 7d1df68158266..6114628b3adf4 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`metric visualization dimension groups configuration generates configuration 1`] = ` +exports[`metric visualization dimension groups configuration primary layer generates configuration 1`] = ` Object { "groups": Array [ Object { @@ -20,7 +20,6 @@ Object { }, "groupId": "metric", "groupLabel": "Primary metric", - "layerId": "first", "paramEditorCustomProps": Object { "headingLabel": "Value", }, @@ -42,11 +41,9 @@ Object { }, "groupId": "secondaryMetric", "groupLabel": "Secondary metric", - "layerId": "first", "paramEditorCustomProps": Object { "headingLabel": "Value", }, - "requiredMinDimensionCount": 0, "supportsMoreColumns": false, }, Object { @@ -65,12 +62,10 @@ Object { "groupId": "max", "groupLabel": "Maximum value", "groupTooltip": "If the maximum value is specified, the minimum value is fixed at zero.", - "layerId": "first", "paramEditorCustomProps": Object { "headingLabel": "Value", }, "prioritizedOperation": "max", - "requiredMinDimensionCount": 0, "supportStaticValue": true, "supportsMoreColumns": false, }, @@ -90,15 +85,13 @@ Object { }, "groupId": "breakdownBy", "groupLabel": "Break down by", - "layerId": "first", - "requiredMinDimensionCount": 0, "supportsMoreColumns": false, }, ], } `; -exports[`metric visualization dimension groups configuration operation filtering breakdownBy supports correct operations 1`] = ` +exports[`metric visualization dimension groups configuration primary layer operation filtering breakdownBy supports correct operations 1`] = ` Array [ Object { "dataType": "number", @@ -111,7 +104,7 @@ Array [ ] `; -exports[`metric visualization dimension groups configuration operation filtering max supports correct operations 1`] = ` +exports[`metric visualization dimension groups configuration primary layer operation filtering max supports correct operations 1`] = ` Array [ Object { "dataType": "number", @@ -120,7 +113,7 @@ Array [ ] `; -exports[`metric visualization dimension groups configuration operation filtering metric supports correct operations 1`] = ` +exports[`metric visualization dimension groups configuration primary layer operation filtering metric supports correct operations 1`] = ` Array [ Object { "dataType": "number", @@ -129,7 +122,7 @@ Array [ ] `; -exports[`metric visualization dimension groups configuration operation filtering secondaryMetric supports correct operations 1`] = ` +exports[`metric visualization dimension groups configuration primary layer operation filtering secondaryMetric supports correct operations 1`] = ` Array [ Object { "dataType": "number", @@ -137,3 +130,63 @@ Array [ }, ] `; + +exports[`metric visualization dimension groups configuration trendline layer generates configuration 1`] = ` +Object { + "groups": Array [ + Object { + "accessors": Array [ + Object { + "columnId": "trendline-metric-col-id", + }, + ], + "filterOperations": [Function], + "groupId": "trendMetric", + "groupLabel": "Primary metric", + "hideGrouping": true, + "nestingOrder": 3, + "supportsMoreColumns": false, + }, + Object { + "accessors": Array [ + Object { + "columnId": "trendline-secondary-metric-col-id", + }, + ], + "filterOperations": [Function], + "groupId": "trendSecondaryMetric", + "groupLabel": "Secondary metric", + "hideGrouping": true, + "nestingOrder": 2, + "supportsMoreColumns": false, + }, + Object { + "accessors": Array [ + Object { + "columnId": "trendline-time-col-id", + }, + ], + "filterOperations": [Function], + "groupId": "trendTime", + "groupLabel": "Time field", + "hideGrouping": true, + "nestingOrder": 1, + "supportsMoreColumns": false, + }, + Object { + "accessors": Array [ + Object { + "columnId": "trendline-breakdown-col-id", + }, + ], + "filterOperations": [Function], + "groupId": "trendBreakdownBy", + "groupLabel": "Break down by", + "hideGrouping": true, + "nestingOrder": 0, + "supportsMoreColumns": false, + }, + ], + "hidden": true, +} +`; diff --git a/x-pack/plugins/lens/public/visualizations/metric/constants.ts b/x-pack/plugins/lens/public/visualizations/metric/constants.ts index 58a35d89146ed..9aed58ccfe44c 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/constants.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/constants.ts @@ -12,4 +12,8 @@ export const GROUP_ID = { SECONDARY_METRIC: 'secondaryMetric', MAX: 'max', BREAKDOWN_BY: 'breakdownBy', + TREND_METRIC: 'trendMetric', + TREND_SECONDARY_METRIC: 'trendSecondaryMetric', + TREND_TIME: 'trendTime', + TREND_BREAKDOWN_BY: 'trendBreakdownBy', } as const; diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx index fb7c505600e9a..9826e60a83bc9 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx @@ -8,14 +8,14 @@ /* eslint-disable max-classes-per-file */ import React, { FormEvent } from 'react'; -import { VisualizationDimensionEditorProps } from '../../types'; +import { OperationDescriptor, VisualizationDimensionEditorProps } from '../../types'; import { CustomPaletteParams, PaletteOutput, PaletteRegistry } from '@kbn/coloring'; import { MetricVisualizationState } from './visualization'; -import { DimensionEditor } from './dimension_editor'; +import { DimensionEditor, SupportingVisType } from './dimension_editor'; import { HTMLAttributes, mount, ReactWrapper, shallow } from 'enzyme'; import { CollapseSetting } from '../../shared_components/collapse_setting'; -import { EuiButtonGroup, EuiColorPicker } from '@elastic/eui'; +import { EuiButtonGroup, EuiColorPicker, PropsOf } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { LayoutDirection } from '@elastic/charts'; import { act } from 'react-dom/test-utils'; @@ -24,6 +24,8 @@ import { createMockFramePublicAPI } from '../../mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { euiLightVars } from '@kbn/ui-theme'; import { DebouncedInput } from '../../shared_components/debounced_input'; +import { DatasourcePublicAPI } from '../..'; +import { CollapseFunction } from '../../../common/expressions'; jest.mock('lodash', () => { const original = jest.requireActual('lodash'); @@ -37,9 +39,14 @@ jest.mock('lodash', () => { const SELECTORS = { PRIMARY_METRIC_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_primary_metric"]', SECONDARY_METRIC_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_secondary_metric"]', + MAX_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_maximum"]', BREAKDOWN_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_breakdown"]', }; +// see https://github.com/facebook/jest/issues/4402#issuecomment-534516219 +const expectCalledBefore = (mock1: jest.Mock, mock2: jest.Mock) => + expect(mock1.mock.invocationCallOrder[0]).toBeLessThan(mock2.mock.invocationCallOrder[0]); + describe('dimension editor', () => { const palette: PaletteOutput = { type: 'palette', @@ -63,6 +70,13 @@ describe('dimension editor', () => { maxCols: 5, color: 'static-color', palette, + showBar: true, + trendlineLayerId: 'second', + trendlineLayerType: 'metricTrendline', + trendlineMetricAccessor: 'trendline-metric-col-id', + trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-accessor', + trendlineTimeAccessor: 'trendline-time-col-id', + trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', }; let props: VisualizationDimensionEditorProps & { @@ -75,6 +89,14 @@ describe('dimension editor', () => { groupId: 'some-group', accessor: 'some-accessor', state: fullState, + datasource: { + hasDefaultTimeField: jest.fn(), + getOperationForColumnId: jest.fn(() => ({ + hasReducedTimeRange: false, + })), + } as unknown as DatasourcePublicAPI, + removeLayer: jest.fn(), + addLayer: jest.fn(), frame: createMockFramePublicAPI(), setState: jest.fn(), panelRef: {} as React.MutableRefObject, @@ -82,8 +104,11 @@ describe('dimension editor', () => { }; }); + afterEach(() => jest.clearAllMocks()); + describe('primary metric dimension', () => { const accessor = 'primary-metric-col-id'; + const metricAccessorState = { ...fullState, metricAccessor: accessor }; beforeEach(() => { props.frame.activeData = { @@ -129,16 +154,53 @@ describe('dimension editor', () => { this.colorPicker.props().onChange!(color, {} as EuiColorPickerOutput); }); } + + private get supportingVisButtonGroup() { + return this._wrapper.find( + 'EuiButtonGroup[data-test-subj="lnsMetric_supporting_visualization_buttons"]' + ) as unknown as ReactWrapper>; + } + + public get currentSupportingVis() { + return this.supportingVisButtonGroup + .props() + .idSelected?.split('--')[1] as SupportingVisType; + } + + public isDisabled(type: SupportingVisType) { + return this.supportingVisButtonGroup.props().options.find(({ id }) => id.includes(type)) + ?.isDisabled; + } + + public setSupportingVis(type: SupportingVisType) { + this.supportingVisButtonGroup.props().onChange(`some-id--${type}`); + } + + private get progressDirectionControl() { + return this._wrapper.find( + 'EuiButtonGroup[data-test-subj="lnsMetric_progress_direction_buttons"]' + ) as unknown as ReactWrapper>; + } + + public get progressDirectionShowing() { + return this.progressDirectionControl.exists(); + } + + public setProgressDirection(direction: LayoutDirection) { + this.progressDirectionControl.props().onChange(direction); + this._wrapper.update(); + } } const mockSetState = jest.fn(); - const getHarnessWithState = (state: MetricVisualizationState) => + const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) => new Harness( mountWithIntl( @@ -156,21 +218,25 @@ describe('dimension editor', () => { expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeTruthy(); expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeFalsy(); + expect(component.exists(SELECTORS.MAX_EDITOR)).toBeFalsy(); expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy(); }); describe('static color controls', () => { it('is hidden when dynamic coloring is enabled', () => { - const harnessWithPalette = getHarnessWithState({ ...fullState, palette }); + const harnessWithPalette = getHarnessWithState({ ...metricAccessorState, palette }); expect(harnessWithPalette.colorPicker.exists()).toBeFalsy(); - const harnessNoPalette = getHarnessWithState({ ...fullState, palette: undefined }); + const harnessNoPalette = getHarnessWithState({ + ...metricAccessorState, + palette: undefined, + }); expect(harnessNoPalette.colorPicker.exists()).toBeTruthy(); }); it('fills with default value', () => { const localHarness = getHarnessWithState({ - ...fullState, + ...metricAccessorState, palette: undefined, color: undefined, }); @@ -179,7 +245,7 @@ describe('dimension editor', () => { it('sets color', () => { const localHarness = getHarnessWithState({ - ...fullState, + ...metricAccessorState, palette: undefined, color: 'some-color', }); @@ -200,6 +266,144 @@ describe('dimension editor', () => { `); }); }); + + describe('supporting visualizations', () => { + const stateWOTrend = { + ...metricAccessorState, + trendlineLayerId: undefined, + }; + + describe('reflecting visualization state', () => { + it('should select the correct button', () => { + expect( + getHarnessWithState({ ...stateWOTrend, showBar: false, maxAccessor: undefined }) + .currentSupportingVis + ).toBe('none'); + expect( + getHarnessWithState({ ...stateWOTrend, showBar: true }).currentSupportingVis + ).toBe('bar'); + expect( + getHarnessWithState(metricAccessorState).currentSupportingVis + ).toBe('trendline'); + }); + + it('should disable bar when no max dimension', () => { + expect( + getHarnessWithState({ + ...stateWOTrend, + showBar: false, + maxAccessor: 'something', + }).isDisabled('bar') + ).toBeFalsy(); + expect( + getHarnessWithState({ + ...stateWOTrend, + showBar: false, + maxAccessor: undefined, + }).isDisabled('bar') + ).toBeTruthy(); + }); + + it('should disable trendline when no default time field', () => { + expect( + getHarnessWithState(stateWOTrend, { + hasDefaultTimeField: () => false, + getOperationForColumnId: (id) => ({} as OperationDescriptor), + } as DatasourcePublicAPI).isDisabled('trendline') + ).toBeTruthy(); + expect( + getHarnessWithState(stateWOTrend, { + hasDefaultTimeField: () => true, + getOperationForColumnId: (id) => ({} as OperationDescriptor), + } as DatasourcePublicAPI).isDisabled('trendline') + ).toBeFalsy(); + }); + }); + + it('should disable trendline when a metric dimension has a reduced time range', () => { + expect( + getHarnessWithState(stateWOTrend, { + hasDefaultTimeField: () => true, + getOperationForColumnId: (id) => + ({ hasReducedTimeRange: id === stateWOTrend.metricAccessor } as OperationDescriptor), + } as DatasourcePublicAPI).isDisabled('trendline') + ).toBeTruthy(); + expect( + getHarnessWithState(stateWOTrend, { + hasDefaultTimeField: () => true, + getOperationForColumnId: (id) => + ({ + hasReducedTimeRange: id === stateWOTrend.secondaryMetricAccessor, + } as OperationDescriptor), + } as DatasourcePublicAPI).isDisabled('trendline') + ).toBeTruthy(); + }); + + describe('responding to buttons', () => { + it('enables trendline', () => { + getHarnessWithState(stateWOTrend).setSupportingVis('trendline'); + + expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false }); + expect(props.addLayer).toHaveBeenCalledWith('metricTrendline'); + + expectCalledBefore(mockSetState, props.addLayer as jest.Mock); + }); + + it('enables bar', () => { + getHarnessWithState(metricAccessorState).setSupportingVis('bar'); + + expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: true }); + expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId); + + expectCalledBefore(mockSetState, props.removeLayer as jest.Mock); + }); + + it('selects none from bar', () => { + getHarnessWithState(stateWOTrend).setSupportingVis('none'); + + expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false }); + expect(props.removeLayer).not.toHaveBeenCalled(); + }); + + it('selects none from trendline', () => { + getHarnessWithState(metricAccessorState).setSupportingVis('none'); + + expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: false }); + expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId); + + expectCalledBefore(mockSetState, props.removeLayer as jest.Mock); + }); + }); + + describe('progress bar direction controls', () => { + it('hides direction controls if bar not showing', () => { + expect( + getHarnessWithState({ ...stateWOTrend, showBar: false }).progressDirectionShowing + ).toBeFalsy(); + }); + + it('toggles progress direction', () => { + const harness = getHarnessWithState(metricAccessorState); + + expect(harness.progressDirectionShowing).toBeTruthy(); + expect(harness.currentState.progressDirection).toBe('vertical'); + + harness.setProgressDirection('horizontal'); + harness.setProgressDirection('vertical'); + harness.setProgressDirection('horizontal'); + + expect(mockSetState).toHaveBeenCalledTimes(3); + expect(mockSetState.mock.calls.map((args) => args[0].progressDirection)) + .toMatchInlineSnapshot(` + Array [ + "horizontal", + "vertical", + "horizontal", + ] + `); + }); + }); + }); }); describe('secondary metric dimension', () => { @@ -234,6 +438,7 @@ describe('dimension editor', () => { expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeTruthy(); expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy(); + expect(component.exists(SELECTORS.MAX_EDITOR)).toBeFalsy(); expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeFalsy(); }); @@ -317,81 +522,21 @@ describe('dimension editor', () => { }); describe('maximum dimension', () => { - const accessor = 'maximum-col-id'; - class Harness { - public _wrapper; + const accessor = 'max-col-id'; - constructor( - wrapper: ReactWrapper> - ) { - this._wrapper = wrapper; - } - - private get rootComponent() { - return this._wrapper.find(DimensionEditor); - } - - private get progressDirectionControl() { - return this._wrapper.find(EuiButtonGroup); - } - - public get currentState() { - return this.rootComponent.props().state; - } - - public setProgressDirection(direction: LayoutDirection) { - this.progressDirectionControl.props().onChange(direction); - this._wrapper.update(); - } - - public get progressDirectionDisabled() { - return this.progressDirectionControl.find(EuiButtonGroup).props().isDisabled; - } - - public setMaxCols(max: number) { - act(() => { - this._wrapper.find('EuiFieldNumber[data-test-subj="lnsMetric_max_cols"]').props() - .onChange!({ - target: { value: String(max) }, - } as unknown as FormEvent); - }); - } - } - - let harness: Harness; - const mockSetState = jest.fn(); - - beforeEach(() => { - harness = new Harness( - mountWithIntl( - - ) + it('renders when the accessor matches', () => { + const component = shallow( + ); - }); - - afterEach(() => mockSetState.mockClear()); - - it('toggles progress direction', () => { - expect(harness.currentState.progressDirection).toBe('vertical'); - - harness.setProgressDirection('horizontal'); - harness.setProgressDirection('vertical'); - harness.setProgressDirection('horizontal'); - expect(mockSetState).toHaveBeenCalledTimes(3); - expect(mockSetState.mock.calls.map((args) => args[0].progressDirection)) - .toMatchInlineSnapshot(` - Array [ - "horizontal", - "vertical", - "horizontal", - ] - `); + expect(component.exists(SELECTORS.MAX_EDITOR)).toBeTruthy(); + expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeFalsy(); + expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeFalsy(); + expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy(); }); }); @@ -415,7 +560,7 @@ describe('dimension editor', () => { return this.collapseSetting.props().value; } - public setCollapseFn(fn: string) { + public setCollapseFn(fn: CollapseFunction) { return this.collapseSetting.props().onChange(fn); } @@ -458,6 +603,7 @@ describe('dimension editor', () => { expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeTruthy(); expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeFalsy(); + expect(component.exists(SELECTORS.MAX_EDITOR)).toBeFalsy(); expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeFalsy(); }); diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx index 2f1ea5dc13502..4ca35b060e023 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx @@ -29,7 +29,6 @@ import { DEFAULT_MIN_STOP, } from '@kbn/coloring'; import { getDataBoundsForPalette } from '@kbn/expression-metric-vis-plugin/public'; -import { css } from '@emotion/react'; import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils'; import { @@ -39,10 +38,17 @@ import { } from '../../shared_components'; import type { VisualizationDimensionEditorProps } from '../../types'; import { defaultNumberPaletteParams, defaultPercentagePaletteParams } from './palette_config'; -import { DEFAULT_MAX_COLUMNS, getDefaultColor, MetricVisualizationState } from './visualization'; +import { + DEFAULT_MAX_COLUMNS, + getDefaultColor, + MetricVisualizationState, + showingBar, +} from './visualization'; import { CollapseSetting } from '../../shared_components/collapse_setting'; import { DebouncedInput } from '../../shared_components/debounced_input'; +export type SupportingVisType = 'none' | 'bar' | 'trendline'; + type Props = VisualizationDimensionEditorProps & { paletteService: PaletteRegistry; }; @@ -117,7 +123,7 @@ function BreakdownByEditor({ setState, state }: SubProps) { { + onChange={(collapseFn) => { setState({ ...state, collapseFn, @@ -129,49 +135,7 @@ function BreakdownByEditor({ setState, state }: SubProps) { } function MaximumEditor({ setState, state, idPrefix }: SubProps) { - return ( - - { - const newDirection = id.replace(idPrefix, '') as LayoutDirection; - setState({ - ...state, - progressDirection: newDirection, - }); - }} - /> - - ); + return null; } function SecondaryMetricEditor({ accessor, idPrefix, frame, layerId, setState, state }: SubProps) { @@ -299,17 +263,176 @@ function PrimaryMetricEditor(props: SubProps) { const togglePalette = () => setIsPaletteOpen(!isPaletteOpen); + const supportingVisLabel = i18n.translate('xpack.lens.metric.supportingVis.label', { + defaultMessage: 'Supporting visualization', + }); + + const hasDefaultTimeField = props.datasource?.hasDefaultTimeField(); + const metricHasReducedTimeRange = Boolean( + state.metricAccessor && + props.datasource?.getOperationForColumnId(state.metricAccessor)?.hasReducedTimeRange + ); + const secondaryMetricHasReducedTimeRange = Boolean( + state.secondaryMetricAccessor && + props.datasource?.getOperationForColumnId(state.secondaryMetricAccessor)?.hasReducedTimeRange + ); + + const supportingVisHelpTexts: string[] = []; + + const supportsTrendline = + hasDefaultTimeField && !metricHasReducedTimeRange && !secondaryMetricHasReducedTimeRange; + + if (!supportsTrendline) { + supportingVisHelpTexts.push( + !hasDefaultTimeField + ? i18n.translate('xpack.lens.metric.supportingVis.needDefaultTimeField', { + defaultMessage: 'Use a data view with a default time field to enable trend lines.', + }) + : metricHasReducedTimeRange + ? i18n.translate('xpack.lens.metric.supportingVis.metricHasReducedTimeRange', { + defaultMessage: + 'Remove the reduced time range on this dimension to enable trend lines.', + }) + : secondaryMetricHasReducedTimeRange + ? i18n.translate('xpack.lens.metric.supportingVis.secondaryMetricHasReducedTimeRange', { + defaultMessage: + 'Remove the reduced time range on the secondary metric dimension to enable trend lines.', + }) + : '' + ); + } + + if (!state.maxAccessor) { + supportingVisHelpTexts.push( + i18n.translate('xpack.lens.metric.summportingVis.needMaxDimension', { + defaultMessage: 'Add a maximum dimension to enable the progress bar.', + }) + ); + } + + const buttonIdPrefix = `${idPrefix}--`; + return ( <> + ( +
      {text}
      + ))} + > + { + const supportingVisualizationType = id.split('--')[1] as SupportingVisType; + + switch (supportingVisualizationType) { + case 'trendline': + setState({ + ...state, + showBar: false, + }); + props.addLayer('metricTrendline'); + break; + case 'bar': + setState({ + ...state, + showBar: true, + }); + if (state.trendlineLayerId) props.removeLayer(state.trendlineLayerId); + break; + case 'none': + setState({ + ...state, + showBar: false, + }); + if (state.trendlineLayerId) props.removeLayer(state.trendlineLayerId); + break; + } + }} + /> +
      + {showingBar(state) && ( + + { + const newDirection = id.replace(idPrefix, '') as LayoutDirection; + setState({ + ...state, + progressDirection: newDirection, + }); + }} + /> + + )} {!hasDynamicColoring && } {hasDynamicColoring && ( - <> - + - - - color)} - type={FIXED_PROGRESSION} - onClick={togglePalette} + + color)} + type={FIXED_PROGRESSION} + onClick={togglePalette} + /> + + + + {i18n.translate('xpack.lens.paletteTableGradient.customize', { + defaultMessage: 'Edit', + })} + + + { + setState({ + ...state, + palette: newPalette, + }); + }} /> - - - - {i18n.translate('xpack.lens.paletteTableGradient.customize', { - defaultMessage: 'Edit', - })} - - - { - setState({ - ...state, - palette: newPalette, - }); - }} - /> - - - - - + + + + )} ); @@ -439,7 +560,7 @@ function StaticColorControls({ state, setState }: Pick( { onChange: setColor, - value: state.color || getDefaultColor(!!state.maxAccessor), + value: state.color || getDefaultColor(state), }, { allowFalsyValue: true } ); diff --git a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts new file mode 100644 index 0000000000000..97c8a3c995aab --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts @@ -0,0 +1,172 @@ +/* + * 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 { CustomPaletteParams, CUSTOM_PALETTE, PaletteRegistry } from '@kbn/coloring'; +import { + EXPRESSION_METRIC_NAME, + EXPRESSION_METRIC_TRENDLINE_NAME, +} from '@kbn/expression-metric-vis-plugin/public'; +import { buildExpressionFunction } from '@kbn/expressions-plugin/common'; +import { Ast } from '@kbn/interpreter'; +import { CollapseArgs, CollapseFunction } from '../../../common/expressions'; +import { CollapseExpressionFunction } from '../../../common/expressions/collapse/types'; +import { DatasourceLayers } from '../../types'; +import { showingBar } from './metric_visualization'; +import { DEFAULT_MAX_COLUMNS, getDefaultColor, MetricVisualizationState } from './visualization'; + +// TODO - deduplicate with gauges? +function computePaletteParams(params: CustomPaletteParams) { + return { + ...params, + // rewrite colors and stops as two distinct arguments + colors: (params?.stops || []).map(({ color }) => color), + stops: params?.name === 'custom' ? (params?.stops || []).map(({ stop }) => stop) : [], + reverse: false, // managed at UI level + }; +} + +const getTrendlineExpression = ( + state: MetricVisualizationState, + datasourceExpressionsByLayers: Record +): Ast | undefined => { + if (!state.trendlineLayerId || !state.trendlineMetricAccessor || !state.trendlineTimeAccessor) { + return; + } + + const datasourceExpression = datasourceExpressionsByLayers[state.trendlineLayerId]; + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: EXPRESSION_METRIC_TRENDLINE_NAME, + arguments: { + metric: [state.trendlineMetricAccessor], + timeField: [state.trendlineTimeAccessor], + breakdownBy: + state.trendlineBreakdownByAccessor && !state.collapseFn + ? [state.trendlineBreakdownByAccessor] + : [], + inspectorTableId: [state.trendlineLayerId], + ...(datasourceExpression + ? { + table: [ + { + ...datasourceExpression, + chain: [ + ...datasourceExpression.chain, + ...(state.collapseFn + ? [ + buildExpressionFunction('lens_collapse', { + by: [state.trendlineTimeAccessor], + metric: [state.trendlineMetricAccessor], + fn: [state.collapseFn], + }).toAst(), + ] + : []), + ], + }, + ], + } + : {}), + }, + }, + ], + }; +}; + +export const toExpression = ( + paletteService: PaletteRegistry, + state: MetricVisualizationState, + datasourceLayers: DatasourceLayers, + datasourceExpressionsByLayers: Record | undefined = {} +): Ast | null => { + if (!state.metricAccessor) { + return null; + } + + const datasource = datasourceLayers[state.layerId]; + const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; + + const maxPossibleTiles = + // if there's a collapse function, no need to calculate since we're dealing with a single tile + state.breakdownByAccessor && !state.collapseFn + ? datasource?.getMaxPossibleNumValues(state.breakdownByAccessor) + : null; + + const getCollapseFnArguments = (): CollapseArgs => { + const metric = [state.metricAccessor, state.secondaryMetricAccessor, state.maxAccessor].filter( + Boolean + ) as string[]; + + const collapseFn = state.collapseFn as CollapseFunction; + + const fn = metric.map((accessor) => { + if (accessor !== state.maxAccessor) { + return collapseFn; + } else { + const isMaxStatic = Boolean( + datasource?.getOperationForColumnId(state.maxAccessor!)?.isStaticValue + ); + // we do this because the user expects the static value they set to be the same + // even if they define a collapse on the breakdown by + return isMaxStatic ? 'max' : collapseFn; + } + }); + + return { + by: [], + metric, + fn, + }; + }; + + const collapseExpressionFunction = state.collapseFn + ? buildExpressionFunction( + 'lens_collapse', + getCollapseFnArguments() + ).toAst() + : undefined; + + const trendlineExpression = getTrendlineExpression(state, datasourceExpressionsByLayers); + + return { + type: 'expression', + chain: [ + ...(datasourceExpression?.chain ?? []), + ...(collapseExpressionFunction ? [collapseExpressionFunction] : []), + { + type: 'function', + function: EXPRESSION_METRIC_NAME, + arguments: { + metric: state.metricAccessor ? [state.metricAccessor] : [], + secondaryMetric: state.secondaryMetricAccessor ? [state.secondaryMetricAccessor] : [], + secondaryPrefix: + typeof state.secondaryPrefix !== 'undefined' ? [state.secondaryPrefix] : [], + max: showingBar(state) ? [state.maxAccessor] : [], + breakdownBy: + state.breakdownByAccessor && !state.collapseFn ? [state.breakdownByAccessor] : [], + trendline: trendlineExpression ? [trendlineExpression] : [], + subtitle: state.subtitle ? [state.subtitle] : [], + progressDirection: state.progressDirection ? [state.progressDirection] : [], + color: [state.color || getDefaultColor(state)], + palette: state.palette?.params + ? [ + paletteService + .get(CUSTOM_PALETTE) + .toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)), + ] + : [], + maxCols: [state.maxCols ?? DEFAULT_MAX_COLUMNS], + minTiles: maxPossibleTiles ? [maxPossibleTiles] : [], + inspectorTableId: [state.layerId], + }, + }, + ], + }; +}; diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx index 130c0555af073..a62a8c7c478d8 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx @@ -48,6 +48,13 @@ describe('metric toolbar', () => { maxCols: 5, color: 'static-color', palette, + showBar: true, + trendlineLayerId: 'second', + trendlineLayerType: 'metricTrendline', + trendlineMetricAccessor: 'trendline-metric-col-id', + trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id', + trendlineTimeAccessor: 'trendline-time-col-id', + trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', }; const frame = createMockFramePublicAPI(); diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts index 87b4835187d37..c6194956c2f0b 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts @@ -21,6 +21,7 @@ import { import { GROUP_ID } from './constants'; import { getMetricVisualization, MetricVisualizationState } from './visualization'; import { themeServiceMock } from '@kbn/core/public/mocks'; +import { Ast } from '@kbn/interpreter'; const paletteService = chartPluginMock.createPaletteRegistry(); const theme = themeServiceMock.createStartContract(); @@ -39,7 +40,26 @@ describe('metric visualization', () => { }, }; - const fullState: Required = { + const trendlineProps = { + trendlineLayerId: 'second', + trendlineLayerType: 'metricTrendline', + trendlineMetricAccessor: 'trendline-metric-col-id', + trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id', + trendlineTimeAccessor: 'trendline-time-col-id', + trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', + } as const; + + const fullState: Required< + Omit< + MetricVisualizationState, + | 'trendlineLayerId' + | 'trendlineLayerType' + | 'trendlineMetricAccessor' + | 'trendlineSecondaryMetricAccessor' + | 'trendlineTimeAccessor' + | 'trendlineBreakdownByAccessor' + > + > = { layerId: 'first', layerType: 'data', metricAccessor: 'metric-col-id', @@ -53,6 +73,12 @@ describe('metric visualization', () => { maxCols: 5, color: 'static-color', palette, + showBar: false, + }; + + const fullStateWTrend: Required = { + ...fullState, + ...trendlineProps, }; const mockFrameApi = createMockFramePublicAPI(); @@ -71,149 +97,163 @@ describe('metric visualization', () => { }); describe('dimension groups configuration', () => { - test('generates configuration', () => { - expect( - visualization.getConfiguration({ - state: fullState, - layerId: fullState.layerId, - frame: mockFrameApi, - }) - ).toMatchSnapshot(); - }); + describe('primary layer', () => { + test('generates configuration', () => { + expect( + visualization.getConfiguration({ + state: fullState, + layerId: fullState.layerId, + frame: mockFrameApi, + }) + ).toMatchSnapshot(); + }); - test('color-by-value', () => { - expect( - visualization.getConfiguration({ - state: fullState, - layerId: fullState.layerId, - frame: mockFrameApi, - }).groups[0].accessors - ).toMatchInlineSnapshot(` - Array [ - Object { - "columnId": "metric-col-id", - "palette": Array [], - "triggerIcon": "colorBy", - }, - ] - `); + test('color-by-value', () => { + expect( + visualization.getConfiguration({ + state: fullState, + layerId: fullState.layerId, + frame: mockFrameApi, + }).groups[0].accessors + ).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "metric-col-id", + "palette": Array [], + "triggerIcon": "colorBy", + }, + ] + `); - expect( - visualization.getConfiguration({ - state: { ...fullState, palette: undefined, color: undefined }, - layerId: fullState.layerId, - frame: mockFrameApi, - }).groups[0].accessors - ).toMatchInlineSnapshot(` - Array [ - Object { - "color": "#0077cc", - "columnId": "metric-col-id", - "triggerIcon": "color", - }, - ] - `); - }); + expect( + visualization.getConfiguration({ + state: { ...fullState, palette: undefined, color: undefined }, + layerId: fullState.layerId, + frame: mockFrameApi, + }).groups[0].accessors + ).toMatchInlineSnapshot(` + Array [ + Object { + "color": "#f5f7fa", + "columnId": "metric-col-id", + "triggerIcon": "color", + }, + ] + `); + }); - test('static coloring', () => { - expect( - visualization.getConfiguration({ - state: { ...fullState, palette: undefined }, - layerId: fullState.layerId, - frame: mockFrameApi, - }).groups[0].accessors - ).toMatchInlineSnapshot(` - Array [ - Object { - "color": "static-color", - "columnId": "metric-col-id", - "triggerIcon": "color", - }, - ] - `); + test('static coloring', () => { + expect( + visualization.getConfiguration({ + state: { ...fullState, palette: undefined }, + layerId: fullState.layerId, + frame: mockFrameApi, + }).groups[0].accessors + ).toMatchInlineSnapshot(` + Array [ + Object { + "color": "static-color", + "columnId": "metric-col-id", + "triggerIcon": "color", + }, + ] + `); - expect( - visualization.getConfiguration({ - state: { ...fullState, color: undefined }, - layerId: fullState.layerId, - frame: mockFrameApi, - }).groups[0].accessors - ).toMatchInlineSnapshot(` - Array [ - Object { - "columnId": "metric-col-id", - "palette": Array [], - "triggerIcon": "colorBy", - }, - ] - `); - }); + expect( + visualization.getConfiguration({ + state: { ...fullState, color: undefined }, + layerId: fullState.layerId, + frame: mockFrameApi, + }).groups[0].accessors + ).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "metric-col-id", + "palette": Array [], + "triggerIcon": "colorBy", + }, + ] + `); + }); - test('collapse function', () => { - expect( - visualization.getConfiguration({ - state: fullState, - layerId: fullState.layerId, - frame: mockFrameApi, - }).groups[3].accessors - ).toMatchInlineSnapshot(` - Array [ - Object { - "columnId": "breakdown-col-id", - "triggerIcon": "aggregate", - }, - ] - `); + test('collapse function', () => { + expect( + visualization.getConfiguration({ + state: fullState, + layerId: fullState.layerId, + frame: mockFrameApi, + }).groups[3].accessors + ).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "breakdown-col-id", + "triggerIcon": "aggregate", + }, + ] + `); - expect( - visualization.getConfiguration({ - state: { ...fullState, collapseFn: undefined }, - layerId: fullState.layerId, - frame: mockFrameApi, - }).groups[3].accessors - ).toMatchInlineSnapshot(` - Array [ - Object { - "columnId": "breakdown-col-id", - "triggerIcon": undefined, + expect( + visualization.getConfiguration({ + state: { ...fullState, collapseFn: undefined }, + layerId: fullState.layerId, + frame: mockFrameApi, + }).groups[3].accessors + ).toMatchInlineSnapshot(` + Array [ + Object { + "columnId": "breakdown-col-id", + "triggerIcon": undefined, + }, + ] + `); + }); + + describe('operation filtering', () => { + const unsupportedDataType = 'string'; + + const operations: OperationMetadata[] = [ + { + isBucketed: true, + dataType: 'number', }, - ] - `); - }); + { + isBucketed: true, + dataType: unsupportedDataType, + }, + { + isBucketed: false, + dataType: 'number', + }, + { + isBucketed: false, + dataType: unsupportedDataType, + }, + ]; - describe('operation filtering', () => { - const unsupportedDataType = 'string'; - - const operations: OperationMetadata[] = [ - { - isBucketed: true, - dataType: 'number', - }, - { - isBucketed: true, - dataType: unsupportedDataType, - }, - { - isBucketed: false, - dataType: 'number', - }, - { - isBucketed: false, - dataType: unsupportedDataType, - }, - ]; - - const testConfig = visualization - .getConfiguration({ - state: fullState, - layerId: fullState.layerId, - frame: mockFrameApi, - }) - .groups.map(({ groupId, filterOperations }) => [groupId, filterOperations]); + const testConfig = visualization + .getConfiguration({ + state: fullState, + layerId: fullState.layerId, + frame: mockFrameApi, + }) + .groups.map(({ groupId, filterOperations }) => [groupId, filterOperations]); - it.each(testConfig)('%s supports correct operations', (_, filterFn) => { + it.each(testConfig)('%s supports correct operations', (_, filterFn) => { + expect( + operations.filter(filterFn as (operation: OperationMetadata) => boolean) + ).toMatchSnapshot(); + }); + }); + }); + + describe('trendline layer', () => { + test('generates configuration', () => { expect( - operations.filter(filterFn as (operation: OperationMetadata) => boolean) + visualization.getConfiguration({ + state: fullStateWTrend, + layerId: fullStateWTrend.trendlineLayerId, + frame: mockFrameApi, + }) ).toMatchSnapshot(); }); }); @@ -231,6 +271,7 @@ describe('metric visualization', () => { datasourceLayers = { first: mockDatasource.publicAPIMock, + second: mockDatasource.publicAPIMock, }; }); @@ -263,9 +304,10 @@ describe('metric visualization', () => { "color": Array [ "static-color", ], - "max": Array [ - "max-metric-col-id", + "inspectorTableId": Array [ + "first", ], + "max": Array [], "maxCols": Array [ 5, ], @@ -301,6 +343,7 @@ describe('metric visualization', () => { "subtitle": Array [ "subtitle", ], + "trendline": Array [], }, "function": "metricVis", "type": "function", @@ -324,9 +367,10 @@ describe('metric visualization', () => { "color": Array [ "static-color", ], - "max": Array [ - "max-metric-col-id", + "inspectorTableId": Array [ + "first", ], + "max": Array [], "maxCols": Array [ 5, ], @@ -364,6 +408,7 @@ describe('metric visualization', () => { "subtitle": Array [ "subtitle", ], + "trendline": Array [], }, "function": "metricVis", "type": "function", @@ -374,6 +419,165 @@ describe('metric visualization', () => { `); }); + describe('trendline expression', () => { + const getTrendlineExpression = (state: MetricVisualizationState) => { + const expression = visualization.toExpression( + state, + datasourceLayers, + {}, + { + [trendlineProps.trendlineLayerId]: { chain: [] } as unknown as Ast, + } + ) as ExpressionAstExpression; + + return expression.chain!.find((fn) => fn.function === 'metricVis')!.arguments.trendline[0]; + }; + + it('adds trendline if prerequisites are present', () => { + expect(getTrendlineExpression({ ...fullStateWTrend, collapseFn: undefined })) + .toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "breakdownBy": Array [ + "trendline-breakdown-col-id", + ], + "inspectorTableId": Array [ + "second", + ], + "metric": Array [ + "trendline-metric-col-id", + ], + "table": Array [ + Object { + "chain": Array [], + }, + ], + "timeField": Array [ + "trendline-time-col-id", + ], + }, + "function": "metricTrendline", + "type": "function", + }, + ], + "type": "expression", + } + `); + + expect( + getTrendlineExpression({ ...fullStateWTrend, trendlineBreakdownByAccessor: undefined }) + ).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "breakdownBy": Array [], + "inspectorTableId": Array [ + "second", + ], + "metric": Array [ + "trendline-metric-col-id", + ], + "table": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "by": Array [ + "trendline-time-col-id", + ], + "fn": Array [ + "sum", + ], + "metric": Array [ + "trendline-metric-col-id", + ], + }, + "function": "lens_collapse", + "type": "function", + }, + ], + }, + ], + "timeField": Array [ + "trendline-time-col-id", + ], + }, + "function": "metricTrendline", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('should apply collapse-by fn', () => { + expect(getTrendlineExpression(fullStateWTrend)).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "breakdownBy": Array [], + "inspectorTableId": Array [ + "second", + ], + "metric": Array [ + "trendline-metric-col-id", + ], + "table": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "by": Array [ + "trendline-time-col-id", + ], + "fn": Array [ + "sum", + ], + "metric": Array [ + "trendline-metric-col-id", + ], + }, + "function": "lens_collapse", + "type": "function", + }, + ], + }, + ], + "timeField": Array [ + "trendline-time-col-id", + ], + }, + "function": "metricTrendline", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('no trendline if no trendline layer', () => { + expect( + getTrendlineExpression({ ...fullStateWTrend, trendlineLayerId: undefined }) + ).toBeUndefined(); + }); + + it('no trendline if either metric or timefield are missing', () => { + expect( + getTrendlineExpression({ ...fullStateWTrend, trendlineMetricAccessor: undefined }) + ).toBeUndefined(); + + expect( + getTrendlineExpression({ ...fullStateWTrend, trendlineTimeAccessor: undefined }) + ).toBeUndefined(); + }); + }); + describe('with collapse function', () => { it('builds breakdown by metric with collapse function', () => { const ast = visualization.toExpression( @@ -491,6 +695,7 @@ describe('metric visualization', () => { visualization.toExpression( { ...fullState, + showBar: true, color: undefined, }, datasourceLayers @@ -498,12 +703,42 @@ describe('metric visualization', () => { ).chain[1].arguments.color[0] ).toBe(euiLightVars.euiColorPrimary); + expect( + ( + visualization.toExpression( + { + ...fullState, + showBar: false, + color: undefined, + }, + datasourceLayers + ) as ExpressionAstExpression + ).chain[1].arguments.color[0] + ).toBe(euiLightVars.euiColorLightestShade); + + expect( + ( + visualization.toExpression( + { + ...fullState, + maxAccessor: undefined, + showBar: false, + color: undefined, + }, + datasourceLayers + ) as ExpressionAstExpression + ).chain[1].arguments.color[0] + ).toBe(euiLightVars.euiColorLightestShade); + + // this case isn't currently relevant because other parts of the code don't allow showBar to be + // set when there isn't a max dimension but this test covers the branch anyhow expect( ( visualization.toExpression( { ...fullState, maxAccessor: undefined, + showBar: true, color: undefined, }, datasourceLayers @@ -523,8 +758,141 @@ describe('metric visualization', () => { `); }); - test('getLayerIds returns the single layer ID', () => { + it('appends a trendline layer', () => { + const newLayerId = 'new-layer-id'; + const chk = visualization.appendLayer!(fullState, newLayerId, 'metricTrendline', ''); + expect(chk.trendlineLayerId).toBe(newLayerId); + expect(chk.trendlineLayerType).toBe('metricTrendline'); + }); + + it('removes trendline layer', () => { + const chk = visualization.removeLayer!(fullStateWTrend, fullStateWTrend.trendlineLayerId); + expect(chk.trendlineLayerId).toBeUndefined(); + expect(chk.trendlineLayerType).toBeUndefined(); + expect(chk.trendlineMetricAccessor).toBeUndefined(); + expect(chk.trendlineTimeAccessor).toBeUndefined(); + expect(chk.trendlineBreakdownByAccessor).toBeUndefined(); + }); + + test('getLayerIds', () => { expect(visualization.getLayerIds(fullState)).toEqual([fullState.layerId]); + expect(visualization.getLayerIds(fullStateWTrend)).toEqual([ + fullStateWTrend.layerId, + fullStateWTrend.trendlineLayerId, + ]); + }); + + test('getLayersToLinkTo', () => { + expect( + visualization.getLayersToLinkTo!(fullStateWTrend, fullStateWTrend.trendlineLayerId) + ).toEqual([fullStateWTrend.layerId]); + expect(visualization.getLayersToLinkTo!(fullStateWTrend, 'foo-id')).toEqual([]); + }); + + describe('linked dimensions', () => { + it('doesnt report links when no trendline layer', () => { + expect(visualization.getLinkedDimensions!(fullState)).toHaveLength(0); + }); + + it('links metrics when present on leader layer', () => { + const localState: MetricVisualizationState = { + ...fullStateWTrend, + breakdownByAccessor: undefined, + secondaryMetricAccessor: undefined, + }; + + expect(visualization.getLinkedDimensions!(localState)).toMatchInlineSnapshot(` + Array [ + Object { + "from": Object { + "columnId": "metric-col-id", + "groupId": "metric", + "layerId": "first", + }, + "to": Object { + "columnId": "trendline-metric-col-id", + "groupId": "trendMetric", + "layerId": "second", + }, + }, + ] + `); + + const newColumnId = visualization.getLinkedDimensions!({ + ...localState, + trendlineMetricAccessor: undefined, + })[0].to.columnId; + expect(newColumnId).toBeUndefined(); + }); + + it('links secondary metrics when present on leader layer', () => { + const localState: MetricVisualizationState = { + ...fullStateWTrend, + metricAccessor: undefined, + breakdownByAccessor: undefined, + }; + + expect(visualization.getLinkedDimensions!(localState)).toMatchInlineSnapshot(` + Array [ + Object { + "from": Object { + "columnId": "secondary-metric-col-id", + "groupId": "secondaryMetric", + "layerId": "first", + }, + "to": Object { + "columnId": "trendline-secondary-metric-col-id", + "groupId": "trendSecondaryMetric", + "layerId": "second", + }, + }, + ] + `); + + const newColumnId = visualization.getLinkedDimensions!({ + ...localState, + trendlineSecondaryMetricAccessor: undefined, + })[0].to.columnId; + expect(newColumnId).toBeUndefined(); + }); + + it('links breakdowns when present', () => { + const localState: MetricVisualizationState = { + ...fullStateWTrend, + metricAccessor: undefined, + secondaryMetricAccessor: undefined, + }; + + expect(visualization.getLinkedDimensions!(localState)).toMatchInlineSnapshot(` + Array [ + Object { + "from": Object { + "columnId": "breakdown-col-id", + "groupId": "breakdownBy", + "layerId": "first", + }, + "to": Object { + "columnId": "trendline-breakdown-col-id", + "groupId": "trendBreakdownBy", + "layerId": "second", + }, + }, + ] + `); + + const newColumnId = visualization.getLinkedDimensions!({ + ...localState, + trendlineBreakdownByAccessor: undefined, + })[0].to.columnId; + expect(newColumnId).toBeUndefined(); + }); + }); + + it('marks trendline layer for removal on index pattern switch', () => { + expect(visualization.getLayersToRemoveOnIndexPatternChange!(fullStateWTrend)).toEqual([ + fullStateWTrend.trendlineLayerId, + ]); + expect(visualization.getLayersToRemoveOnIndexPatternChange!(fullState)).toEqual([]); }); it('gives a description', () => { @@ -540,15 +908,32 @@ describe('metric visualization', () => { it('works without state', () => { const supportedLayers = visualization.getSupportedLayers(); expect(supportedLayers[0].initialDimensions).toBeUndefined(); - expect(supportedLayers).toMatchInlineSnapshot(` - Array [ - Object { - "initialDimensions": undefined, - "label": "Visualization", - "type": "data", - }, - ] + expect(supportedLayers[0]).toMatchInlineSnapshot(` + Object { + "canAddViaMenu": true, + "disabled": true, + "initialDimensions": undefined, + "label": "Visualization", + "type": "data", + } + `); + + expect({ ...supportedLayers[1], initialDimensions: undefined }).toMatchInlineSnapshot(` + Object { + "canAddViaMenu": true, + "disabled": false, + "initialDimensions": undefined, + "label": "Trendline", + "type": "metricTrendline", + } `); + + expect(supportedLayers[1].initialDimensions).toHaveLength(1); + expect(supportedLayers[1].initialDimensions![0]).toMatchObject({ + groupId: GROUP_ID.TREND_TIME, + autoTimeField: true, + columnId: expect.any(String), + }); }); it('includes max static value dimension when state provided', () => { @@ -563,52 +948,65 @@ describe('metric visualization', () => { }); }); - it('sets dimensions', () => { + describe('setting dimensions', () => { const state = {} as MetricVisualizationState; const columnId = 'col-id'; - expect( - visualization.setDimension({ - prevState: state, - columnId, - groupId: GROUP_ID.METRIC, - layerId: 'some-id', - frame: mockFrameApi, - }) - ).toEqual({ - metricAccessor: columnId, - }); - expect( - visualization.setDimension({ - prevState: state, - columnId, - groupId: GROUP_ID.SECONDARY_METRIC, - layerId: 'some-id', - frame: mockFrameApi, - }) - ).toEqual({ - secondaryMetricAccessor: columnId, + + const cases: Array<{ + groupId: typeof GROUP_ID[keyof typeof GROUP_ID]; + accessor: keyof MetricVisualizationState; + }> = [ + { groupId: GROUP_ID.METRIC, accessor: 'metricAccessor' }, + { groupId: GROUP_ID.SECONDARY_METRIC, accessor: 'secondaryMetricAccessor' }, + { groupId: GROUP_ID.MAX, accessor: 'maxAccessor' }, + { groupId: GROUP_ID.BREAKDOWN_BY, accessor: 'breakdownByAccessor' }, + { groupId: GROUP_ID.TREND_METRIC, accessor: 'trendlineMetricAccessor' }, + { groupId: GROUP_ID.TREND_SECONDARY_METRIC, accessor: 'trendlineSecondaryMetricAccessor' }, + { groupId: GROUP_ID.TREND_TIME, accessor: 'trendlineTimeAccessor' }, + { groupId: GROUP_ID.TREND_BREAKDOWN_BY, accessor: 'trendlineBreakdownByAccessor' }, + ]; + + it.each(cases)('sets %s', ({ groupId, accessor }) => { + expect( + visualization.setDimension({ + prevState: state, + columnId, + groupId, + layerId: 'some-id', + frame: mockFrameApi, + }) + ).toEqual( + expect.objectContaining({ + [accessor]: columnId, + }) + ); }); - expect( - visualization.setDimension({ - prevState: state, - columnId, - groupId: GROUP_ID.MAX, - layerId: 'some-id', - frame: mockFrameApi, - }) - ).toEqual({ - maxAccessor: columnId, + + it('shows the progress bar when maximum dimension set', () => { + expect( + visualization.setDimension({ + prevState: state, + columnId, + groupId: GROUP_ID.MAX, + layerId: 'some-id', + frame: mockFrameApi, + }) + ).toEqual({ + maxAccessor: columnId, + showBar: true, + }); }); - expect( - visualization.setDimension({ - prevState: state, - columnId, - groupId: GROUP_ID.BREAKDOWN_BY, - layerId: 'some-id', - frame: mockFrameApi, - }) - ).toEqual({ - breakdownByAccessor: columnId, + + it('does NOT show the progress bar when maximum dimension set when trendline enabled', () => { + expect( + visualization.setDimension({ + prevState: { ...state, ...trendlineProps }, + columnId, + groupId: GROUP_ID.MAX, + layerId: 'some-id', + frame: mockFrameApi, + }) + ).not.toHaveProperty('showBar'); }); }); @@ -619,7 +1017,7 @@ describe('metric visualization', () => { layerId: 'some-id', columnId: '', frame: mockFrameApi, - prevState: fullState, + prevState: fullStateWTrend, }; it('removes metric dimension', () => { @@ -660,6 +1058,38 @@ describe('metric visualization', () => { expect(removed).not.toHaveProperty('collapseFn'); expect(removed).not.toHaveProperty('maxCols'); }); + it('removes trend time dimension', () => { + const removed = visualization.removeDimension({ + ...removeDimensionParam, + columnId: fullStateWTrend.trendlineTimeAccessor, + }); + + expect(removed).not.toHaveProperty('trendlineTimeAccessor'); + }); + it('removes trend metric dimension', () => { + const removed = visualization.removeDimension({ + ...removeDimensionParam, + columnId: fullStateWTrend.trendlineMetricAccessor, + }); + + expect(removed).not.toHaveProperty('trendlineMetricAccessor'); + }); + it('removes trend secondary metric dimension', () => { + const removed = visualization.removeDimension({ + ...removeDimensionParam, + columnId: fullStateWTrend.trendlineSecondaryMetricAccessor, + }); + + expect(removed).not.toHaveProperty('trendlineSecondaryMetricAccessor'); + }); + it('removes trend breakdown-by dimension', () => { + const removed = visualization.removeDimension({ + ...removeDimensionParam, + columnId: fullStateWTrend.trendlineBreakdownByAccessor, + }); + + expect(removed).not.toHaveProperty('trendlineBreakdownByAccessor'); + }); }); it('implements custom display options', () => { diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index 776944bbdd62c..95c9785b2e9fe 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -9,23 +9,24 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { render } from 'react-dom'; -import { Ast, AstFunction } from '@kbn/interpreter'; -import { PaletteOutput, PaletteRegistry, CUSTOM_PALETTE, CustomPaletteParams } from '@kbn/coloring'; +import { PaletteOutput, PaletteRegistry, CustomPaletteParams } from '@kbn/coloring'; import { ThemeServiceStart } from '@kbn/core/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { LayoutDirection } from '@elastic/charts'; import { euiLightVars, euiThemeVars } from '@kbn/ui-theme'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { IconChartMetric } from '@kbn/chart-icons'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import { CollapseFunction } from '../../../common/expressions'; import type { LayerType } from '../../../common'; +import { layerTypes } from '../../../common/layer_types'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import { getSuggestions } from './suggestions'; import { Visualization, OperationMetadata, - DatasourceLayers, AccessorConfig, + VisualizationConfigProps, + VisualizationDimensionGroupConfig, Suggestion, } from '../../types'; import { GROUP_ID, LENS_METRIC_ID } from './constants'; @@ -33,11 +34,17 @@ import { DimensionEditor } from './dimension_editor'; import { Toolbar } from './toolbar'; import { generateId } from '../../id_generator'; import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector'; +import { toExpression } from './to_expression'; export const DEFAULT_MAX_COLUMNS = 3; -export const getDefaultColor = (hasMax: boolean) => - hasMax ? euiLightVars.euiColorPrimary : euiThemeVars.euiColorLightestShade; +export const showingBar = ( + state: MetricVisualizationState +): state is MetricVisualizationState & { showBar: true; maxAccessor: string } => + Boolean(state.showBar && state.maxAccessor); + +export const getDefaultColor = (state: MetricVisualizationState) => + showingBar(state) ? euiLightVars.euiColorPrimary : euiThemeVars.euiColorLightestShade; export interface MetricVisualizationState { layerId: string; @@ -48,120 +55,250 @@ export interface MetricVisualizationState { breakdownByAccessor?: string; // the dimensions can optionally be single numbers // computed by collapsing all rows - collapseFn?: string; + collapseFn?: CollapseFunction; subtitle?: string; secondaryPrefix?: string; progressDirection?: LayoutDirection; + showBar?: boolean; color?: string; palette?: PaletteOutput; maxCols?: number; + + trendlineLayerId?: string; + trendlineLayerType?: LayerType; + trendlineTimeAccessor?: string; + trendlineMetricAccessor?: string; + trendlineSecondaryMetricAccessor?: string; + trendlineBreakdownByAccessor?: string; } export const supportedDataTypes = new Set(['number']); -// TODO - deduplicate with gauges? -function computePaletteParams(params: CustomPaletteParams) { - return { - ...params, - // rewrite colors and stops as two distinct arguments - colors: (params?.stops || []).map(({ color }) => color), - stops: params?.name === 'custom' ? (params?.stops || []).map(({ stop }) => stop) : [], - reverse: false, // managed at UI level - }; -} +export const metricLabel = i18n.translate('xpack.lens.metric.label', { + defaultMessage: 'Metric', +}); +const metricGroupLabel = i18n.translate('xpack.lens.metric.groupLabel', { + defaultMessage: 'Goal and single value', +}); -const toExpression = ( - paletteService: PaletteRegistry, - state: MetricVisualizationState, - datasourceLayers: DatasourceLayers, - datasourceExpressionsByLayers: Record | undefined = {} -): Ast | null => { - if (!state.metricAccessor) { - return null; - } - - const datasource = datasourceLayers[state.layerId]; - const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; - - const maxPossibleTiles = - // if there's a collapse function, no need to calculate since we're dealing with a single tile - state.breakdownByAccessor && !state.collapseFn - ? datasource?.getMaxPossibleNumValues(state.breakdownByAccessor) - : null; - - const getCollapseFnArguments = () => { - const metric = [state.metricAccessor, state.secondaryMetricAccessor, state.maxAccessor].filter( - Boolean - ); +const getMetricLayerConfiguration = ( + props: VisualizationConfigProps +): { + groups: VisualizationDimensionGroupConfig[]; +} => { + const isSupportedMetric = (op: OperationMetadata) => + !op.isBucketed && supportedDataTypes.has(op.dataType); + + const isSupportedDynamicMetric = (op: OperationMetadata) => + !op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue; + + const getPrimaryAccessorDisplayConfig = (): Partial => { + const stops = props.state.palette?.params?.stops || []; + const hasStaticColoring = !!props.state.color; + const hasDynamicColoring = !!props.state.palette; + return hasDynamicColoring + ? { + triggerIcon: 'colorBy', + palette: stops.map(({ color }) => color), + } + : hasStaticColoring + ? { + triggerIcon: 'color', + color: props.state.color, + } + : { + triggerIcon: 'color', + color: getDefaultColor(props.state), + }; + }; - const fn = metric.map((accessor) => { - if (accessor !== state.maxAccessor) { - return state.collapseFn; - } else { - const isMaxStatic = Boolean( - datasource?.getOperationForColumnId(state.maxAccessor!)?.isStaticValue - ); - // we do this because the user expects the static value they set to be the same - // even if they define a collapse on the breakdown by - return isMaxStatic ? 'max' : state.collapseFn; - } - }); + const isBucketed = (op: OperationMetadata) => op.isBucketed; - return { - by: [], - metric, - fn, - }; + const formatterOptions: FormatSelectorOptions = { + disableExtraOptions: true, }; return { - type: 'expression', - chain: [ - ...(datasourceExpression?.chain ?? []), - ...(state.collapseFn - ? [ - { - type: 'function', - function: 'lens_collapse', - arguments: getCollapseFnArguments(), - } as AstFunction, - ] - : []), + groups: [ + { + groupId: GROUP_ID.METRIC, + dataTestSubj: 'lnsMetric_primaryMetricDimensionPanel', + groupLabel: i18n.translate('xpack.lens.primaryMetric.label', { + defaultMessage: 'Primary metric', + }), + paramEditorCustomProps: { + headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', { + defaultMessage: 'Value', + }), + }, + accessors: props.state.metricAccessor + ? [ + { + columnId: props.state.metricAccessor, + ...getPrimaryAccessorDisplayConfig(), + }, + ] + : [], + supportsMoreColumns: !props.state.metricAccessor, + filterOperations: isSupportedDynamicMetric, + enableDimensionEditor: true, + enableFormatSelector: true, + formatSelectorOptions: formatterOptions, + requiredMinDimensionCount: 1, + }, { - type: 'function', - function: 'metricVis', // TODO import from plugin - arguments: { - metric: state.metricAccessor ? [state.metricAccessor] : [], - secondaryMetric: state.secondaryMetricAccessor ? [state.secondaryMetricAccessor] : [], - secondaryPrefix: - typeof state.secondaryPrefix !== 'undefined' ? [state.secondaryPrefix] : [], - max: state.maxAccessor ? [state.maxAccessor] : [], - breakdownBy: - state.breakdownByAccessor && !state.collapseFn ? [state.breakdownByAccessor] : [], - subtitle: state.subtitle ? [state.subtitle] : [], - progressDirection: state.progressDirection ? [state.progressDirection] : [], - color: [state.color || getDefaultColor(!!state.maxAccessor)], - palette: state.palette?.params - ? [ - paletteService - .get(CUSTOM_PALETTE) - .toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)), - ] - : [], - maxCols: [state.maxCols ?? DEFAULT_MAX_COLUMNS], - minTiles: maxPossibleTiles ? [maxPossibleTiles] : [], + groupId: GROUP_ID.SECONDARY_METRIC, + dataTestSubj: 'lnsMetric_secondaryMetricDimensionPanel', + groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', { + defaultMessage: 'Secondary metric', + }), + paramEditorCustomProps: { + headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', { + defaultMessage: 'Value', + }), }, + accessors: props.state.secondaryMetricAccessor + ? [ + { + columnId: props.state.secondaryMetricAccessor, + }, + ] + : [], + supportsMoreColumns: !props.state.secondaryMetricAccessor, + filterOperations: isSupportedDynamicMetric, + enableDimensionEditor: true, + enableFormatSelector: true, + formatSelectorOptions: formatterOptions, + }, + { + groupId: GROUP_ID.MAX, + dataTestSubj: 'lnsMetric_maxDimensionPanel', + groupLabel: i18n.translate('xpack.lens.metric.max', { defaultMessage: 'Maximum value' }), + paramEditorCustomProps: { + headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', { + defaultMessage: 'Value', + }), + }, + accessors: props.state.maxAccessor + ? [ + { + columnId: props.state.maxAccessor, + }, + ] + : [], + supportsMoreColumns: !props.state.maxAccessor, + filterOperations: isSupportedMetric, + enableDimensionEditor: true, + enableFormatSelector: false, + formatSelectorOptions: formatterOptions, + supportStaticValue: true, + prioritizedOperation: 'max', + groupTooltip: i18n.translate('xpack.lens.metric.maxTooltip', { + defaultMessage: 'If the maximum value is specified, the minimum value is fixed at zero.', + }), + }, + { + groupId: GROUP_ID.BREAKDOWN_BY, + dataTestSubj: 'lnsMetric_breakdownByDimensionPanel', + groupLabel: i18n.translate('xpack.lens.metric.breakdownBy', { + defaultMessage: 'Break down by', + }), + accessors: props.state.breakdownByAccessor + ? [ + { + columnId: props.state.breakdownByAccessor, + triggerIcon: props.state.collapseFn ? ('aggregate' as const) : undefined, + }, + ] + : [], + supportsMoreColumns: !props.state.breakdownByAccessor, + filterOperations: isBucketed, + enableDimensionEditor: true, + enableFormatSelector: true, + formatSelectorOptions: formatterOptions, }, ], }; }; -export const metricLabel = i18n.translate('xpack.lens.metric.label', { - defaultMessage: 'Metric', -}); -const metricGroupLabel = i18n.translate('xpack.lens.metric.groupLabel', { - defaultMessage: 'Goal and single value', -}); +const getTrendlineLayerConfiguration = ( + props: VisualizationConfigProps +): { + hidden: boolean; + groups: VisualizationDimensionGroupConfig[]; +} => { + return { + hidden: true, + groups: [ + { + groupId: GROUP_ID.TREND_METRIC, + groupLabel: i18n.translate('xpack.lens.primaryMetric.label', { + defaultMessage: 'Primary metric', + }), + accessors: props.state.trendlineMetricAccessor + ? [ + { + columnId: props.state.trendlineMetricAccessor, + }, + ] + : [], + supportsMoreColumns: !props.state.trendlineMetricAccessor, + filterOperations: () => false, + hideGrouping: true, + nestingOrder: 3, + }, + { + groupId: GROUP_ID.TREND_SECONDARY_METRIC, + groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', { + defaultMessage: 'Secondary metric', + }), + accessors: props.state.trendlineSecondaryMetricAccessor + ? [ + { + columnId: props.state.trendlineSecondaryMetricAccessor, + }, + ] + : [], + supportsMoreColumns: !props.state.trendlineSecondaryMetricAccessor, + filterOperations: () => false, + hideGrouping: true, + nestingOrder: 2, + }, + { + groupId: GROUP_ID.TREND_TIME, + groupLabel: i18n.translate('xpack.lens.metric.timeField', { defaultMessage: 'Time field' }), + accessors: props.state.trendlineTimeAccessor + ? [ + { + columnId: props.state.trendlineTimeAccessor, + }, + ] + : [], + supportsMoreColumns: !props.state.trendlineTimeAccessor, + filterOperations: () => false, + hideGrouping: true, + nestingOrder: 1, + }, + { + groupId: GROUP_ID.TREND_BREAKDOWN_BY, + groupLabel: i18n.translate('xpack.lens.metric.breakdownBy', { + defaultMessage: 'Break down by', + }), + accessors: props.state.trendlineBreakdownByAccessor + ? [ + { + columnId: props.state.trendlineBreakdownByAccessor, + }, + ] + : [], + supportsMoreColumns: !props.state.trendlineBreakdownByAccessor, + filterOperations: () => false, + hideGrouping: true, + nestingOrder: 0, + }, + ], + }; +}; const removeMetricDimension = (state: MetricVisualizationState) => { delete state.metricAccessor; @@ -177,6 +314,7 @@ const removeSecondaryMetricDimension = (state: MetricVisualizationState) => { const removeMaxDimension = (state: MetricVisualizationState) => { delete state.maxAccessor; delete state.progressDirection; + delete state.showBar; }; const removeBreakdownByDimension = (state: MetricVisualizationState) => { @@ -222,7 +360,7 @@ export const getMetricVisualization = ({ }, getLayerIds(state) { - return [state.layerId]; + return state.trendlineLayerId ? [state.layerId, state.trendlineLayerId] : [state.layerId]; }, getDescription() { @@ -238,7 +376,7 @@ export const getMetricVisualization = ({ return ( state ?? { layerId: addNewLayer(), - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, palette: mainPalette, } ); @@ -246,153 +384,25 @@ export const getMetricVisualization = ({ triggers: [VIS_EVENT_TO_TRIGGER.filter], getConfiguration(props) { - const isSupportedMetric = (op: OperationMetadata) => - !op.isBucketed && supportedDataTypes.has(op.dataType); - - const isSupportedDynamicMetric = (op: OperationMetadata) => - !op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue; - - const getPrimaryAccessorDisplayConfig = (): Partial => { - const stops = props.state.palette?.params?.stops || []; - const hasStaticColoring = !!props.state.color; - const hasDynamicColoring = !!props.state.palette; - return hasDynamicColoring - ? { - triggerIcon: 'colorBy', - palette: stops.map(({ color }) => color), - } - : hasStaticColoring - ? { - triggerIcon: 'color', - color: props.state.color, - } - : { - triggerIcon: 'color', - color: getDefaultColor(!!props.state.maxAccessor), - }; - }; - - const isBucketed = (op: OperationMetadata) => op.isBucketed; + return props.layerId === props.state.layerId + ? getMetricLayerConfiguration(props) + : getTrendlineLayerConfiguration(props); + }, - const formatterOptions: FormatSelectorOptions = { - disableExtraOptions: true, - }; + getLayerType(layerId, state) { + if (state?.layerId === layerId) { + return state.layerType; + } - return { - groups: [ - { - groupId: GROUP_ID.METRIC, - dataTestSubj: 'lnsMetric_primaryMetricDimensionPanel', - groupLabel: i18n.translate('xpack.lens.primaryMetric.label', { - defaultMessage: 'Primary metric', - }), - paramEditorCustomProps: { - headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', { - defaultMessage: 'Value', - }), - }, - layerId: props.state.layerId, - accessors: props.state.metricAccessor - ? [ - { - columnId: props.state.metricAccessor, - ...getPrimaryAccessorDisplayConfig(), - }, - ] - : [], - supportsMoreColumns: !props.state.metricAccessor, - filterOperations: isSupportedDynamicMetric, - enableDimensionEditor: true, - enableFormatSelector: true, - formatSelectorOptions: formatterOptions, - requiredMinDimensionCount: 1, - }, - { - groupId: GROUP_ID.SECONDARY_METRIC, - dataTestSubj: 'lnsMetric_secondaryMetricDimensionPanel', - groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', { - defaultMessage: 'Secondary metric', - }), - paramEditorCustomProps: { - headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', { - defaultMessage: 'Value', - }), - }, - layerId: props.state.layerId, - accessors: props.state.secondaryMetricAccessor - ? [ - { - columnId: props.state.secondaryMetricAccessor, - }, - ] - : [], - supportsMoreColumns: !props.state.secondaryMetricAccessor, - filterOperations: isSupportedDynamicMetric, - enableDimensionEditor: true, - enableFormatSelector: true, - formatSelectorOptions: formatterOptions, - requiredMinDimensionCount: 0, - }, - { - groupId: GROUP_ID.MAX, - dataTestSubj: 'lnsMetric_maxDimensionPanel', - groupLabel: i18n.translate('xpack.lens.metric.max', { defaultMessage: 'Maximum value' }), - paramEditorCustomProps: { - headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', { - defaultMessage: 'Value', - }), - }, - layerId: props.state.layerId, - accessors: props.state.maxAccessor - ? [ - { - columnId: props.state.maxAccessor, - }, - ] - : [], - supportsMoreColumns: !props.state.maxAccessor, - filterOperations: isSupportedMetric, - enableDimensionEditor: true, - enableFormatSelector: false, - formatSelectorOptions: formatterOptions, - supportStaticValue: true, - prioritizedOperation: 'max', - requiredMinDimensionCount: 0, - groupTooltip: i18n.translate('xpack.lens.metric.maxTooltip', { - defaultMessage: - 'If the maximum value is specified, the minimum value is fixed at zero.', - }), - }, - { - groupId: GROUP_ID.BREAKDOWN_BY, - dataTestSubj: 'lnsMetric_breakdownByDimensionPanel', - groupLabel: i18n.translate('xpack.lens.metric.breakdownBy', { - defaultMessage: 'Break down by', - }), - layerId: props.state.layerId, - accessors: props.state.breakdownByAccessor - ? [ - { - columnId: props.state.breakdownByAccessor, - triggerIcon: props.state.collapseFn ? ('aggregate' as const) : undefined, - }, - ] - : [], - supportsMoreColumns: !props.state.breakdownByAccessor, - filterOperations: isBucketed, - enableDimensionEditor: true, - enableFormatSelector: true, - formatSelectorOptions: formatterOptions, - requiredMinDimensionCount: 0, - }, - ], - }; + if (state?.trendlineLayerId === layerId) { + return state.trendlineLayerType; + } }, getSupportedLayers(state) { return [ { - type: LayerTypes.DATA, + type: layerTypes.DATA, label: i18n.translate('xpack.lens.metric.addLayer', { defaultMessage: 'Visualization', }), @@ -405,14 +415,112 @@ export const getMetricVisualization = ({ }, ] : undefined, + disabled: true, + canAddViaMenu: true, + }, + { + type: layerTypes.METRIC_TRENDLINE, + label: i18n.translate('xpack.lens.metric.layerType.trendLine', { + defaultMessage: 'Trendline', + }), + initialDimensions: [ + { groupId: GROUP_ID.TREND_TIME, columnId: generateId(), autoTimeField: true }, + ], + disabled: Boolean(state?.trendlineLayerId), + canAddViaMenu: true, }, ]; }, - getLayerType(layerId, state) { - if (state?.layerId === layerId) { - return state.layerType; + appendLayer(state, layerId, layerType) { + if (layerType !== layerTypes.METRIC_TRENDLINE) { + throw new Error(`Metric vis only supports layers of type ${layerTypes.METRIC_TRENDLINE}!`); } + + return { ...state, trendlineLayerId: layerId, trendlineLayerType: layerType }; + }, + + removeLayer(state) { + const newState: MetricVisualizationState = { + ...state, + trendlineLayerId: undefined, + trendlineLayerType: undefined, + trendlineMetricAccessor: undefined, + trendlineTimeAccessor: undefined, + trendlineBreakdownByAccessor: undefined, + }; + + return newState; + }, + + getLayersToLinkTo(state, newLayerId: string): string[] { + return newLayerId === state.trendlineLayerId ? [state.layerId] : []; + }, + + getLinkedDimensions(state) { + if (!state.trendlineLayerId) { + return []; + } + + const links: Array<{ + from: { columnId: string; groupId: string; layerId: string }; + to: { + columnId?: string; + groupId: string; + layerId: string; + }; + }> = []; + + if (state.metricAccessor) { + links.push({ + from: { + columnId: state.metricAccessor, + groupId: GROUP_ID.METRIC, + layerId: state.layerId, + }, + to: { + columnId: state.trendlineMetricAccessor, + groupId: GROUP_ID.TREND_METRIC, + layerId: state.trendlineLayerId, + }, + }); + } + + if (state.secondaryMetricAccessor) { + links.push({ + from: { + columnId: state.secondaryMetricAccessor, + groupId: GROUP_ID.SECONDARY_METRIC, + layerId: state.layerId, + }, + to: { + columnId: state.trendlineSecondaryMetricAccessor, + groupId: GROUP_ID.TREND_SECONDARY_METRIC, + layerId: state.trendlineLayerId, + }, + }); + } + + if (state.breakdownByAccessor) { + links.push({ + from: { + columnId: state.breakdownByAccessor, + groupId: GROUP_ID.BREAKDOWN_BY, + layerId: state.layerId, + }, + to: { + columnId: state.trendlineBreakdownByAccessor, + groupId: GROUP_ID.TREND_BREAKDOWN_BY, + layerId: state.trendlineLayerId, + }, + }); + } + + return links; + }, + + getLayersToRemoveOnIndexPatternChange: (state) => { + return state.trendlineLayerId ? [state.trendlineLayerId] : []; }, toExpression: (state, datasourceLayers, attributes, datasourceExpressionsByLayers) => @@ -430,10 +538,25 @@ export const getMetricVisualization = ({ break; case GROUP_ID.MAX: updated.maxAccessor = columnId; + if (!prevState.trendlineLayerId) { + updated.showBar = true; + } break; case GROUP_ID.BREAKDOWN_BY: updated.breakdownByAccessor = columnId; break; + case GROUP_ID.TREND_TIME: + updated.trendlineTimeAccessor = columnId; + break; + case GROUP_ID.TREND_METRIC: + updated.trendlineMetricAccessor = columnId; + break; + case GROUP_ID.TREND_SECONDARY_METRIC: + updated.trendlineSecondaryMetricAccessor = columnId; + break; + case GROUP_ID.TREND_BREAKDOWN_BY: + updated.trendlineBreakdownByAccessor = columnId; + break; } return updated; @@ -455,6 +578,19 @@ export const getMetricVisualization = ({ removeBreakdownByDimension(updated); } + if (prevState.trendlineTimeAccessor === columnId) { + delete updated.trendlineTimeAccessor; + } + if (prevState.trendlineMetricAccessor === columnId) { + delete updated.trendlineMetricAccessor; + } + if (prevState.trendlineSecondaryMetricAccessor === columnId) { + delete updated.trendlineSecondaryMetricAccessor; + } + if (prevState.trendlineBreakdownByAccessor === columnId) { + delete updated.trendlineBreakdownByAccessor; + } + return updated; }, diff --git a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx index 4dcb01c3e93f9..10d34a4f891ae 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx @@ -331,7 +331,7 @@ export function DimensionEditor( )} { + onChange={(collapseFn) => { props.setState({ ...props.state, layers: props.state.layers.map((layer) => diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts index 25975ffb6ec7d..a08250cb3d5b9 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts @@ -20,6 +20,7 @@ import { FramePublicAPI } from '../../types'; import { themeServiceMock } from '@kbn/core/public/mocks'; import { cloneDeep } from 'lodash'; import { PartitionChartsMeta } from './partition_charts_meta'; +import { CollapseFunction } from '../../../common/expressions'; jest.mock('../../id_generator'); @@ -76,7 +77,7 @@ describe('pie_visualization', () => { it("doesn't count collapsed dimensions", () => { state.layers[0].collapseFns = { - [colIds[0]]: 'some-fn', + [colIds[0]]: 'some-fn' as CollapseFunction, }; expect(pieVisualization.getErrorMessages(state)).toHaveLength(0); diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index 465c81ca2e3e5..5530ea928fc68 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -36,6 +36,7 @@ import { } from '@kbn/chart-icons'; import { DistributiveOmit } from '@elastic/eui'; +import { CollapseFunction } from '../../../common/expressions'; import type { VisualizationType } from '../../types'; import type { ValueLabelConfig } from '../../../common/types'; @@ -99,7 +100,7 @@ export interface XYDataLayerConfig { yConfig?: YConfig[]; splitAccessor?: string; palette?: PaletteOutput; - collapseFn?: string; + collapseFn?: CollapseFunction; xScaleType?: XScaleType; isHistogram?: boolean; columnToLabel?: string; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 8c6da8bf95a6b..12baaab25af84 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -292,6 +292,7 @@ describe('xy_visualization', () => { label: 'date_histogram', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -1685,6 +1686,7 @@ describe('xy_visualization', () => { label: 'date_histogram', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -1715,6 +1717,7 @@ describe('xy_visualization', () => { label: 'date_histogram', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -1759,6 +1762,7 @@ describe('xy_visualization', () => { label: 'histogram', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -1791,6 +1795,7 @@ describe('xy_visualization', () => { label: 'top values', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -1821,6 +1826,7 @@ describe('xy_visualization', () => { label: 'top values', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; @@ -1930,6 +1936,7 @@ describe('xy_visualization', () => { label: 'date_histogram', isStaticValue: false, hasTimeShift: false, + hasReducedTimeRange: false, }; } return null; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 0b482b92be9ee..6622dd40615c8 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -156,6 +156,10 @@ export const getXyVisualization = ({ }, appendLayer(state, layerId, layerType, indexPatternId) { + if (layerType === 'metricTrendline') { + return state; + } + const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType; return { ...state, diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx index 5354c392d357a..e7580fe3b9d6a 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import { IconChartBarHorizontal, IconChartBarStacked, IconChartMixedXy } from '@kbn/chart-icons'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import type { LayerType as XYLayerType } from '@kbn/expression-xy-plugin/common'; import { DatasourceLayers, OperationMetadata, VisualizationType } from '../../types'; import { State, @@ -21,7 +21,7 @@ import { SeriesType, } from './types'; import { isHorizontalChart } from './state_helpers'; -import type { LayerType } from '../../../common'; +import { layerTypes } from '../..'; export function getAxisName( axis: 'x' | 'y' | 'yLeft' | 'yRight', @@ -121,7 +121,7 @@ export function checkScaleOperation( } export const isDataLayer = (layer: XYLayerConfig): layer is XYDataLayerConfig => - layer.layerType === LayerTypes.DATA || !layer.layerType; + layer.layerType === layerTypes.DATA || !layer.layerType; export const getDataLayers = (layers: XYLayerConfig[]) => (layers || []).filter((layer): layer is XYDataLayerConfig => isDataLayer(layer)); @@ -131,31 +131,31 @@ export const getFirstDataLayer = (layers: XYLayerConfig[]) => export const isReferenceLayer = ( layer: Pick -): layer is XYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE; +): layer is XYReferenceLineLayerConfig => layer.layerType === layerTypes.REFERENCELINE; export const getReferenceLayers = (layers: Array>) => (layers || []).filter((layer): layer is XYReferenceLineLayerConfig => isReferenceLayer(layer)); export const isAnnotationsLayer = ( layer: Pick -): layer is XYAnnotationLayerConfig => layer.layerType === LayerTypes.ANNOTATIONS; +): layer is XYAnnotationLayerConfig => layer.layerType === layerTypes.ANNOTATIONS; export const getAnnotationsLayers = (layers: Array>) => (layers || []).filter((layer): layer is XYAnnotationLayerConfig => isAnnotationsLayer(layer)); export interface LayerTypeToLayer { - [LayerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig; - [LayerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig; - [LayerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => XYAnnotationLayerConfig; + [layerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig; + [layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig; + [layerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => XYAnnotationLayerConfig; } export const getLayerTypeOptions = (layer: XYLayerConfig, options: LayerTypeToLayer) => { if (isDataLayer(layer)) { - return options[LayerTypes.DATA](layer); + return options[layerTypes.DATA](layer); } else if (isReferenceLayer(layer)) { - return options[LayerTypes.REFERENCELINE](layer); + return options[layerTypes.REFERENCELINE](layer); } - return options[LayerTypes.ANNOTATIONS](layer); + return options[layerTypes.ANNOTATIONS](layer); }; export function getVisualizationType(state: State): VisualizationType | 'mixed' { @@ -211,7 +211,7 @@ export const defaultIcon = IconChartBarStacked; export const defaultSeriesType = 'bar_stacked'; export const supportedDataLayer = { - type: LayerTypes.DATA, + type: layerTypes.DATA, label: i18n.translate('xpack.lens.xyChart.addDataLayerLabel', { defaultMessage: 'Visualization', }), @@ -253,7 +253,7 @@ export function getMessageIdsForDimension( } const newLayerFn = { - [LayerTypes.DATA]: ({ + [layerTypes.DATA]: ({ layerId, seriesType, }: { @@ -261,16 +261,16 @@ const newLayerFn = { seriesType: SeriesType; }): XYDataLayerConfig => ({ layerId, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, accessors: [], seriesType, }), - [LayerTypes.REFERENCELINE]: ({ layerId }: { layerId: string }): XYReferenceLineLayerConfig => ({ + [layerTypes.REFERENCELINE]: ({ layerId }: { layerId: string }): XYReferenceLineLayerConfig => ({ layerId, - layerType: LayerTypes.REFERENCELINE, + layerType: layerTypes.REFERENCELINE, accessors: [], }), - [LayerTypes.ANNOTATIONS]: ({ + [layerTypes.ANNOTATIONS]: ({ layerId, indexPatternId, }: { @@ -278,7 +278,7 @@ const newLayerFn = { indexPatternId: string; }): XYAnnotationLayerConfig => ({ layerId, - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, annotations: [], indexPatternId, ignoreGlobalFilters: true, @@ -287,12 +287,12 @@ const newLayerFn = { export function newLayerState({ layerId, - layerType = LayerTypes.DATA, + layerType = layerTypes.DATA, seriesType, indexPatternId, }: { layerId: string; - layerType?: LayerType; + layerType?: XYLayerType; seriesType: SeriesType; indexPatternId: string; }) { @@ -300,7 +300,7 @@ export function newLayerState({ } export function getLayersByType(state: State, byType?: string) { - return state.layers.filter(({ layerType = LayerTypes.DATA }) => + return state.layers.filter(({ layerType = layerTypes.DATA }) => byType ? layerType === byType : true ); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx index 185a73c64d4e6..5601bac2ccbd0 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx @@ -11,6 +11,7 @@ import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { AnnotationsPanel } from '.'; import { FramePublicAPI } from '../../../../types'; +import { DatasourcePublicAPI } from '../../../..'; import { createMockFramePublicAPI } from '../../../../mocks'; import { State } from '../../types'; import { Position } from '@elastic/charts'; @@ -88,6 +89,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -152,6 +156,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -191,6 +198,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -293,6 +303,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -350,6 +363,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -403,6 +419,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -476,6 +495,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -547,6 +569,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -615,6 +640,9 @@ describe('AnnotationsPanel', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx index 088ba4537393e..6caa8238808e9 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx @@ -273,6 +273,9 @@ describe('XY Config panels', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -298,6 +301,9 @@ describe('XY Config panels', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -344,6 +350,9 @@ describe('XY Config panels', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -387,6 +396,9 @@ describe('XY Config panels', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); @@ -430,6 +442,9 @@ describe('XY Config panels', () => { formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} + addLayer={jest.fn()} + removeLayer={jest.fn()} + datasource={{} as DatasourcePublicAPI} /> ); diff --git a/x-pack/test/functional/apps/lens/group3/metric.ts b/x-pack/test/functional/apps/lens/group3/metric.ts index 6b311c65f6fef..843422e87c51f 100644 --- a/x-pack/test/functional/apps/lens/group3/metric.ts +++ b/x-pack/test/functional/apps/lens/group3/metric.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const retry = getService('retry'); + const inspector = getService('inspector'); const clickMetric = async (title: string) => { const tiles = await PageObjects.lens.getMetricTiles(); @@ -62,6 +63,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { extraText: 'Average of bytes 19.76K', value: '19.76K', color: 'rgba(245, 247, 250, 1)', + showingTrendline: false, showingBar: false, }, { @@ -70,6 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { extraText: 'Average of bytes 18.99K', value: '18.99K', color: 'rgba(245, 247, 250, 1)', + showingTrendline: false, showingBar: false, }, { @@ -78,6 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { extraText: 'Average of bytes 17.25K', value: '17.25K', color: 'rgba(245, 247, 250, 1)', + showingTrendline: false, showingBar: false, }, { @@ -86,6 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { extraText: 'Average of bytes 15.69K', value: '15.69K', color: 'rgba(245, 247, 250, 1)', + showingTrendline: false, showingBar: false, }, { @@ -94,6 +99,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { extraText: 'Average of bytes 15.61K', value: '15.61K', color: 'rgba(245, 247, 250, 1)', + showingTrendline: false, showingBar: false, }, { @@ -102,6 +108,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { extraText: 'Average of bytes 5.72K', value: '5.72K', color: 'rgba(245, 247, 250, 1)', + showingTrendline: false, showingBar: false, }, ]); @@ -118,6 +125,44 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.removeDimension('lnsMetric_maxDimensionPanel'); }); + it('should enable trendlines', async () => { + await PageObjects.lens.openDimensionEditor( + 'lnsMetric_primaryMetricDimensionPanel > lns-dimensionTrigger' + ); + + await testSubjects.click('lnsMetric_supporting_visualization_trendline'); + + await PageObjects.lens.waitForVisualization('mtrVis'); + + expect( + (await PageObjects.lens.getMetricVisualizationData()).some( + (datum) => datum.showingTrendline + ) + ).to.be(true); + + await inspector.open('lnsApp_inspectButton'); + + expect(await inspector.getNumberOfTables()).to.equal(2); + + await inspector.close(); + + await PageObjects.lens.openDimensionEditor( + 'lnsMetric_primaryMetricDimensionPanel > lns-dimensionTrigger' + ); + + await testSubjects.click('lnsMetric_supporting_visualization_none'); + + await PageObjects.lens.waitForVisualization('mtrVis'); + + expect( + (await PageObjects.lens.getMetricVisualizationData()).some( + (datum) => datum.showingTrendline + ) + ).to.be(false); + + await PageObjects.lens.closeDimensionEditor(); + }); + it('should filter by click', async () => { expect((await filterBar.getFiltersLabel()).length).to.be(0); const title = '93.28.27.24'; diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/goal.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/goal.ts index 97083c498a453..38d943e48a826 100644 --- a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/goal.ts +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/goal.ts @@ -41,6 +41,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '140.05%', color: 'rgba(245, 247, 250, 1)', showingBar: true, + showingTrendline: false, }, ]); }); diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/metric.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/metric.ts index 759b91a6e3843..eef46d2c0cdb7 100644 --- a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/metric.ts +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/metric.ts @@ -50,6 +50,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '14.01K', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, ]); }); @@ -78,6 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '13.1B', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, ]); }); @@ -106,6 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '1.44K', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, ]); }); @@ -159,6 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '13.23B', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, { title: 'win 7', @@ -167,6 +171,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '13.19B', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, { title: 'win xp', @@ -175,6 +180,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '13.07B', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, { title: 'win 8', @@ -183,6 +189,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '13.03B', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, { title: 'ios', @@ -191,6 +198,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: '13.01B', color: 'rgba(245, 247, 250, 1)', showingBar: false, + showingTrendline: false, }, { title: undefined, @@ -199,6 +207,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { value: undefined, color: 'rgba(0, 0, 0, 0)', showingBar: false, + showingTrendline: false, }, ]); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 883e21f1002a9..c11a9a37de7cf 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -1189,6 +1189,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont color: await ( await this.getMetricElementIfExists('.echMetric', tile) )?.getComputedStyle('background-color'), + showingTrendline: Boolean( + await this.getMetricElementIfExists('.echSingleMetricSparkline', tile) + ), }; }, From e71a522567b0c23e894f75bfc8332630c06234b9 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:54:37 -0400 Subject: [PATCH 55/74] [ResponseOps][Stack Connectors] Opsgenie connector UI (#142411) * Starting opsgenie backend * Adding more integration tests * Updating readme * Starting ui * Adding hash and alias * Fixing tests * Switch to platinum for now * Adding server side translations * Fixing merge issues * Fixing file location error * Working ui * Default alias is working * Almost working validation fails sometimes * Adding end to end tests * Adding more tests * Adding note and description fields * Removing todo * Fixing test errors * Addressing feedback * Trying to fix test flakiness --- .../plugins/stack_connectors/common/index.ts | 2 + .../stack_connectors/common/opsgenie.ts | 13 + .../public/connector_types/index.ts | 2 + .../public/connector_types/stack/index.ts | 1 + .../stack/opsgenie/connector.test.tsx | 115 ++++++++ .../stack/opsgenie/connector.tsx | 37 +++ .../stack/opsgenie/constants.ts | 10 + .../connector_types/stack/opsgenie/index.ts | 8 + .../connector_types/stack/opsgenie/logo.tsx | 15 ++ .../stack/opsgenie/model.test.tsx | 91 +++++++ .../connector_types/stack/opsgenie/model.tsx | 88 ++++++ .../stack/opsgenie/params.test.tsx | 247 +++++++++++++++++ .../connector_types/stack/opsgenie/params.tsx | 219 +++++++++++++++ .../stack/opsgenie/translations.ts | 85 ++++++ .../connector_types/stack/opsgenie/types.ts | 17 ++ .../server/connector_types/index.ts | 1 - .../server/connector_types/stack/index.ts | 11 +- .../stack/opsgenie/connector.test.ts | 2 +- .../stack/opsgenie/connector.ts | 5 +- .../connector_types/stack/opsgenie/index.ts | 13 +- .../connector_types/stack/opsgenie/types.ts | 13 + .../triggers_actions_ui/public/types.ts | 6 +- .../functional/services/actions/common.ts | 29 ++ .../test/functional/services/actions/index.ts | 19 ++ .../functional/services/actions/opsgenie.ts | 48 ++++ x-pack/test/functional/services/index.ts | 4 + .../test/functional/services/rules/common.ts | 83 ++++++ .../test/functional/services/rules/index.ts | 15 ++ .../alert_create_flyout.ts | 47 +--- .../{connectors.ts => connectors/general.ts} | 81 +++--- .../triggers_actions_ui/connectors/index.ts | 15 ++ .../connectors/opsgenie.ts | 254 ++++++++++++++++++ .../triggers_actions_ui/connectors/utils.ts | 88 ++++++ x-pack/test/functional_with_es_ssl/config.ts | 1 + .../page_objects/triggers_actions_ui_page.ts | 6 +- 35 files changed, 1587 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/stack_connectors/common/opsgenie.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.test.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/constants.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/index.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/logo.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.test.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.test.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.tsx create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/translations.ts create mode 100644 x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/types.ts create mode 100644 x-pack/test/functional/services/actions/common.ts create mode 100644 x-pack/test/functional/services/actions/index.ts create mode 100644 x-pack/test/functional/services/actions/opsgenie.ts create mode 100644 x-pack/test/functional/services/rules/common.ts create mode 100644 x-pack/test/functional/services/rules/index.ts rename x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/{connectors.ts => connectors/general.ts} (86%) create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/index.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/utils.ts diff --git a/x-pack/plugins/stack_connectors/common/index.ts b/x-pack/plugins/stack_connectors/common/index.ts index bdaa075cbfa01..5f78f4fad2810 100644 --- a/x-pack/plugins/stack_connectors/common/index.ts +++ b/x-pack/plugins/stack_connectors/common/index.ts @@ -13,3 +13,5 @@ export enum AdditionalEmailServices { } export const INTERNAL_BASE_STACK_CONNECTORS_API_PATH = '/internal/stack_connectors'; + +export { OpsgenieSubActions, OpsgenieConnectorTypeId } from './opsgenie'; diff --git a/x-pack/plugins/stack_connectors/common/opsgenie.ts b/x-pack/plugins/stack_connectors/common/opsgenie.ts new file mode 100644 index 0000000000000..970330350b409 --- /dev/null +++ b/x-pack/plugins/stack_connectors/common/opsgenie.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum OpsgenieSubActions { + CreateAlert = 'createAlert', + CloseAlert = 'closeAlert', +} + +export const OpsgenieConnectorTypeId = '.opsgenie'; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/index.ts index a2684c0b20734..517bd32f70955 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/index.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/index.ts @@ -16,6 +16,7 @@ import { getSlackConnectorType, getTeamsConnectorType, getWebhookConnectorType, + getOpsgenieConnectorType, getXmattersConnectorType, } from './stack'; @@ -56,5 +57,6 @@ export function registerConnectorTypes({ connectorTypeRegistry.register(getServiceNowSIRConnectorType()); connectorTypeRegistry.register(getJiraConnectorType()); connectorTypeRegistry.register(getResilientConnectorType()); + connectorTypeRegistry.register(getOpsgenieConnectorType()); connectorTypeRegistry.register(getTeamsConnectorType()); } diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts index fec0283a799ab..3bb2aafafd49b 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/index.ts @@ -13,4 +13,5 @@ export { getServiceNowITOMConnectorType } from './servicenow_itom'; export { getSlackConnectorType } from './slack'; export { getTeamsConnectorType } from './teams'; export { getWebhookConnectorType } from './webhook'; +export { getOpsgenieConnectorType } from './opsgenie'; export { getXmattersConnectorType } from './xmatters'; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.test.tsx new file mode 100644 index 0000000000000..635d183eca80a --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.test.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import OpsgenieConnectorFields from './connector'; +import { ConnectorFormTestProvider } from '../../lib/test_utils'; +import { act, screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana'); + +const actionConnector = { + actionTypeId: '.opsgenie', + name: 'opsgenie', + config: { + apiUrl: 'https://test.com', + }, + secrets: { + apiKey: 'secret', + }, + isDeprecated: false, +}; + +describe('OpsgenieConnectorFields renders', () => { + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the fields', async () => { + render( + + {}} + /> + + ); + + expect(screen.getByTestId('config.apiUrl-input')).toBeInTheDocument(); + expect(screen.getByTestId('secrets.apiKey-input')).toBeInTheDocument(); + }); + + describe('Validation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const tests: Array<[string, string]> = [ + ['config.apiUrl-input', 'not-valid'], + ['secrets.apiKey-input', ''], + ]; + + it('connector validation succeeds when connector config is valid', async () => { + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.opsgenie', + name: 'opsgenie', + config: { + apiUrl: 'https://test.com', + }, + secrets: { + apiKey: 'secret', + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it.each(tests)('validates correctly %p', async (field, value) => { + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.tsx new file mode 100644 index 0000000000000..08380dfb216e3 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/connector.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 React from 'react'; +import type { + ActionConnectorFieldsProps, + ConfigFieldSchema, + SecretsFieldSchema, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { SimpleConnectorForm } from '@kbn/triggers-actions-ui-plugin/public'; +import * as i18n from './translations'; + +const configFormSchema: ConfigFieldSchema[] = [ + { id: 'apiUrl', label: i18n.API_URL_LABEL, isUrlField: true }, +]; + +const secretsFormSchema: SecretsFieldSchema[] = [ + { id: 'apiKey', label: i18n.API_KEY_LABEL, isPasswordField: true }, +]; + +const OpsgenieConnectorFields: React.FC = ({ readOnly, isEdit }) => { + return ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { OpsgenieConnectorFields as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/constants.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/constants.ts new file mode 100644 index 0000000000000..9d5ed9bc7bf48 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 { AlertProvidedActionVariables } from '@kbn/triggers-actions-ui-plugin/public'; + +export const DEFAULT_ALIAS = `{{${AlertProvidedActionVariables.ruleId}}}:{{${AlertProvidedActionVariables.alertId}}}`; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/index.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/index.ts new file mode 100644 index 0000000000000..95cdcae8dd485 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/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 { getConnectorType as getOpsgenieConnectorType } from './model'; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/logo.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/logo.tsx new file mode 100644 index 0000000000000..7e705a788c0c7 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/logo.tsx @@ -0,0 +1,15 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { LogoProps } from '../../types'; + +const Logo = (props: LogoProps) => ; + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.test.tsx new file mode 100644 index 0000000000000..f3b2c70a6e09f --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; +import { registerConnectorTypes } from '../..'; +import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { registrationServicesMock } from '../../../mocks'; +import { OpsgenieConnectorTypeId, OpsgenieSubActions } from '../../../../common'; + +let connectorTypeModel: ConnectorTypeModel; + +beforeAll(() => { + const connectorTypeRegistry = new TypeRegistry(); + registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock }); + const getResult = connectorTypeRegistry.get(OpsgenieConnectorTypeId); + if (getResult !== null) { + connectorTypeModel = getResult; + } +}); + +describe('connectorTypeRegistry.get() works', () => { + it('sets the id field in the connector type static data to the correct opsgenie value', () => { + expect(connectorTypeModel.id).toEqual(OpsgenieConnectorTypeId); + }); +}); + +describe('opsgenie action params validation', () => { + it('results in no errors when the action params are valid for creating an alert', async () => { + const actionParams = { + subAction: OpsgenieSubActions.CreateAlert, + subActionParams: { + message: 'hello', + }, + }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.message': [], + 'subActionParams.alias': [], + }, + }); + }); + + it('results in no errors when the action params are valid for closing an alert', async () => { + const actionParams = { + subAction: OpsgenieSubActions.CloseAlert, + subActionParams: { + alias: '123', + }, + }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.message': [], + 'subActionParams.alias': [], + }, + }); + }); + + it('sets the message error when the message is missing for creating an alert', async () => { + const actionParams = { + subAction: OpsgenieSubActions.CreateAlert, + subActionParams: {}, + }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.message': ['Message is required.'], + 'subActionParams.alias': [], + }, + }); + }); + + it('sets the alias error when the alias is missing for closing an alert', async () => { + const actionParams = { + subAction: OpsgenieSubActions.CloseAlert, + subActionParams: {}, + }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.message': [], + 'subActionParams.alias': ['Alias is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.tsx new file mode 100644 index 0000000000000..03290fbeca7ad --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/model.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + ActionTypeModel as ConnectorTypeModel, + GenericValidationResult, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { RecursivePartial } from '@elastic/eui'; +import { OpsgenieSubActions } from '../../../../common'; +import type { + OpsgenieActionConfig, + OpsgenieActionParams, + OpsgenieActionSecrets, +} from '../../../../server/connector_types/stack'; +import { DEFAULT_ALIAS } from './constants'; + +const SELECT_MESSAGE = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.selectMessageText', + { + defaultMessage: 'Create or close an alert in Opsgenie.', + } +); + +const TITLE = i18n.translate('xpack.stackConnectors.components.opsgenie.connectorTypeTitle', { + defaultMessage: 'Opsgenie', +}); + +export const getConnectorType = (): ConnectorTypeModel< + OpsgenieActionConfig, + OpsgenieActionSecrets, + OpsgenieActionParams +> => { + return { + id: '.opsgenie', + iconClass: lazy(() => import('./logo')), + selectMessage: SELECT_MESSAGE, + actionTypeTitle: TITLE, + validateParams: async ( + actionParams: RecursivePartial + ): Promise> => { + const translations = await import('./translations'); + const errors = { + 'subActionParams.message': new Array(), + 'subActionParams.alias': new Array(), + }; + + const validationResult = { + errors, + }; + + if ( + actionParams.subAction === OpsgenieSubActions.CreateAlert && + !actionParams?.subActionParams?.message?.length + ) { + errors['subActionParams.message'].push(translations.MESSAGE_IS_REQUIRED); + } + + if ( + actionParams.subAction === OpsgenieSubActions.CloseAlert && + !actionParams?.subActionParams?.alias?.length + ) { + errors['subActionParams.alias'].push(translations.ALIAS_IS_REQUIRED); + } + + return validationResult; + }, + actionConnectorFields: lazy(() => import('./connector')), + actionParamsFields: lazy(() => import('./params')), + defaultActionParams: { + subAction: OpsgenieSubActions.CreateAlert, + subActionParams: { + alias: DEFAULT_ALIAS, + }, + }, + defaultRecoveredActionParams: { + subAction: OpsgenieSubActions.CloseAlert, + subActionParams: { + alias: DEFAULT_ALIAS, + }, + }, + }; +}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.test.tsx new file mode 100644 index 0000000000000..4a7650f8c6cff --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.test.tsx @@ -0,0 +1,247 @@ +/* + * 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 { act, screen, render, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import OpsgenieParamFields from './params'; +import { OpsgenieSubActions } from '../../../../common'; +import { OpsgenieActionParams } from '../../../../server/connector_types/stack'; + +describe('OpsgenieParamFields', () => { + const editAction = jest.fn(); + const createAlertActionParams: OpsgenieActionParams = { + subAction: OpsgenieSubActions.CreateAlert, + subActionParams: { message: 'hello', alias: '123' }, + }; + + const closeAlertActionParams: OpsgenieActionParams = { + subAction: OpsgenieSubActions.CloseAlert, + subActionParams: { alias: '456' }, + }; + + const connector = { + secrets: { apiKey: '123' }, + config: { apiUrl: 'http://test.com' }, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + isDeprecated: false, + }; + + const defaultCreateAlertProps = { + actionParams: createAlertActionParams, + errors: { + 'subActionParams.message': [], + 'subActionParams.alias': [], + }, + editAction, + index: 0, + messageVariables: [], + actionConnector: connector, + }; + + const defaultCloseAlertProps = { + actionParams: closeAlertActionParams, + errors: { + 'subActionParams.message': [], + 'subActionParams.alias': [], + }, + editAction, + index: 0, + messageVariables: [], + actionConnector: connector, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the create alert component', async () => { + render(); + + expect(screen.getByText('Message')).toBeInTheDocument(); + expect(screen.getByText('Alias')).toBeInTheDocument(); + expect(screen.getByTestId('opsgenie-subActionSelect')); + + expect(screen.getByDisplayValue('hello')).toBeInTheDocument(); + expect(screen.getByDisplayValue('123')).toBeInTheDocument(); + }); + + it('renders the close alert component', async () => { + render(); + + expect(screen.queryByText('Message')).not.toBeInTheDocument(); + expect(screen.getByText('Alias')).toBeInTheDocument(); + expect(screen.getByTestId('opsgenie-subActionSelect')); + + expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument(); + expect(screen.queryByDisplayValue('123')).not.toBeInTheDocument(); + expect(screen.getByDisplayValue('456')).toBeInTheDocument(); + }); + + it('calls editAction when the message field is changed', async () => { + render(); + + fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'a new message' } }); + + expect(editAction).toBeCalledTimes(1); + expect(editAction.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "subActionParams", + Object { + "alias": "123", + "message": "a new message", + }, + 0, + ] + `); + }); + + it('calls editAction when the description field is changed', async () => { + render(); + + fireEvent.change(screen.getByTestId('descriptionTextArea'), { + target: { value: 'a new description' }, + }); + + expect(editAction).toBeCalledTimes(1); + expect(editAction.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "subActionParams", + Object { + "alias": "123", + "description": "a new description", + "message": "hello", + }, + 0, + ] + `); + }); + + it('calls editAction when the alias field is changed for closeAlert', async () => { + render(); + + fireEvent.change(screen.getByDisplayValue('456'), { target: { value: 'a new alias' } }); + + expect(editAction).toBeCalledTimes(1); + expect(editAction.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "subActionParams", + Object { + "alias": "a new alias", + }, + 0, + ] + `); + }); + + it('does not render the create or close alert components if the subAction is undefined', async () => { + render(); + + expect(screen.queryByTestId('opsgenie-alias-row')).not.toBeInTheDocument(); + expect(screen.queryByText('Message')).not.toBeInTheDocument(); + }); + + it('preserves the previous alias value when switching between the create and close alert event actions', async () => { + const { rerender } = render(); + + expect(screen.getByDisplayValue('hello')).toBeInTheDocument(); + expect(screen.getByDisplayValue('123')).toBeInTheDocument(); + + fireEvent.change(screen.getByDisplayValue('123'), { target: { value: 'a new alias' } }); + expect(editAction).toBeCalledTimes(1); + + rerender( + + ); + + expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument(); + + expect(editAction).toBeCalledTimes(2); + + expect(editAction.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "subActionParams", + Object { + "alias": "a new alias", + }, + 0, + ] + `); + }); + + it('only preserves the previous alias value when switching between the create and close alert event actions', async () => { + const { rerender } = render(); + + expect(screen.getByDisplayValue('hello')).toBeInTheDocument(); + expect(screen.getByDisplayValue('123')).toBeInTheDocument(); + + fireEvent.change(screen.getByDisplayValue('123'), { target: { value: 'a new alias' } }); + expect(editAction).toBeCalledTimes(1); + + rerender( + + ); + + expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument(); + + expect(editAction).toBeCalledTimes(2); + + expect(editAction.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "subActionParams", + Object { + "alias": "a new alias", + }, + 0, + ] + `); + }); + + it('calls editAction when changing the subAction', async () => { + render(); + + act(() => + userEvent.selectOptions( + screen.getByTestId('opsgenie-subActionSelect'), + screen.getByText('Close Alert') + ) + ); + + expect(editAction).toBeCalledTimes(1); + expect(editAction.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "subAction", + "closeAlert", + 0, + ] + `); + }); +}); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.tsx new file mode 100644 index 0000000000000..8dd6ab450af75 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/params.tsx @@ -0,0 +1,219 @@ +/* + * 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, { useCallback, useEffect, useRef } from 'react'; +import { + ActionParamsProps, + TextAreaWithMessageVariables, + TextFieldWithMessageVariables, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { EuiFormRow, EuiSelect, RecursivePartial } from '@elastic/eui'; +import { OpsgenieSubActions } from '../../../../common'; +import type { + OpsgenieActionParams, + OpsgenieCloseAlertParams, + OpsgenieCreateAlertParams, +} from '../../../../server/connector_types/stack'; +import * as i18n from './translations'; + +type SubActionProps = Omit< + ActionParamsProps, + 'actionParams' | 'editAction' +> & { + subActionParams?: RecursivePartial; + editSubAction: ActionParamsProps['editAction']; +}; + +const CreateAlertComponent: React.FC> = ({ + editSubAction, + errors, + index, + messageVariables, + subActionParams, +}) => { + const isMessageInvalid = + errors['subActionParams.message'] !== undefined && + errors['subActionParams.message'].length > 0 && + subActionParams?.message !== undefined; + + return ( + <> + + + + + + + + + ); +}; + +CreateAlertComponent.displayName = 'CreateAlertComponent'; + +const CloseAlertComponent: React.FC> = ({ + editSubAction, + errors, + index, + messageVariables, + subActionParams, +}) => { + const isAliasInvalid = + errors['subActionParams.alias'] !== undefined && + errors['subActionParams.alias'].length > 0 && + subActionParams?.alias !== undefined; + + return ( + <> + + + + + + ); +}; + +CloseAlertComponent.displayName = 'CloseAlertComponent'; + +const actionOptions = [ + { + value: OpsgenieSubActions.CreateAlert, + text: i18n.CREATE_ALERT_ACTION, + }, + { + value: OpsgenieSubActions.CloseAlert, + text: i18n.CLOSE_ALERT_ACTION, + }, +]; + +const OpsgenieParamFields: React.FC> = ({ + actionParams, + editAction, + errors, + index, + messageVariables, +}) => { + const { subAction, subActionParams } = actionParams; + + const currentSubAction = useRef(subAction ?? OpsgenieSubActions.CreateAlert); + + const onActionChange = useCallback( + (event: React.ChangeEvent) => { + editAction('subAction', event.target.value, index); + }, + [editAction, index] + ); + + const editSubAction = useCallback( + (key, value) => { + editAction('subActionParams', { ...subActionParams, [key]: value }, index); + }, + [editAction, index, subActionParams] + ); + + useEffect(() => { + if (!subAction) { + editAction('subAction', OpsgenieSubActions.CreateAlert, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [index, subAction]); + + useEffect(() => { + if (subAction != null && currentSubAction.current !== subAction) { + currentSubAction.current = subAction; + const params = subActionParams?.alias ? { alias: subActionParams.alias } : undefined; + editAction('subActionParams', params, index); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [subAction, currentSubAction]); + + return ( + <> + + + + + {subAction != null && subAction === OpsgenieSubActions.CreateAlert && ( + + )} + + {subAction != null && subAction === OpsgenieSubActions.CloseAlert && ( + + )} + + ); +}; + +OpsgenieParamFields.displayName = 'OpsgenieParamFields'; + +// eslint-disable-next-line import/no-default-export +export { OpsgenieParamFields as default }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/translations.ts new file mode 100644 index 0000000000000..a5dd4e14c9c13 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/translations.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const API_URL_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_KEY_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.apiKeySecret', + { + defaultMessage: 'API Key', + } +); + +export const ACTION_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.actionLabel', + { + defaultMessage: 'Action', + } +); + +export const CREATE_ALERT_ACTION = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.createAlertAction', + { + defaultMessage: 'Create Alert', + } +); + +export const CLOSE_ALERT_ACTION = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.closeAlertAction', + { + defaultMessage: 'Close Alert', + } +); + +export const MESSAGE_FIELD_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.messageLabel', + { + defaultMessage: 'Message', + } +); + +export const NOTE_FIELD_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.noteLabel', + { + defaultMessage: 'Note (optional)', + } +); + +export const DESCRIPTION_FIELD_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.descriptionLabel', + { + defaultMessage: 'Description (optional)', + } +); + +export const MESSAGE_IS_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.requiredMessageTextField', + { + defaultMessage: 'Message is required.', + } +); + +export const ALIAS_FIELD_LABEL = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.aliasLabel', + { + defaultMessage: 'Alias', + } +); + +export const ALIAS_IS_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.opsgenie.requiredAliasTextField', + { + defaultMessage: 'Alias is required.', + } +); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/types.ts b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/types.ts new file mode 100644 index 0000000000000..e1637e99f2149 --- /dev/null +++ b/x-pack/plugins/stack_connectors/public/connector_types/stack/opsgenie/types.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 { UserConfiguredActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; +import type { + OpsgenieActionConfig, + OpsgenieActionSecrets, +} from '../../../../server/connector_types/stack'; + +export type OpsgenieActionConnector = UserConfiguredActionConnector< + OpsgenieActionConfig, + OpsgenieActionSecrets +>; 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 dd12f93bbc607..2440bbc9a28e0 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/index.ts @@ -45,7 +45,6 @@ export { SlackConnectorTypeId, TeamsConnectorTypeId, WebhookConnectorTypeId, - OpsgenieConnectorTypeId, XmattersConnectorTypeId, } from './stack'; export type { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts index 1d7df7b21ed94..4d9b5194a10f3 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/index.ts @@ -49,7 +49,16 @@ export { } from './webhook'; export type { ActionParamsType as WebhookActionParams } from './webhook'; -export { getOpsgenieConnectorType, OpsgenieConnectorTypeId } from './opsgenie'; +export { getOpsgenieConnectorType } from './opsgenie'; +export type { + OpsgenieActionConfig, + OpsgenieActionSecrets, + OpsgenieActionParams, + OpsgenieCloseAlertSubActionParams, + OpsgenieCreateAlertSubActionParams, + OpsgenieCloseAlertParams, + OpsgenieCreateAlertParams, +} from './opsgenie'; export { getConnectorType as getXmattersConnectorType, diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.test.ts index af9380b8ea75b..89dd11a2effcb 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.test.ts @@ -12,7 +12,7 @@ import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.moc import { actionsMock } from '@kbn/actions-plugin/server/mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { MockedLogger } from '@kbn/logging-mocks'; -import { OpsgenieConnectorTypeId } from '.'; +import { OpsgenieConnectorTypeId } from '../../../../common'; import { OpsgenieConnector } from './connector'; import * as utils from '@kbn/actions-plugin/server/lib/axios_utils'; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.ts index 4c053c954a192..cb454d87bf7bd 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/connector.ts @@ -8,6 +8,7 @@ import crypto from 'crypto'; import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server'; import { AxiosError } from 'axios'; +import { OpsgenieSubActions } from '../../../../common'; import { CloseAlertParamsSchema, CreateAlertParamsSchema, Response } from './schema'; import { CloseAlertParams, Config, CreateAlertParams, Secrets } from './types'; import * as i18n from './translations'; @@ -25,13 +26,13 @@ export class OpsgenieConnector extends SubActionConnector { this.registerSubAction({ method: this.createAlert.name, - name: 'createAlert', + name: OpsgenieSubActions.CreateAlert, schema: CreateAlertParamsSchema, }); this.registerSubAction({ method: this.closeAlert.name, - name: 'closeAlert', + name: OpsgenieSubActions.CloseAlert, schema: CloseAlertParamsSchema, }); } diff --git a/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/index.ts index b5c453ae22e0b..4eacd59791439 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/index.ts @@ -15,13 +15,12 @@ import { SubActionConnectorType, ValidatorType, } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { OpsgenieConnectorTypeId } from '../../../../common'; import { OpsgenieConnector } from './connector'; import { ConfigSchema, SecretsSchema } from './schema'; import { Config, Secrets } from './types'; import * as i18n from './translations'; -export const OpsgenieConnectorTypeId = '.opsgenie'; - export const getOpsgenieConnectorType = (): SubActionConnectorType => { return { Service: OpsgenieConnector, @@ -37,3 +36,13 @@ export const getOpsgenieConnectorType = (): SubActionConnectorType; export type Secrets = TypeOf; export type CreateAlertParams = TypeOf; export type CloseAlertParams = TypeOf; + +export interface CreateAlertSubActionParams { + subAction: OpsgenieSubActions.CreateAlert; + subActionParams: CreateAlertParams; +} + +export interface CloseAlertSubActionParams { + subAction: OpsgenieSubActions.CloseAlert; + subActionParams: CloseAlertParams; +} + +export type Params = CreateAlertSubActionParams | CloseAlertSubActionParams; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 65b6d50e22285..c001c7b90fa39 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -13,7 +13,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { IconType, EuiFlyoutSize } from '@elastic/eui'; +import type { IconType, EuiFlyoutSize, RecursivePartial } from '@elastic/eui'; import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui'; import { ActionType, @@ -204,8 +204,8 @@ export interface ActionTypeModel > | null; actionParamsFields: React.LazyExoticComponent>>; - defaultActionParams?: Partial; - defaultRecoveredActionParams?: Partial; + defaultActionParams?: RecursivePartial; + defaultRecoveredActionParams?: RecursivePartial; customConnectorSelectItem?: CustomConnectorSelectionItem; isExperimental?: boolean; } diff --git a/x-pack/test/functional/services/actions/common.ts b/x-pack/test/functional/services/actions/common.ts new file mode 100644 index 0000000000000..55ba48001bfe3 --- /dev/null +++ b/x-pack/test/functional/services/actions/common.ts @@ -0,0 +1,29 @@ +/* + * 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 { ProvidedType } from '@kbn/test'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export type ActionsCommon = ProvidedType; + +export function ActionsCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async openNewConnectorForm(name: string) { + const createBtn = await testSubjects.find('createActionButton'); + const createBtnIsVisible = await createBtn.isDisplayed(); + if (createBtnIsVisible) { + await createBtn.click(); + } else { + await testSubjects.click('createFirstActionButton'); + } + + await testSubjects.click(`.${name}-card`); + }, + }; +} diff --git a/x-pack/test/functional/services/actions/index.ts b/x-pack/test/functional/services/actions/index.ts new file mode 100644 index 0000000000000..86388ed8e76f0 --- /dev/null +++ b/x-pack/test/functional/services/actions/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ActionsCommonServiceProvider } from './common'; +import { ActionsOpsgenieServiceProvider } from './opsgenie'; + +export function ActionsServiceProvider(context: FtrProviderContext) { + const common = ActionsCommonServiceProvider(context); + + return { + opsgenie: ActionsOpsgenieServiceProvider(context, common), + common: ActionsCommonServiceProvider(context), + }; +} diff --git a/x-pack/test/functional/services/actions/opsgenie.ts b/x-pack/test/functional/services/actions/opsgenie.ts new file mode 100644 index 0000000000000..32f4d82068354 --- /dev/null +++ b/x-pack/test/functional/services/actions/opsgenie.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import type { ActionsCommon } from './common'; + +export interface ConnectorFormFields { + name: string; + apiUrl: string; + apiKey: string; +} + +export function ActionsOpsgenieServiceProvider( + { getService, getPageObject }: FtrProviderContext, + common: ActionsCommon +) { + const testSubjects = getService('testSubjects'); + + return { + async createNewConnector(fields: ConnectorFormFields) { + await common.openNewConnectorForm('opsgenie'); + await this.setConnectorFields(fields); + + const flyOutSaveButton = await testSubjects.find('create-connector-flyout-save-btn'); + expect(await flyOutSaveButton.isEnabled()).to.be(true); + await flyOutSaveButton.click(); + }, + + async setConnectorFields({ name, apiUrl, apiKey }: ConnectorFormFields) { + await testSubjects.setValue('nameInput', name); + await testSubjects.setValue('config.apiUrl-input', apiUrl); + await testSubjects.setValue('secrets.apiKey-input', apiKey); + }, + + async updateConnectorFields(fields: ConnectorFormFields) { + await this.setConnectorFields(fields); + + const editFlyOutSaveButton = await testSubjects.find('edit-connector-flyout-save-btn'); + expect(await editFlyOutSaveButton.isEnabled()).to.be(true); + await editFlyOutSaveButton.click(); + }, + }; +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 444d7f3e39d75..ce18ca4681310 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -70,6 +70,8 @@ import { SearchSessionsService } from './search_sessions'; import { ObservabilityProvider } from './observability'; // import { CompareImagesProvider } from './compare_images'; import { CasesServiceProvider } from './cases'; +import { ActionsServiceProvider } from './actions'; +import { RulesServiceProvider } from './rules'; import { AiopsProvider } from './aiops'; // define the name and providers for services that should be @@ -130,6 +132,8 @@ export const services = { searchSessions: SearchSessionsService, observability: ObservabilityProvider, // compareImages: CompareImagesProvider, + actions: ActionsServiceProvider, + rules: RulesServiceProvider, cases: CasesServiceProvider, aiops: AiopsProvider, }; diff --git a/x-pack/test/functional/services/rules/common.ts b/x-pack/test/functional/services/rules/common.ts new file mode 100644 index 0000000000000..c6614c91c0924 --- /dev/null +++ b/x-pack/test/functional/services/rules/common.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export type RulesCommon = ProvidedType; + +export function RulesCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + const find = getService('find'); + const retry = getService('retry'); + const browser = getService('browser'); + + return { + async clickCreateAlertButton() { + const createBtn = await find.byCssSelector( + '[data-test-subj="createRuleButton"],[data-test-subj="createFirstRuleButton"]' + ); + await createBtn.click(); + }, + + async cancelRuleCreation() { + await testSubjects.click('cancelSaveRuleButton'); + await testSubjects.existOrFail('confirmRuleCloseModal'); + await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmRuleCloseModal'); + }, + + async setNotifyThrottleInput(value: string = '10') { + await testSubjects.click('notifyWhenSelect'); + await testSubjects.click('onThrottleInterval'); + await testSubjects.setValue('throttleInput', value); + }, + + async defineIndexThresholdAlert(alertName: string) { + await browser.refresh(); + await this.clickCreateAlertButton(); + await testSubjects.scrollIntoView('ruleNameInput'); + await testSubjects.setValue('ruleNameInput', alertName); + await testSubjects.click(`.index-threshold-SelectOption`); + await testSubjects.scrollIntoView('selectIndexExpression'); + await testSubjects.click('selectIndexExpression'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('ruleNameInput'); + await nameInput.click(); + + await testSubjects.click('whenExpression'); + await testSubjects.click('whenExpressionSelect'); + await retry.try(async () => { + const aggTypeOptions = await find.allByCssSelector('#aggTypeField option'); + expect(aggTypeOptions[1]).not.to.be(undefined); + await aggTypeOptions[1].click(); + }); + + await testSubjects.click('ofExpressionPopover'); + const ofComboBox = await find.byCssSelector('#ofField'); + await ofComboBox.click(); + const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox'); + const ofOptions = ofOptionsString.trim().split('\n'); + expect(ofOptions.length > 0).to.be(true); + await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]); + }, + }; +} diff --git a/x-pack/test/functional/services/rules/index.ts b/x-pack/test/functional/services/rules/index.ts new file mode 100644 index 0000000000000..ced7b96ed40a9 --- /dev/null +++ b/x-pack/test/functional/services/rules/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { RulesCommonServiceProvider } from './common'; + +export function RulesServiceProvider(context: FtrProviderContext) { + return { + common: RulesCommonServiceProvider(context), + }; +} 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 a83d8de0a2cb8..042605c0ae585 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 @@ -17,8 +17,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const find = getService('find'); const retry = getService('retry'); - const comboBox = getService('comboBox'); const browser = getService('browser'); + const rules = getService('rules'); async function getAlertsByName(name: string) { const { @@ -62,44 +62,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); } - async function defineIndexThresholdAlert(alertName: string) { - await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('ruleNameInput', alertName); - await testSubjects.click(`.index-threshold-SelectOption`); - await testSubjects.click('selectIndexExpression'); - const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); - await indexComboBox.click(); - await indexComboBox.type('k'); - const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); - await filterSelectItem.click(); - await testSubjects.click('thresholdAlertTimeFieldSelect'); - await retry.try(async () => { - const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); - expect(fieldOptions[1]).not.to.be(undefined); - await fieldOptions[1].click(); - }); - await testSubjects.click('closePopover'); - // need this two out of popup clicks to close them - const nameInput = await testSubjects.find('ruleNameInput'); - await nameInput.click(); - - await testSubjects.click('whenExpression'); - await testSubjects.click('whenExpressionSelect'); - await retry.try(async () => { - const aggTypeOptions = await find.allByCssSelector('#aggTypeField option'); - expect(aggTypeOptions[1]).not.to.be(undefined); - await aggTypeOptions[1].click(); - }); - - await testSubjects.click('ofExpressionPopover'); - const ofComboBox = await find.byCssSelector('#ofField'); - await ofComboBox.click(); - const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox'); - const ofOptions = ofOptionsString.trim().split('\n'); - expect(ofOptions.length > 0).to.be(true); - await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]); - } - async function defineAlwaysFiringAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('ruleNameInput', alertName); @@ -107,10 +69,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } async function discardNewRuleCreation() { - await testSubjects.click('cancelSaveRuleButton'); - await testSubjects.existOrFail('confirmRuleCloseModal'); - await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton'); - await testSubjects.missingOrFail('confirmRuleCloseModal'); + await rules.common.cancelRuleCreation(); } describe('create alert', function () { @@ -128,7 +87,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); - await defineIndexThresholdAlert(alertName); + await rules.common.defineIndexThresholdAlert(alertName); await testSubjects.click('notifyWhenSelect'); await testSubjects.click('onThrottleInterval'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts similarity index 86% rename from x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts rename to x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts index c0e9a145fe47e..c0ee2bed54a02 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts @@ -6,29 +6,31 @@ */ import expect from '@kbn/expect'; -import { findIndex } from 'lodash'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { ObjectRemover } from '../../lib/object_remover'; -import { generateUniqueKey, getTestActionData } from '../../lib/get_test_data'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ObjectRemover } from '../../../lib/object_remover'; +import { generateUniqueKey } from '../../../lib/get_test_data'; +import { + getConnectorByName, + createSlackConnectorAndObjectRemover, + createSlackConnector, +} from './utils'; + +export default ({ getPageObjects, getPageObject, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const find = getService('find'); const retry = getService('retry'); const supertest = getService('supertest'); - const objectRemover = new ObjectRemover(supertest); + let objectRemover: ObjectRemover; const browser = getService('browser'); - describe('Connectors', function () { + describe('General connector functionality', function () { before(async () => { - const { body: createdAction } = await supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send(getTestActionData()) - .expect(200); + objectRemover = await createSlackConnectorAndObjectRemover({ getService }); + }); + + beforeEach(async () => { await pageObjects.common.navigateToApp('triggersActionsConnectors'); - objectRemover.add(createdAction.id, 'action', 'actions'); }); after(async () => { @@ -66,14 +68,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { actionType: 'Slack', }, ]); - const connector = await getConnector(connectorName); + const connector = await getConnectorByName(connectorName, supertest); objectRemover.add(connector.id, 'action', 'actions'); }); it('should edit a connector', async () => { const connectorName = generateUniqueKey(); const updatedConnectorName = `${connectorName}updated`; - const createdAction = await createConnector(connectorName); + const createdAction = await createSlackConnector({ + name: connectorName, + supertest, + }); objectRemover.add(createdAction.id, 'action', 'actions'); await browser.refresh(); @@ -169,7 +174,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should reset connector when canceling an edit', async () => { const connectorName = generateUniqueKey(); - const createdAction = await createConnector(connectorName); + const createdAction = await createSlackConnector({ + name: connectorName, + supertest, + }); objectRemover.add(createdAction.id, 'action', 'actions'); await browser.refresh(); @@ -197,8 +205,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should delete a connector', async () => { const connectorName = generateUniqueKey(); - await createConnector(connectorName); - const createdAction = await createConnector(generateUniqueKey()); + await createSlackConnector({ name: connectorName, supertest }); + const createdAction = await createSlackConnector({ + name: generateUniqueKey(), + supertest, + }); objectRemover.add(createdAction.id, 'action', 'actions'); await browser.refresh(); @@ -223,8 +234,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should bulk delete connectors', async () => { const connectorName = generateUniqueKey(); - await createConnector(connectorName); - const createdAction = await createConnector(generateUniqueKey()); + await createSlackConnector({ name: connectorName, supertest }); + const createdAction = await createSlackConnector({ + name: generateUniqueKey(), + supertest, + }); objectRemover.add(createdAction.id, 'action', 'actions'); await browser.refresh(); @@ -279,22 +293,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - async function createConnector(connectorName: string) { - const { body: createdAction } = await supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: connectorName, - config: {}, - secrets: { - webhookUrl: 'https://test.com', - }, - connector_type_id: '.slack', - }) - .expect(200); - return createdAction; - } - async function createIndexConnector(connectorName: string, indexName: string) { const { body: createdAction } = await supertest .post(`/api/actions/connector`) @@ -311,13 +309,4 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .expect(200); return createdAction; } - - async function getConnector(name: string) { - const { body } = await supertest - .get(`/api/actions/connectors`) - .set('kbn-xsrf', 'foo') - .expect(200); - const i = findIndex(body, (c: any) => c.name === name); - return body[i]; - } }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/index.ts new file mode 100644 index 0000000000000..bda1247767b74 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Connectors', function () { + loadTestFile(require.resolve('./general')); + loadTestFile(require.resolve('./opsgenie')); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts new file mode 100644 index 0000000000000..ff428bdb44e2e --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ObjectRemover } from '../../../lib/object_remover'; +import { generateUniqueKey } from '../../../lib/get_test_data'; +import { createConnector, createSlackConnectorAndObjectRemover, getConnectorByName } from './utils'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const find = getService('find'); + const retry = getService('retry'); + const supertest = getService('supertest'); + const actions = getService('actions'); + const rules = getService('rules'); + const browser = getService('browser'); + let objectRemover: ObjectRemover; + + describe('Opsgenie', () => { + before(async () => { + objectRemover = await createSlackConnectorAndObjectRemover({ getService }); + }); + + after(async () => { + await objectRemover.removeAll(); + }); + + describe('connector page', () => { + beforeEach(async () => { + await pageObjects.common.navigateToApp('triggersActionsConnectors'); + }); + + it('should create the connector', async () => { + const connectorName = generateUniqueKey(); + + await actions.opsgenie.createNewConnector({ + name: connectorName, + apiUrl: 'https://test.com', + apiKey: 'apiKey', + }); + + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Created '${connectorName}'`); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResults = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResults).to.eql([ + { + name: connectorName, + actionType: 'Opsgenie', + }, + ]); + const connector = await getConnectorByName(connectorName, supertest); + objectRemover.add(connector.id, 'action', 'actions'); + }); + + it('should edit the connector', async () => { + const connectorName = generateUniqueKey(); + const updatedConnectorName = `${connectorName}updated`; + const createdAction = await createOpsgenieConnector(connectorName); + objectRemover.add(createdAction.id, 'action', 'actions'); + browser.refresh(); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + await actions.opsgenie.updateConnectorFields({ + name: updatedConnectorName, + apiUrl: 'https://test.com', + apiKey: 'apiKey', + }); + + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Updated '${updatedConnectorName}'`); + + await testSubjects.click('euiFlyoutCloseButton'); + await pageObjects.triggersActionsUI.searchConnectors(updatedConnectorName); + + const searchResultsAfterEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsAfterEdit).to.eql([ + { + name: updatedConnectorName, + actionType: 'Opsgenie', + }, + ]); + }); + + it('should reset connector when canceling an edit', async () => { + const connectorName = generateUniqueKey(); + const createdAction = await createOpsgenieConnector(connectorName); + objectRemover.add(createdAction.id, 'action', 'actions'); + browser.refresh(); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + await testSubjects.setValue('nameInput', 'some test name to cancel'); + await testSubjects.click('edit-connector-flyout-close-btn'); + await testSubjects.click('confirmModalConfirmButton'); + + await find.waitForDeletedByCssSelector( + '[data-test-subj="edit-connector-flyout-close-btn"]' + ); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + expect(await testSubjects.getAttribute('nameInput', 'value')).to.eql(connectorName); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + it('should disable the run button when the message field is not filled', async () => { + const connectorName = generateUniqueKey(); + const createdAction = await createOpsgenieConnector(connectorName); + objectRemover.add(createdAction.id, 'action', 'actions'); + browser.refresh(); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + await find.clickByCssSelector('[data-test-subj="testConnectorTab"]'); + + expect(await (await testSubjects.find('executeActionButton')).isEnabled()).to.be(false); + }); + }); + + describe('alerts page', () => { + const defaultAlias = '{{rule.id}}:{{alert.id}}'; + const connectorName = generateUniqueKey(); + + before(async () => { + const createdAction = await createOpsgenieConnector(connectorName); + objectRemover.add(createdAction.id, 'action', 'actions'); + + await pageObjects.common.navigateToApp('triggersActions'); + }); + + beforeEach(async () => { + await setupRule(); + await selectOpsgenieConnectorInRuleAction(connectorName); + }); + + afterEach(async () => { + await rules.common.cancelRuleCreation(); + }); + + it('should default to the create alert action', async () => { + expect(await testSubjects.getAttribute('opsgenie-subActionSelect', 'value')).to.eql( + 'createAlert' + ); + + expect(await testSubjects.getAttribute('aliasInput', 'value')).to.eql(defaultAlias); + }); + + it('should default to the close alert action when setting the run when to recovered', async () => { + await testSubjects.click('addNewActionConnectorActionGroup-0'); + await testSubjects.click('addNewActionConnectorActionGroup-0-option-recovered'); + + expect(await testSubjects.getAttribute('opsgenie-subActionSelect', 'value')).to.eql( + 'closeAlert' + ); + expect(await testSubjects.getAttribute('aliasInput', 'value')).to.eql(defaultAlias); + }); + + it('should preserve the alias when switching between create and close alert actions', async () => { + await testSubjects.setValue('aliasInput', 'new alias'); + await testSubjects.selectValue('opsgenie-subActionSelect', 'closeAlert'); + + expect(await testSubjects.getAttribute('opsgenie-subActionSelect', 'value')).to.be( + 'closeAlert' + ); + expect(await testSubjects.getAttribute('aliasInput', 'value')).to.be('new alias'); + }); + + it('should not preserve the message when switching to close alert and back to create alert', async () => { + await testSubjects.setValue('messageInput', 'a message'); + await testSubjects.selectValue('opsgenie-subActionSelect', 'closeAlert'); + + await testSubjects.missingOrFail('messageInput'); + await retry.waitFor('message input to be displayed', async () => { + await testSubjects.selectValue('opsgenie-subActionSelect', 'createAlert'); + return await testSubjects.exists('messageInput'); + }); + + expect(await testSubjects.getAttribute('messageInput', 'value')).to.be(''); + }); + + it('should not preserve the alias when switching run when to recover', async () => { + await testSubjects.setValue('aliasInput', 'an alias'); + await testSubjects.click('addNewActionConnectorActionGroup-0'); + await testSubjects.click('addNewActionConnectorActionGroup-0-option-recovered'); + + await testSubjects.missingOrFail('messageInput'); + + expect(await testSubjects.getAttribute('aliasInput', 'value')).to.be(defaultAlias); + }); + + it('should not preserve the alias when switching run when to threshold met', async () => { + await testSubjects.click('addNewActionConnectorActionGroup-0'); + await testSubjects.click('addNewActionConnectorActionGroup-0-option-recovered'); + await testSubjects.missingOrFail('messageInput'); + + await testSubjects.setValue('aliasInput', 'an alias'); + await testSubjects.click('addNewActionConnectorActionGroup-0'); + await testSubjects.click('addNewActionConnectorActionGroup-0-option-threshold met'); + await testSubjects.exists('messageInput'); + + expect(await testSubjects.getAttribute('aliasInput', 'value')).to.be(defaultAlias); + }); + }); + + const setupRule = async () => { + const alertName = generateUniqueKey(); + await retry.try(async () => { + await rules.common.defineIndexThresholdAlert(alertName); + }); + + await rules.common.setNotifyThrottleInput(); + }; + + const selectOpsgenieConnectorInRuleAction = async (name: string) => { + await testSubjects.click('.opsgenie-alerting-ActionTypeSelectOption'); + await testSubjects.selectValue('comboBoxInput', name); + }; + + const createOpsgenieConnector = async (name: string) => { + return createConnector({ + name, + config: { apiUrl: 'https//test.com' }, + secrets: { apiKey: '1234' }, + connectorTypeId: '.opsgenie', + supertest, + }); + }; + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/utils.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/utils.ts new file mode 100644 index 0000000000000..f8b7ea162c0ff --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/utils.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; +import { findIndex } from 'lodash'; + +import { ObjectRemover } from '../../../lib/object_remover'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getTestActionData } from '../../../lib/get_test_data'; + +export const createSlackConnectorAndObjectRemover = async ({ + getService, +}: { + getService: FtrProviderContext['getService']; +}) => { + const supertest = getService('supertest'); + const objectRemover = new ObjectRemover(supertest); + + const testData = getTestActionData(); + const createdAction = await createSlackConnector({ + name: testData.name, + supertest, + }); + objectRemover.add(createdAction.id, 'action', 'actions'); + + return objectRemover; +}; + +export const createSlackConnector = async ({ + name, + supertest, +}: { + name: string; + supertest: SuperTest.SuperTest; +}) => { + const connector = await createConnector({ + name, + config: {}, + secrets: { webhookUrl: 'https://test.com' }, + connectorTypeId: '.slack', + supertest, + }); + + return connector; +}; + +export const getConnectorByName = async ( + name: string, + supertest: SuperTest.SuperTest +) => { + const { body } = await supertest + .get(`/api/actions/connectors`) + .set('kbn-xsrf', 'foo') + .expect(200); + const i = findIndex(body, (c: any) => c.name === name); + return body[i]; +}; + +export const createConnector = async ({ + name, + config, + secrets, + connectorTypeId, + supertest, +}: { + name: string; + config: Record; + secrets: Record; + connectorTypeId: string; + supertest: SuperTest.SuperTest; +}) => { + const { body: createdAction } = await supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name, + config, + secrets, + connector_type_id: connectorTypeId, + }) + .expect(200); + + return createdAction; +}; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 36b2b0ae44aee..50641faa1e91e 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -13,6 +13,7 @@ import { pageObjects } from './page_objects'; // .server-log is specifically not enabled const enabledActionTypes = [ + '.opsgenie', '.email', '.index', '.pagerduty', diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index d26b1124f8271..f72677bff7f05 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -18,6 +18,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) const find = getService('find'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const rules = getService('rules'); function getRowItemData(row: CustomCheerio, $: CustomCheerioStatic) { return { @@ -164,10 +165,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) await switchBtn.click(); }, async clickCreateAlertButton() { - const createBtn = await find.byCssSelector( - '[data-test-subj="createRuleButton"],[data-test-subj="createFirstRuleButton"]' - ); - await createBtn.click(); + await rules.common.clickCreateAlertButton(); }, async setAlertName(value: string) { await testSubjects.setValue('ruleNameInput', value); From 75f2d0c714b6d2239b895a170952a0c65fe89d34 Mon Sep 17 00:00:00 2001 From: Stef Nestor Date: Tue, 18 Oct 2022 14:18:36 -0600 Subject: [PATCH 56/74] Append DevTool commands automatically prefix Space (#140443) In DevTools, if you go to run `kbn:/s/foo/api/MY_REQUEST` then IF default space you effectively run `KIBANA/s/foo/api/MY_REQUEST` BUT IF non-default space e.g. `admin` you end up running `KIBANA/s/admin/s/foo/api/MY_REQUEST` which is invalid. This is not pointed out in Dev Tools and since this page updated to the emphasize the DevTools example, this is tripping up more users who think it should work via this page. Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/api.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index aa567487b296a..f71b32fa5b9ba 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -17,6 +17,8 @@ For example: `GET kbn:/api/index_management/indices` -------------------------------------------------- +Note: this will automatically prefix `s/{space_id}/` on the API request if ran from a non-default Kibana Space. + [float] [[api-authentication]] === Authentication From 4e3461a9b6a758cbe3f381b134aa43fc9d14496a Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Tue, 18 Oct 2022 14:37:03 -0600 Subject: [PATCH 57/74] [Shared UX] Markdown Component (#142228) --- .github/CODEOWNERS | 3 + package.json | 6 + packages/BUILD.bazel | 6 + packages/shared-ux/markdown/impl/BUILD.bazel | 147 +++++++++++ packages/shared-ux/markdown/impl/README.mdx | 33 +++ .../impl/__snapshots__/markdown.test.tsx.snap | 247 ++++++++++++++++++ packages/shared-ux/markdown/impl/index.ts | 9 + .../shared-ux/markdown/impl/jest.config.js | 13 + packages/shared-ux/markdown/impl/kibana.jsonc | 7 + .../shared-ux/markdown/impl/markdown.test.tsx | 31 +++ packages/shared-ux/markdown/impl/markdown.tsx | 78 ++++++ .../markdown/impl/markdown_editor.stories.tsx | 41 +++ .../markdown/impl/markdown_format.stories.tsx | 49 ++++ packages/shared-ux/markdown/impl/package.json | 8 + .../shared-ux/markdown/impl/tsconfig.json | 20 ++ packages/shared-ux/markdown/mocks/BUILD.bazel | 142 ++++++++++ packages/shared-ux/markdown/mocks/README.md | 3 + packages/shared-ux/markdown/mocks/index.ts | 9 + .../shared-ux/markdown/mocks/kibana.jsonc | 7 + .../shared-ux/markdown/mocks/package.json | 8 + .../shared-ux/markdown/mocks/storybook.ts | 88 +++++++ .../shared-ux/markdown/mocks/tsconfig.json | 17 ++ packages/shared-ux/markdown/types/BUILD.bazel | 67 +++++ packages/shared-ux/markdown/types/README.md | 3 + packages/shared-ux/markdown/types/index.d.ts | 25 ++ .../shared-ux/markdown/types/kibana.jsonc | 7 + .../shared-ux/markdown/types/package.json | 8 + .../shared-ux/markdown/types/tsconfig.json | 17 ++ yarn.lock | 24 ++ 29 files changed, 1123 insertions(+) create mode 100644 packages/shared-ux/markdown/impl/BUILD.bazel create mode 100644 packages/shared-ux/markdown/impl/README.mdx create mode 100644 packages/shared-ux/markdown/impl/__snapshots__/markdown.test.tsx.snap create mode 100644 packages/shared-ux/markdown/impl/index.ts create mode 100644 packages/shared-ux/markdown/impl/jest.config.js create mode 100644 packages/shared-ux/markdown/impl/kibana.jsonc create mode 100644 packages/shared-ux/markdown/impl/markdown.test.tsx create mode 100644 packages/shared-ux/markdown/impl/markdown.tsx create mode 100644 packages/shared-ux/markdown/impl/markdown_editor.stories.tsx create mode 100644 packages/shared-ux/markdown/impl/markdown_format.stories.tsx create mode 100644 packages/shared-ux/markdown/impl/package.json create mode 100644 packages/shared-ux/markdown/impl/tsconfig.json create mode 100644 packages/shared-ux/markdown/mocks/BUILD.bazel create mode 100644 packages/shared-ux/markdown/mocks/README.md create mode 100644 packages/shared-ux/markdown/mocks/index.ts create mode 100644 packages/shared-ux/markdown/mocks/kibana.jsonc create mode 100644 packages/shared-ux/markdown/mocks/package.json create mode 100644 packages/shared-ux/markdown/mocks/storybook.ts create mode 100644 packages/shared-ux/markdown/mocks/tsconfig.json create mode 100644 packages/shared-ux/markdown/types/BUILD.bazel create mode 100644 packages/shared-ux/markdown/types/README.md create mode 100644 packages/shared-ux/markdown/types/index.d.ts create mode 100644 packages/shared-ux/markdown/types/kibana.jsonc create mode 100644 packages/shared-ux/markdown/types/package.json create mode 100644 packages/shared-ux/markdown/types/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 863edb6925730..54356e4ebb4b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -990,6 +990,9 @@ packages/shared-ux/card/no_data/types @elastic/shared-ux packages/shared-ux/link/redirect_app/impl @elastic/shared-ux packages/shared-ux/link/redirect_app/mocks @elastic/shared-ux packages/shared-ux/link/redirect_app/types @elastic/shared-ux +packages/shared-ux/markdown/impl @elastic/shared-ux +packages/shared-ux/markdown/mocks @elastic/shared-ux +packages/shared-ux/markdown/types @elastic/shared-ux packages/shared-ux/page/analytics_no_data/impl @elastic/shared-ux packages/shared-ux/page/analytics_no_data/mocks @elastic/shared-ux packages/shared-ux/page/analytics_no_data/types @elastic/shared-ux diff --git a/package.json b/package.json index e65e82fa8cef2..bd1fe819a8402 100644 --- a/package.json +++ b/package.json @@ -373,6 +373,9 @@ "@kbn/shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/impl", "@kbn/shared-ux-link-redirect-app-mocks": "link:bazel-bin/packages/shared-ux/link/redirect_app/mocks", "@kbn/shared-ux-link-redirect-app-types": "link:bazel-bin/packages/shared-ux/link/redirect_app/types", + "@kbn/shared-ux-markdown": "link:bazel-bin/packages/shared-ux/markdown/impl", + "@kbn/shared-ux-markdown-mocks": "link:bazel-bin/packages/shared-ux/markdown/mocks", + "@kbn/shared-ux-markdown-types": "link:bazel-bin/packages/shared-ux/markdown/types", "@kbn/shared-ux-page-analytics-no-data": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/impl", "@kbn/shared-ux-page-analytics-no-data-mocks": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/mocks", "@kbn/shared-ux-page-analytics-no-data-types": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/types", @@ -1133,6 +1136,9 @@ "@types/kbn__shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/impl/npm_module_types", "@types/kbn__shared-ux-link-redirect-app-mocks": "link:bazel-bin/packages/shared-ux/link/redirect_app/mocks/npm_module_types", "@types/kbn__shared-ux-link-redirect-app-types": "link:bazel-bin/packages/shared-ux/link/redirect_app/types/npm_module_types", + "@types/kbn__shared-ux-markdown": "link:bazel-bin/packages/shared-ux/markdown/impl/npm_module_types", + "@types/kbn__shared-ux-markdown-mocks": "link:bazel-bin/packages/shared-ux/markdown/mocks/npm_module_types", + "@types/kbn__shared-ux-markdown-types": "link:bazel-bin/packages/shared-ux/markdown/types/npm_module_types", "@types/kbn__shared-ux-page-analytics-no-data": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/impl/npm_module_types", "@types/kbn__shared-ux-page-analytics-no-data-mocks": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/mocks/npm_module_types", "@types/kbn__shared-ux-page-analytics-no-data-types": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/types/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 3eb7a3f694954..0b3494b10fc46 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -316,6 +316,9 @@ filegroup( "//packages/shared-ux/link/redirect_app/impl:build", "//packages/shared-ux/link/redirect_app/mocks:build", "//packages/shared-ux/link/redirect_app/types:build", + "//packages/shared-ux/markdown/impl:build", + "//packages/shared-ux/markdown/mocks:build", + "//packages/shared-ux/markdown/types:build", "//packages/shared-ux/page/analytics_no_data/impl:build", "//packages/shared-ux/page/analytics_no_data/mocks:build", "//packages/shared-ux/page/analytics_no_data/types:build", @@ -641,6 +644,9 @@ filegroup( "//packages/shared-ux/card/no_data/mocks:build_types", "//packages/shared-ux/link/redirect_app/impl:build_types", "//packages/shared-ux/link/redirect_app/mocks:build_types", + "//packages/shared-ux/markdown/impl:build_types", + "//packages/shared-ux/markdown/mocks:build_types", + "//packages/shared-ux/markdown/types:build_types", "//packages/shared-ux/page/analytics_no_data/impl:build_types", "//packages/shared-ux/page/analytics_no_data/mocks:build_types", "//packages/shared-ux/page/kibana_no_data/impl:build_types", diff --git a/packages/shared-ux/markdown/impl/BUILD.bazel b/packages/shared-ux/markdown/impl/BUILD.bazel new file mode 100644 index 0000000000000..838edc4628ebc --- /dev/null +++ b/packages/shared-ux/markdown/impl/BUILD.bazel @@ -0,0 +1,147 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "impl" +PKG_REQUIRE_NAME = "@kbn/shared-ux-markdown" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + "**/*.mdx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//react", + "@npm//enzyme", + "@npm//@elastic/eui", + "//packages/kbn-ambient-ui-types", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "@npm//@elastic/eui", + "//packages/kbn-ambient-ui-types", + "//packages/kbn-shared-ux-utility:npm_module_types", + "//packages/shared-ux/markdown/mocks", + # "//packages/kbn-shared-ux-markdown-mocks:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/markdown/impl/README.mdx b/packages/shared-ux/markdown/impl/README.mdx new file mode 100644 index 0000000000000..5730f20152f27 --- /dev/null +++ b/packages/shared-ux/markdown/impl/README.mdx @@ -0,0 +1,33 @@ +--- +id: sharedUX/Components/Markdown +slug: /shared-ux/components/markdown +title: Markdown +description: A wrapper around EuiMarkdownEditor and EuiMarkdownFormat. +tags: ['shared-ux', 'component'] +date: 2022-10-03 +--- + +## Description + +This markdown component uses the **EuiMarkdownEditor** and **EuiMarkdownFormat** managed by `@elastic/eui`. If `readOnly` is set to true, and `markdownContent` or `children` are set, then the component renders **EuiMarkdownFormat** text. Otherwise the component will render the **EuiMarkdownEditor**. The height of the component can be set, but in order the control the width of the component, you can place the `` component in another component. +Markdown extends all the EuiMarkdownEditorProps except for the `editorId`, `uiPlugins`, and `MarkdownFormatProps`. + +## Component Properties + +| Prop Name | Type | Description | +|---|---|---| +| `readOnly` | `boolean` | Needed to differentiate where markdown is used as a presentation of error messages. This was previous the MarkdownSimple component | +| `openLinksInNewTab` | `boolean` | An optional property needed to replace the Markdown component from kibana-react | +|`markdownContent` | `string` | This prop can be set along with `readOnly` to display error.message etc text to the kibana user. This property is optional. | +| `ariaLabelContent` | `string` | An optional property to be set for the markdown component. It will be `markdown component` if not set explicitly. | +| `height` | `number` or `'full'` | The height of the markdown component can be set to a number. By default, height is set to `'full'`. To set the width, include a container for the markdown component to be within with set width. | +| `placeholder` | `string` or `undefined` | This prop can be set to a string to display the placeholder content of the markdown component. | +| `defaultValue` | `string` | The default value for the markdown editor. If not set it will default to an empty string. | + +## API + +| Export | Description | +|---|---| +| `Markdown` | This component provides a markdown editor or text to be supported with Markdown formatting (must have static content set for the `markdownContent`) prop and be `readOnly` | +| `MarkdownProps` | Exported by `@kbn/shared-ux-markdown-types` | + diff --git a/packages/shared-ux/markdown/impl/__snapshots__/markdown.test.tsx.snap b/packages/shared-ux/markdown/impl/__snapshots__/markdown.test.tsx.snap new file mode 100644 index 0000000000000..300c89f056be9 --- /dev/null +++ b/packages/shared-ux/markdown/impl/__snapshots__/markdown.test.tsx.snap @@ -0,0 +1,247 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shared ux markdown component renders for editor 1`] = ` +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + +
      +
      +
      +