From d715cf6ec4c0f1b32c9eaccc560c754912832782 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 31 Jan 2023 18:12:14 +0200 Subject: [PATCH 01/59] [Cases] Update UI to use the new connector's API (#149276) ## Summary This PR updates the UI to use the new connector's API introduced by this PR https://github.com/elastic/kibana/pull/147295. In this PR I refactor a bit some of the logic we have for the connectors. I left comments to explain the reasoning. ## Testing Please test the following: - The fields of each connector are preserved correctly when changing connectors - The following callout is shown when you do not have selected any connector Screenshot 2023-01-23 at 2 21 33 PM - The edit connector button (pencil) is not shown if you do not have access to use connectors. Also, the following text should be shown. Screenshot 2023-01-23 at 2 20 30 PM - You see the following callout if a) select a connector to a case and b) then delete the connector Screenshot 2023-01-23 at 2 11 49 PM - The text of the connector's push button is shown correctly. It should be `Push ` for the first push and `Updated Push ` for any other push. Screenshot 2023-01-25 at 12 09 29 PM Screenshot 2023-01-25 at 12 09 17 PM - The connector's push button is disabled correctly. - When the connector's push button is disabled a tooltip is shown Screenshot 2023-01-25 at 12 11 27 PM - The push user action label is displayed correctly. It should be `pushed as new incident...` for the first push and `updated incident...` for any other push. Screenshot 2023-01-25 at 9 54 23 AM Screenshot 2023-01-25 at 11 56 56 AM - The push arrow indicators are shown correctly. The top arrow should be displayed only if it is the latest push. The bottom arrow should be displayed only if it is the latest push and new changes need to be pushed. Changes are considered the update of text or description, the addition or edit of a comment, and the addition or removal of a case. Screenshot 2023-01-11 at 1 38 37 PM Screenshot 2023-01-11 at 1 37 31 PM - The "View " shows the latest push incident of each connector when changing connectors. Screenshot 2023-01-23 at 2 25 30 PM Depends on: https://github.com/elastic/kibana/pull/149451, https://github.com/elastic/kibana/pull/149535 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Jonathan Buttner --- .../cases/common/api/connectors/connector.ts | 11 - x-pack/plugins/cases/common/api/helpers.ts | 5 + x-pack/plugins/cases/common/ui/types.ts | 4 +- .../cases/public/common/mock/connectors.ts | 37 +- .../all_cases/all_cases_list.test.tsx | 6 +- .../components/all_cases/all_cases_list.tsx | 4 +- .../components/all_cases/index.test.tsx | 6 +- .../case_action_bar/actions.test.tsx | 8 - .../components/case_action_bar/actions.tsx | 21 +- .../components/case_action_bar/index.test.tsx | 86 ++- .../components/case_action_bar/index.tsx | 193 +++-- .../case_view/case_view_page.test.tsx | 41 +- .../components/case_view/case_view_page.tsx | 1 - .../components/case_view_activity.test.tsx | 32 +- .../components/case_view_activity.tsx | 65 +- .../components/case_view/index.test.tsx | 6 +- .../public/components/case_view/mocks.ts | 2 - .../case_view/use_on_update_field.ts | 2 +- .../configure_cases/connectors.test.tsx | 2 +- .../connectors_dropdown.test.tsx | 8 +- .../components/configure_cases/index.test.tsx | 12 +- .../components/configure_cases/index.tsx | 4 +- .../components/connectors/card.test.tsx | 56 +- .../public/components/connectors/card.tsx | 63 +- .../connectors/jira/case_fields.test.tsx | 15 +- .../servicenow_itsm_case_fields.test.tsx | 14 +- .../servicenow_sir_case_fields.test.tsx | 32 +- .../public/components/create/form.test.tsx | 6 +- .../components/create/form_context.test.tsx | 8 +- .../public/components/create/form_context.tsx | 5 +- .../public/components/create/index.test.tsx | 6 +- .../components/edit_connector/helpers.test.ts | 88 --- .../components/edit_connector/helpers.ts | 27 - .../components/edit_connector/index.test.tsx | 283 ++++--- .../components/edit_connector/index.tsx | 260 +++---- .../edit_connector/push_button.test.tsx | 91 +++ .../components/edit_connector/push_button.tsx | 60 ++ .../edit_connector/push_callouts.test.tsx | 38 + .../edit_connector/push_callouts.tsx | 37 + .../components/edit_connector/translations.ts | 6 + .../use_push_to_service/callout/index.tsx | 35 +- .../use_push_to_service/helpers.tsx | 9 +- .../use_push_to_service/index.test.tsx | 697 +++++++++++------- .../components/use_push_to_service/index.tsx | 128 +--- .../use_push_to_service/translations.ts | 11 +- .../user_actions/comment/comment.tsx | 4 +- .../components/user_actions/index.test.tsx | 42 +- .../public/components/user_actions/index.tsx | 6 +- .../public/components/user_actions/mock.ts | 16 +- .../components/user_actions/pushed.test.tsx | 152 ++-- .../public/components/user_actions/pushed.tsx | 145 ++-- .../public/components/user_actions/types.ts | 13 +- .../cases/public/containers/__mocks__/api.ts | 8 +- .../cases/public/containers/api.test.tsx | 33 + x-pack/plugins/cases/public/containers/api.ts | 28 + .../containers/configure/__mocks__/api.ts | 5 +- .../public/containers/configure/api.test.ts | 6 +- .../cases/public/containers/configure/api.ts | 4 +- ...> use_get_supported_action_connectors.tsx} | 6 +- ..._supported_action_connectors.tsx.test.tsx} | 14 +- .../cases/public/containers/constants.ts | 4 +- .../plugins/cases/public/containers/mock.ts | 32 +- .../use_find_case_user_actions.test.tsx | 680 ++--------------- .../containers/use_find_case_user_actions.tsx | 208 +----- .../use_get_case_connectors.test.tsx | 58 ++ .../containers/use_get_case_connectors.tsx | 37 + .../server/client/user_actions/connectors.ts | 29 +- .../cypress/e2e/cases/connector_options.cy.ts | 4 +- .../cypress/screens/case_details.ts | 4 +- .../common/lib/connectors.ts | 8 +- .../tests/trial/internal/get_connectors.ts | 57 +- 71 files changed, 1968 insertions(+), 2166 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts delete mode 100644 x-pack/plugins/cases/public/components/edit_connector/helpers.ts create mode 100644 x-pack/plugins/cases/public/components/edit_connector/push_button.test.tsx create mode 100644 x-pack/plugins/cases/public/components/edit_connector/push_button.tsx create mode 100644 x-pack/plugins/cases/public/components/edit_connector/push_callouts.test.tsx create mode 100644 x-pack/plugins/cases/public/components/edit_connector/push_callouts.tsx rename x-pack/plugins/cases/public/containers/configure/{use_connectors.tsx => use_get_supported_action_connectors.tsx} (85%) rename x-pack/plugins/cases/public/containers/configure/{use_connectors.test.tsx => use_get_supported_action_connectors.tsx.test.tsx} (78%) create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_case_connectors.tsx diff --git a/x-pack/plugins/cases/common/api/connectors/connector.ts b/x-pack/plugins/cases/common/api/connectors/connector.ts index f3c733c85cd8cd..7d96593d013169 100644 --- a/x-pack/plugins/cases/common/api/connectors/connector.ts +++ b/x-pack/plugins/cases/common/api/connectors/connector.ts @@ -18,14 +18,6 @@ import { SwimlaneFieldsRT } from './swimlane'; export type ActionConnector = ActionResult; export type ActionTypeConnector = ActionType; -export const ConnectorFieldsRt = rt.union([ - JiraFieldsRT, - ResilientFieldsRT, - ServiceNowITSMFieldsRT, - ServiceNowSIRFieldsRT, - rt.null, -]); - export enum ConnectorTypes { casesWebhook = '.cases-webhook', jira = '.jira', @@ -114,6 +106,3 @@ export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< typeof ConnectorServiceNowITSMTypeFieldsRt >; export type ConnectorServiceNowSIRTypeFields = rt.TypeOf; - -// we need to change these types back and forth for storing in ES (arrays overwrite, objects merge) -export type ConnectorFields = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts index 211e9ae6c9b092..ae1f89f4071b71 100644 --- a/x-pack/plugins/cases/common/api/helpers.ts +++ b/x-pack/plugins/cases/common/api/helpers.ts @@ -17,6 +17,7 @@ import { CASE_COMMENT_DELETE_URL, CASE_FIND_USER_ACTIONS_URL, INTERNAL_BULK_GET_ATTACHMENTS_URL, + INTERNAL_CONNECTORS_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -62,3 +63,7 @@ export const getCasesFromAlertsUrl = (alertId: string): string => { export const getCaseBulkGetAttachmentsUrl = (id: string): string => { return INTERNAL_BULK_GET_ATTACHMENTS_URL.replace('{case_id}', id); }; + +export const getCaseConnectorsUrl = (id: string): string => { + return INTERNAL_CONNECTORS_URL.replace('{case_id}', id); +}; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 8231dd945bf9e0..f4df0c1b72f98c 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -17,7 +17,6 @@ import type { CaseStatuses, User, ActionConnector, - CaseExternalServiceBasic, CaseUserActionResponse, SingleCaseMetricsResponse, CommentResponse, @@ -30,6 +29,7 @@ import type { CaseSeverity, CommentResponseExternalReferenceType, CommentResponseTypePersistableState, + GetCaseConnectorsResponse, } from '../api'; import type { PUSH_CASES_CAPABILITY } from '../constants'; import type { SnakeToCamelCase } from '../types'; @@ -81,12 +81,12 @@ export type CaseUserActions = SnakeToCamelCase; export type FindCaseUserActions = Omit, 'userActions'> & { userActions: CaseUserActions[]; }; -export type CaseExternalService = SnakeToCamelCase; export type Case = Omit, 'comments'> & { comments: Comment[] }; export type Cases = Omit, 'cases'> & { cases: Case[] }; export type CasesStatus = SnakeToCamelCase; export type CasesMetrics = SnakeToCamelCase; export type CaseUpdateRequest = SnakeToCamelCase; +export type CaseConnectors = SnakeToCamelCase; export interface ResolvedCase { case: Case; diff --git a/x-pack/plugins/cases/public/common/mock/connectors.ts b/x-pack/plugins/cases/public/common/mock/connectors.ts index e0b7b26a4c8a4d..8a2cf618baff36 100644 --- a/x-pack/plugins/cases/public/common/mock/connectors.ts +++ b/x-pack/plugins/cases/public/common/mock/connectors.ts @@ -6,12 +6,14 @@ */ import type { ActionConnector, ActionTypeConnector } from '../../../common/api'; +import { basicPush } from '../../containers/mock'; +import type { CaseConnectors } from '../../containers/types'; export const connectorsMock: ActionConnector[] = [ { id: 'servicenow-1', actionTypeId: '.servicenow', - name: 'My Connector', + name: 'My SN connector', config: { apiUrl: 'https://instance1.service-now.com', }, @@ -21,7 +23,7 @@ export const connectorsMock: ActionConnector[] = [ { id: 'resilient-2', actionTypeId: '.resilient', - name: 'My Connector 2', + name: 'My Resilient connector', config: { apiUrl: 'https://test/', orgId: '201', @@ -52,7 +54,7 @@ export const connectorsMock: ActionConnector[] = [ { id: 'servicenow-uses-table-api', actionTypeId: '.servicenow', - name: 'My Connector', + name: 'My deprecated SN connector', config: { apiUrl: 'https://instance1.service-now.com', usesTableApi: true, @@ -118,3 +120,32 @@ export const actionTypesMock: ActionTypeConnector[] = [ supportedFeatureIds: ['alerting', 'cases'], }, ]; + +export const getCaseConnectorsMockResponse = ( + overrides: Partial = {} +): CaseConnectors => { + return connectorsMock.reduce( + (acc, connector) => ({ + ...acc, + [connector.id]: { + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: null, + push: { + needsToBePushed: false, + oldestUserActionPushDate: '2023-01-17T09:46:29.813Z', + latestUserActionPushDate: '2023-01-17T09:46:29.813Z', + hasBeenPushed: true, + externalService: { + ...basicPush, + connectorId: connector.id, + connectorName: connector.name, + }, + ...overrides, + }, + }, + }), + {} + ); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 65156c39202b4d..909bb1dd24ea00 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -36,7 +36,7 @@ import { registerConnectorsToMockActionRegistry } from '../../common/mock/regist import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { waitForComponentToUpdate } from '../../common/test_utils'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCases, DEFAULT_QUERY_PARAMS } from '../../containers/use_get_cases'; @@ -52,7 +52,7 @@ jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/user_profiles/use_get_current_user_profile'); jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../common/lib/kibana'); jest.mock('../../common/navigation/hooks'); jest.mock('../app/use_available_owners', () => ({ @@ -66,7 +66,7 @@ const useGetTagsMock = useGetTags as jest.Mock; const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; const useKibanaMock = useKibana as jest.MockedFunction; -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; const useUpdateCaseMock = useUpdateCase as jest.Mock; const useLicenseMock = useLicense as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 3b2b510a26f82a..ab50f16083f8bb 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -23,7 +23,7 @@ import type { EuiBasicTableOnChange } from './types'; import { CasesTable } from './table'; import { useCasesContext } from '../cases_context/use_cases_context'; import { CasesMetrics } from './cases_metrics'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { initialData, useGetCases } from '../../containers/use_get_cases'; import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; @@ -98,7 +98,7 @@ export const AllCasesList = React.memo( const { data: currentUserProfile, isLoading: isLoadingCurrentUserProfile } = useGetCurrentUserProfile(); - const { data: connectors = [] } = useGetConnectors(); + const { data: connectors = [] } = useGetSupportedActionConnectors(); const sorting = useMemo( () => ({ diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 72cdbc0c0ed5b5..7ce32a2f123a58 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -13,7 +13,7 @@ import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer, noCreateCasesPermissions } from '../../common/mock'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { connectorsMock, useGetCasesMockState } from '../../containers/mock'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetCases } from '../../containers/use_get_cases'; import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; @@ -26,14 +26,14 @@ jest.mock('../../containers/use_get_action_license', () => { useGetActionLicense: jest.fn(), }; }); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/api'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/user_profiles/use_get_current_user_profile'); jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); jest.mock('../../api'); -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetActionLicenseMock = useGetActionLicense as jest.Mock; const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx index 16bef4b933d6ec..195d02f7931cf2 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx @@ -29,10 +29,6 @@ jest.mock('react-router-dom', () => { }); const defaultProps = { - allCasesNavigation: { - href: 'all-cases-href', - onClick: () => {}, - }, caseData: basicCase, currentExternalIncident: null, }; @@ -123,10 +119,6 @@ describe('CaseView actions', () => { {...defaultProps} currentExternalIncident={{ ...basicPush, - firstPushIndex: 5, - lastPushIndex: 5, - commentsToUpdate: [], - hasDataToPush: false, }} /> diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index b0697f9c962e71..d464946b60f821 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -13,14 +13,13 @@ import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { PropertyActions } from '../property_actions'; import type { Case } from '../../../common/ui/types'; -import type { CaseService } from '../../containers/use_find_case_user_actions'; import { useAllCasesNavigation } from '../../common/navigation'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesToast } from '../../common/use_cases_toast'; interface CaseViewActions { caseData: Case; - currentExternalIncident: CaseService | null; + currentExternalIncident: Case['externalService']; } const ActionsComponent: React.FC = ({ caseData, currentExternalIncident }) => { @@ -48,22 +47,22 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal showSuccessToast(i18n.COPY_ID_ACTION_SUCCESS); }, }, - ...(permissions.delete + ...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl) ? [ { - iconType: 'trash', - label: i18n.DELETE_CASE(), - color: 'danger' as const, - onClick: openModal, + iconType: 'popout', + label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''), + onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'), }, ] : []), - ...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl) + ...(permissions.delete ? [ { - iconType: 'popout', - label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''), - onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'), + iconType: 'trash', + label: i18n.DELETE_CASE(), + color: 'danger' as const, + onClick: openModal, }, ] : []), diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 81560f99e5427e..7263514fb6baa5 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { mount } from 'enzyme'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { basicCase, caseUserActions, getAlertUserAction } from '../../containers/mock'; +import { basicCase } from '../../containers/mock'; import type { CaseActionBarProps } from '.'; import { CaseActionBar } from '.'; import { @@ -19,42 +19,31 @@ import { noUpdateCasesPermissions, TestProviders, } from '../../common/mock'; -import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; +import { useGetCaseConnectors } from '../../containers/use_get_case_connectors'; import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; -jest.mock('../../containers/use_find_case_user_actions'); +jest.mock('../../containers/use_get_case_connectors'); jest.mock('../case_view/use_on_refresh_case_view_page'); -const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock; -const defaultUseFindCaseUserActions = { - data: { - caseUserActions: [...caseUserActions, getAlertUserAction()], - caseServices: {}, - hasDataToPush: false, - participants: [basicCase.createdBy], - }, - isLoading: false, - isError: false, -}; +const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock; describe('CaseActionBar', () => { + const caseConnectors = getCaseConnectorsMockResponse(); + const onUpdateField = jest.fn(); - const defaultProps = { - allCasesNavigation: { - href: 'all-cases-href', - onClick: () => {}, - }, + const defaultProps: CaseActionBarProps = { caseData: basicCase, - disableAlerting: false, isLoading: false, onUpdateField, - currentExternalIncident: null, - metricsFeatures: [], }; beforeEach(() => { jest.clearAllMocks(); - useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions); + useGetCaseConnectorsMock.mockReturnValue({ + isLoading: false, + data: caseConnectors, + }); }); it('renders', () => { @@ -70,21 +59,6 @@ describe('CaseActionBar', () => { expect(wrapper.find(`[data-test-subj="sync-alerts-switch"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="case-refresh"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="case-view-actions"]`).exists()).toBeTruthy(); - // no loading bar - expect(wrapper.find(`[data-test-subj="case-view-action-bar-spinner"]`).exists()).toBeFalsy(); - }); - - it('shows a loading bar when user actions are loaded', async () => { - useFindCaseUserActionsMock.mockReturnValue({ - data: undefined, - isLoading: true, - }); - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="case-view-action-bar-spinner"]`).exists()).toBeTruthy(); }); it('should show correct status', () => { @@ -249,4 +223,38 @@ describe('CaseActionBar', () => { userEvent.click(screen.getByTestId('property-actions-ellipses')); expect(queryByText('Delete case')).toBeInTheDocument(); }); + + it('shows the external incident action', async () => { + const connector = caseConnectors['servicenow-1']; + const { push, ...connectorWithoutPush } = connector; + + const props = { + ...defaultProps, + caseData: { ...defaultProps.caseData, connector: connectorWithoutPush }, + }; + + render( + + + + ); + + userEvent.click(screen.getByTestId('property-actions-ellipses')); + + await waitFor(() => { + expect(screen.getByTestId('property-actions-popout')).toBeInTheDocument(); + }); + }); + + it('does not show the external incident action', async () => { + render( + + + + ); + + userEvent.click(screen.getByTestId('property-actions-ellipses')); + + expect(screen.queryByTestId('property-actions-popout')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index d21427d5798939..ba65234cd56ab4 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; import { EuiButtonEmpty, @@ -15,13 +15,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, - EuiLoadingSpinner, } from '@elastic/eui'; import type { Case } from '../../../common/ui/types'; import type { CaseStatuses } from '../../../common/api'; import * as i18n from '../case_view/translations'; import { Actions } from './actions'; -import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; import { StatusContextMenu } from './status_context_menu'; import { SyncAlertsSwitch } from '../case_settings/sync_alerts_switch'; import type { OnUpdateFields } from '../case_view/types'; @@ -30,6 +28,7 @@ import { getStatusDate, getStatusTitle } from './helpers'; import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesFeatures } from '../../common/use_cases_features'; +import { useGetCaseConnectors } from '../../containers/use_get_case_connectors'; const MyDescriptionList = styled(EuiDescriptionList)` ${({ theme }) => css` @@ -56,9 +55,14 @@ const CaseActionBarComponent: React.FC = ({ }) => { const { permissions } = useCasesContext(); const { isSyncAlertsEnabled, metricsFeatures } = useCasesFeatures(); - const date = useMemo(() => getStatusDate(caseData), [caseData]); - const title = useMemo(() => getStatusTitle(caseData.status), [caseData.status]); + + const { data: caseConnectors } = useGetCaseConnectors(caseData.id); + + const date = getStatusDate(caseData); + const title = getStatusTitle(caseData.status); + const refreshCaseViewPage = useRefreshCaseViewPage(); + const onStatusChanged = useCallback( (status: CaseStatuses) => onUpdateField({ @@ -68,19 +72,8 @@ const CaseActionBarComponent: React.FC = ({ [onUpdateField] ); - const { data: userActionsData, isLoading: isLoadingUserActions } = useFindCaseUserActions( - caseData.id, - caseData.connector.id - ); - - const currentExternalIncident = useMemo( - () => - userActionsData?.caseServices != null && - userActionsData.caseServices[caseData.connector.id] != null - ? userActionsData.caseServices[caseData.connector.id] - : null, - [userActionsData?.caseServices, caseData.connector] - ); + const currentExternalIncident = + caseConnectors?.[caseData.connector.id]?.push.externalService ?? null; const onSyncAlertsChanged = useCallback( (syncAlerts: boolean) => @@ -93,92 +86,84 @@ const CaseActionBarComponent: React.FC = ({ return ( - {isLoadingUserActions ? ( - - - - ) : ( - <> - - - - - {i18n.STATUS} - - - - - {!metricsFeatures.includes('lifespan') ? ( - - {title} - - - - - ) : null} - - - - - - - {permissions.update && isSyncAlertsEnabled && ( - - - - - {i18n.SYNC_ALERTS} - - - - - - - - - - - )} - - - - {i18n.CASE_REFRESH} - - - - - - - - - )} + + + + + {i18n.STATUS} + + + + + {!metricsFeatures.includes('lifespan') ? ( + + {title} + + + + + ) : null} + + + + + + + {permissions.update && isSyncAlertsEnabled && ( + + + + + {i18n.SYNC_ALERTS} + + + + + + + + + + + )} + + + + {i18n.CASE_REFRESH} + + + + + + + ); }; diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index 1dc62eeba4d979..6c5814c7830f91 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -14,7 +14,7 @@ import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import '../../common/mock/match_media'; import { useCaseViewNavigation, useUrlParams } from '../../common/navigation/hooks'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { basicCaseClosed, connectorsMock } from '../../containers/mock'; import type { UseGetCase } from '../../containers/use_get_case'; import { useGetCase } from '../../containers/use_get_case'; @@ -22,6 +22,7 @@ import { useGetCaseMetrics } from '../../containers/use_get_case_metrics'; import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; import { useGetTags } from '../../containers/use_get_tags'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; +import { useGetCaseConnectors } from '../../containers/use_get_case_connectors'; import { useUpdateCase } from '../../containers/use_update_case'; import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; import { CaseViewPage } from './case_view_page'; @@ -37,6 +38,7 @@ import type { CaseViewPageProps } from './types'; import { userProfiles } from '../../containers/user_profiles/api.mock'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { CASE_VIEW_PAGE_TABS } from '../../../common/types'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_update_case'); @@ -44,8 +46,9 @@ jest.mock('../../containers/use_get_case_metrics'); jest.mock('../../containers/use_find_case_user_actions'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_case'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/use_post_push_to_service'); +jest.mock('../../containers/use_get_case_connectors'); jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); jest.mock('../user_actions/timestamp', () => ({ UserActionTimestamp: () => <>, @@ -59,8 +62,9 @@ const useUrlParamsMock = useUrlParams as jest.Mock; const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock; const useUpdateCaseMock = useUpdateCase as jest.Mock; const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock; -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; +const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock; const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; @@ -94,6 +98,7 @@ describe('CaseViewPage', () => { const pushCaseToExternalService = jest.fn(); const data = caseProps.caseData; let appMockRenderer: AppMockRenderer; + const caseConnectors = getCaseConnectorsMockResponse(); beforeEach(() => { mockGetCase(); @@ -102,6 +107,10 @@ describe('CaseViewPage', () => { useGetCaseMetricsMock.mockReturnValue(defaultGetCaseMetrics); useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions); usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); + useGetCaseConnectorsMock.mockReturnValue({ + isLoading: false, + data: caseConnectors, + }); useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); useBulkGetUserProfilesMock.mockReturnValue({ data: new Map(), isLoading: false }); @@ -249,17 +258,20 @@ describe('CaseViewPage', () => { }); it('should push updates on button click', async () => { - useFindCaseUserActionsMock.mockImplementation(() => ({ - ...defaultUseFindCaseUserActions, + useGetCaseConnectorsMock.mockImplementation(() => ({ + isLoading: false, data: { - ...defaultUseFindCaseUserActions.data, - hasDataToPush: true, + ...caseConnectors, + 'resilient-2': { + ...caseConnectors['resilient-2'], + push: { ...caseConnectors['resilient-2'].push, needsToBePushed: true }, + }, }, })); const result = appMockRenderer.render(); - expect(result.getByTestId('has-data-to-push-button')).toBeInTheDocument(); + expect(result.getByTestId('push-to-external-service')).toBeInTheDocument(); userEvent.click(result.getByTestId('push-to-external-service')); @@ -314,7 +326,7 @@ describe('CaseViewPage', () => { expect(updateObject.updateKey).toEqual('connector'); expect(updateObject.updateValue).toEqual({ id: 'resilient-2', - name: 'My Connector 2', + name: 'My Resilient connector', type: ConnectorTypes.resilient, fields: { incidentTypes: null, @@ -391,20 +403,15 @@ describe('CaseViewPage', () => { it('should show the correct connector name on the push button', async () => { useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); - useFindCaseUserActionsMock.mockImplementation(() => ({ - ...defaultUseFindCaseUserActions, - data: { - ...defaultUseFindCaseUserActions.data, - hasDataToPush: true, - }, - })); const result = appMockRenderer.render( ); await waitFor(() => { - expect(result.getByTestId('has-data-to-push-button')).toHaveTextContent('My Connector 2'); + expect(result.getByTestId('push-to-external-service')).toHaveTextContent( + 'My Resilient connector' + ); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index af796ff796d858..6e7e7348a8ab40 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -59,7 +59,6 @@ export const CaseViewPage = React.memo( const timelineUi = useTimelineContext()?.ui; const { onUpdateField, isLoading, loadingKey } = useOnUpdateField({ - caseId, caseData, }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx index bc6b1d7c0dc811..941a983659183b 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx @@ -21,14 +21,17 @@ import type { Case } from '../../../../common'; import type { CaseViewProps } from '../types'; import { useFindCaseUserActions } from '../../../containers/use_find_case_user_actions'; import { usePostPushToService } from '../../../containers/use_post_push_to_service'; -import { useGetConnectors } from '../../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../../containers/use_get_tags'; import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles'; +import { useGetCaseConnectors } from '../../../containers/use_get_case_connectors'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { waitForComponentToUpdate } from '../../../common/test_utils'; +import { waitFor } from '@testing-library/dom'; +import { getCaseConnectorsMockResponse } from '../../../common/mock/connectors'; jest.mock('../../../containers/use_find_case_user_actions'); -jest.mock('../../../containers/configure/use_connectors'); +jest.mock('../../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../../containers/use_post_push_to_service'); jest.mock('../../user_actions/timestamp', () => ({ UserActionTimestamp: () => <>, @@ -37,6 +40,7 @@ jest.mock('../../../common/navigation/hooks'); jest.mock('../../../containers/use_get_action_license'); jest.mock('../../../containers/use_get_tags'); jest.mock('../../../containers/user_profiles/use_bulk_get_user_profiles'); +jest.mock('../../../containers/use_get_case_connectors'); (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); @@ -76,8 +80,6 @@ const pushCaseToExternalService = jest.fn(); const defaultUseFindCaseUserActions = { data: { caseUserActions: [...caseUserActions, getAlertUserAction()], - caseServices: {}, - hasDataToPush: false, participants: [caseData.createdBy], }, refetch: fetchCaseUserActions, @@ -92,16 +94,23 @@ export const caseProps = { }; const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock; -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; +const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock; describe('Case View Page activity tab', () => { + const caseConnectors = getCaseConnectorsMockResponse(); + beforeAll(() => { useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions); useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); useBulkGetUserProfilesMock.mockReturnValue({ isLoading: false, data: new Map() }); + useGetCaseConnectorsMock.mockReturnValue({ + isLoading: false, + data: caseConnectors, + }); }); let appMockRender: AppMockRenderer; @@ -126,7 +135,7 @@ describe('Case View Page activity tab', () => { expect(result.getByTestId('case-tags')).toBeTruthy(); expect(result.getByTestId('connector-edit-header')).toBeTruthy(); expect(result.getByTestId('case-view-status-action-button')).toBeTruthy(); - expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id); + expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id); await waitForComponentToUpdate(); }); @@ -143,7 +152,7 @@ describe('Case View Page activity tab', () => { expect(result.getByTestId('case-tags')).toBeTruthy(); expect(result.getByTestId('connector-edit-header')).toBeTruthy(); expect(result.queryByTestId('case-view-status-action-button')).not.toBeInTheDocument(); - expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id); + expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id); await waitForComponentToUpdate(); }); @@ -160,7 +169,7 @@ describe('Case View Page activity tab', () => { expect(result.getByTestId('case-tags')).toBeTruthy(); expect(result.getByTestId('connector-edit-header')).toBeTruthy(); expect(result.getByTestId('case-severity-selection')).toBeDisabled(); - expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id); + expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id); await waitForComponentToUpdate(); }); @@ -174,7 +183,7 @@ describe('Case View Page activity tab', () => { const result = appMockRender.render(); expect(result.getByTestId('case-view-loading-content')).toBeTruthy(); expect(result.queryByTestId('case-view-activity')).toBeFalsy(); - expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id); + expect(useFindCaseUserActionsMock).toHaveBeenCalledWith(caseData.id); }); it('should not render the assignees on basic license', () => { @@ -205,7 +214,8 @@ describe('Case View Page activity tab', () => { const result = appMockRender.render(); - expect(result.getByTestId('case-view-edit-connector')).toBeInTheDocument(); - await waitForComponentToUpdate(); + await waitFor(() => { + expect(result.getByTestId('case-view-edit-connector')).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index 6218c152e5c856..bba0106f9984f7 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -5,13 +5,16 @@ * 2.0. */ +/* eslint-disable complexity */ + import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { isEqual, uniq } from 'lodash'; +import { useGetCaseConnectors } from '../../../containers/use_get_case_connectors'; import { useCasesFeatures } from '../../../common/use_cases_features'; import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile'; import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles'; -import { useGetConnectors } from '../../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../../containers/configure/use_get_supported_action_connectors'; import type { CaseSeverity } from '../../../../common/api'; import { useCaseViewNavigation } from '../../../common/navigation'; import type { UseFetchAlertData } from '../../../../common/ui/types'; @@ -25,8 +28,6 @@ import { UserList } from './user_list'; import { useOnUpdateField } from '../use_on_update_field'; import { useCasesContext } from '../../cases_context/use_cases_context'; import * as i18n from '../translations'; -import { getNoneConnector, normalizeActionConnector } from '../../configure_cases/utils'; -import { getConnectorById } from '../../utils'; import { SeveritySidebarSelector } from '../../severity/sidebar_selector'; import { useFindCaseUserActions } from '../../../containers/use_find_case_user_actions'; import { AssignUsers } from './assign_users'; @@ -49,9 +50,12 @@ export const CaseViewActivity = ({ const { getCaseViewUrl } = useCaseViewNavigation(); const { caseAssignmentAuthorized, pushToServiceAuthorized } = useCasesFeatures(); + const { data: caseConnectors, isLoading: isLoadingCaseConnectors } = useGetCaseConnectors( + caseData.id + ); + const { data: userActionsData, isLoading: isLoadingUserActions } = useFindCaseUserActions( - caseData.id, - caseData.connector.id + caseData.id ); const assignees = useMemo( @@ -79,7 +83,6 @@ export const CaseViewActivity = ({ ); const { onUpdateField, isLoading, loadingKey } = useOnUpdateField({ - caseId: caseData.id, caseData, }); @@ -125,37 +128,38 @@ export const CaseViewActivity = ({ [assignees, onUpdateField] ); - const { isLoading: isLoadingConnectors, data: connectors = [] } = useGetConnectors(); - - const [connectorName, isValidConnector] = useMemo(() => { - const connector = connectors.find((c) => c.id === caseData.connector.id); - return [connector?.name ?? '', !!connector]; - }, [connectors, caseData.connector]); + const { isLoading: isLoadingAllAvailableConnectors, data: supportedActionConnectors } = + useGetSupportedActionConnectors(); const onSubmitConnector = useCallback( - (connectorId, connectorFields, onError, onSuccess) => { - const connector = getConnectorById(connectorId, connectors); - const connectorToUpdate = connector - ? normalizeActionConnector(connector) - : getNoneConnector(); - + (connector, onError, onSuccess) => { onUpdateField({ key: 'connector', - value: { ...connectorToUpdate, fields: connectorFields }, + value: connector, onSuccess, onError, }); }, - [onUpdateField, connectors] + [onUpdateField] ); + const showUserActions = + !isLoadingUserActions && + !isLoadingCaseConnectors && + userActionsData && + caseConnectors && + userProfiles; + + const showConnectorSidebar = + pushToServiceAuthorized && userActionsData && caseConnectors && supportedActionConnectors; + return ( <> - {isLoadingUserActions && ( + {(isLoadingUserActions || isLoadingCaseConnectors) && ( )} - {!isLoadingUserActions && userActionsData && userProfiles && ( + {showUserActions && ( - {pushToServiceAuthorized && userActionsData ? ( + {showConnectorSidebar ? ( ) : null} diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index 369947fcc2b87b..1004ce3d2f4378 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -26,7 +26,7 @@ import { useGetCaseMetrics } from '../../containers/use_get_case_metrics'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useKibana } from '../../common/lib/kibana'; import { useFindCaseUserActions } from '../../containers/use_find_case_user_actions'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import CaseView from '.'; @@ -49,7 +49,7 @@ jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_find_case_user_actions'); jest.mock('../../containers/use_get_case'); jest.mock('../../containers/use_get_case_metrics'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/use_post_push_to_service'); jest.mock('../user_actions/timestamp', () => ({ UserActionTimestamp: () => <>, @@ -62,7 +62,7 @@ const useFetchCaseMock = useGetCase as jest.Mock; const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock; const useUpdateCaseMock = useUpdateCase as jest.Mock; const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock; -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useKibanaMock = useKibana as jest.MockedFunction; const useGetTagsMock = useGetTags as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/case_view/mocks.ts b/x-pack/plugins/cases/public/components/case_view/mocks.ts index 88069fe79cfe51..4c4bb40c81df64 100644 --- a/x-pack/plugins/cases/public/components/case_view/mocks.ts +++ b/x-pack/plugins/cases/public/components/case_view/mocks.ts @@ -102,8 +102,6 @@ export const defaultUpdateCaseState = { export const defaultUseFindCaseUserActions = { data: { caseUserActions: [...caseUserActions, getAlertUserAction()], - caseServices: {}, - hasDataToPush: false, participants: [caseData.createdBy], }, refetch: jest.fn(), diff --git a/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts b/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts index 449eb7b2ca8a92..d0fc6f98f0ce67 100644 --- a/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts +++ b/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts @@ -16,7 +16,7 @@ import { useUpdateCase } from '../../containers/use_update_case'; import { getTypedPayload } from '../../containers/utils'; import type { OnUpdateFields } from './types'; -export const useOnUpdateField = ({ caseData, caseId }: { caseData: Case; caseId: string }) => { +export const useOnUpdateField = ({ caseData }: { caseData: Case }) => { const { isLoading, updateKey: loadingKey, updateCaseProperty } = useUpdateCase(); const onUpdateField = useCallback( diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 2057d0153be68f..19ac7d0b667c1c 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -121,7 +121,7 @@ describe('Connectors', () => { newWrapper .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .text() - ).toBe('Update My Connector'); + ).toBe('Update My SN connector'); }); it('shows the deprecated callout when the connector is deprecated', async () => { diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 3f3bdedff3e3c5..9c5681c4025b61 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -83,7 +83,7 @@ describe('ConnectorsDropdown', () => { grow={false} > - My Connector + My SN connector , @@ -108,7 +108,7 @@ describe('ConnectorsDropdown', () => { grow={false} > - My Connector 2 + My Resilient connector , @@ -183,7 +183,7 @@ describe('ConnectorsDropdown', () => { grow={false} > - My Connector + My deprecated SN connector (deprecated) @@ -235,7 +235,7 @@ describe('ConnectorsDropdown', () => { .find('[data-test-subj="dropdown-connectors"]') .first() .text() - .includes('My Connector, is selected') + .includes('My SN connector, is selected') ).toBeTruthy(); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 2699557bb51f11..bad53d324da3c9 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -28,15 +28,15 @@ import { import { ConnectorTypes } from '../../../common/api'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { useGetActionTypes } from '../../containers/configure/use_action_types'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; jest.mock('../../common/lib/kibana'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../../containers/configure/use_action_types'); const useKibanaMock = useKibana as jest.Mocked; -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetUrlSearchMock = jest.fn(); const useGetActionTypesMock = useGetActionTypes as jest.Mock; @@ -417,7 +417,7 @@ describe('ConfigureCases', () => { expect(persistCaseConfigure).toHaveBeenCalledWith({ connector: { id: 'resilient-2', - name: 'My Connector 2', + name: 'My Resilient connector', type: ConnectorTypes.resilient, fields: null, }, @@ -440,7 +440,7 @@ describe('ConfigureCases', () => { ...useCaseConfigureResponse, connector: { id: 'resilient-2', - name: 'My connector 2', + name: 'My Resilient connector', type: ConnectorTypes.resilient, fields: null, }, @@ -459,7 +459,7 @@ describe('ConfigureCases', () => { wrapper .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .text() - ).toBe('Update My Connector 2'); + ).toBe('Update My Resilient connector'); }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index ec9b8a8c7a2fb7..2f7db2202a76b1 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -29,7 +29,7 @@ import { HeaderPage } from '../header_page'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesBreadcrumbs } from '../use_breadcrumbs'; import { CasesDeepLinkId } from '../../common/navigation'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; const FormWrapper = styled.div` ${({ theme }) => css` @@ -77,7 +77,7 @@ export const ConfigureCases: React.FC = React.memo(() => { isLoading: isLoadingConnectors, data: connectors = [], refetch: refetchConnectors, - } = useGetConnectors(); + } = useGetSupportedActionConnectors(); const { isLoading: isLoadingActionTypes, data: actionTypes = [], diff --git a/x-pack/plugins/cases/public/components/connectors/card.test.tsx b/x-pack/plugins/cases/public/components/connectors/card.test.tsx index 6254150620fd4d..a654821830b233 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.test.tsx @@ -6,15 +6,16 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { ConnectorTypes } from '../../../common/api'; import { ConnectorCard } from './card'; +import { createQueryWithMarkup } from '../../common/test_utils'; describe('ConnectorCard ', () => { - it('it does not throw when accessing the icon if the connector type is not registered', () => { + it('does not throw when accessing the icon if the connector type is not registered', () => { expect(() => - mount( + render( { ) ).not.toThrowError(); }); + + it('shows the loading spinner if loading', () => { + render( + + ); + + expect(screen.getByTestId('connector-card-loading')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-card')).not.toBeInTheDocument(); + }); + + it('shows the connector title', () => { + render( + + ); + + expect(screen.getByText('My connector')).toBeInTheDocument(); + }); + + it('shows the connector list items', () => { + const listItems = [ + { title: 'item 1 title', description: 'item 1 desc' }, + { title: 'item 2 title', description: 'item 2 desc' }, + ]; + + render( + + ); + + const getByText = createQueryWithMarkup(screen.getByText); + + for (const item of listItems) { + expect(getByText(`${item.title}: ${item.description}`)).toBeInTheDocument(); + } + }); }); diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index c4cd24787b01c6..a9bf98dd7cf9db 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; -import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import styled from 'styled-components'; +import React, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import type { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; @@ -20,12 +19,6 @@ interface ConnectorCardProps { isLoading: boolean; } -const StyledText = styled.span` - span { - display: block; - } -`; - const ConnectorCardDisplay: React.FC = ({ connectorType, title, @@ -34,44 +27,30 @@ const ConnectorCardDisplay: React.FC = ({ }) => { const { triggersActionsUi } = useKibana().services; - const description = useMemo( - () => ( - - {listItems.length > 0 && - listItems.map((item, i) => ( - - {`${item.title}: `} - {item.description} - - ))} - - ), - [listItems] - ); - - const icon = useMemo( - () => , - // eslint-disable-next-line react-hooks/exhaustive-deps - [connectorType] - ); - return ( <> {isLoading && } {!isLoading && ( - - - + + + + + {title} + + + + + + + + {listItems.length > 0 && + listItems.map((item, i) => ( + + {`${item.title}: `} + {`${item.description}`} + + ))} - {icon} )} diff --git a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx index 0e4d92139234bc..751e9dadca491d 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx @@ -122,15 +122,12 @@ describe('Jira Fields', () => { connector={connector} /> ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( - 'Issue type: Task' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( - 'Parent issue: Parent Task' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( - 'Priority: High' - ); + + const nodes = wrapper.find('[data-test-subj="card-list-item"]').hostNodes(); + + expect(nodes.at(0).text()).toEqual('Issue type: Task'); + expect(nodes.at(1).text()).toEqual('Parent issue: Parent Task'); + expect(nodes.at(2).text()).toEqual('Priority: High'); }); test('it sets parent correctly', async () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index 2273343d119b89..95510090cada11 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -71,15 +71,11 @@ describe('ServiceNowITSM Fields', () => { onChoicesSuccess(mockChoices); }); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( - 'Urgency: 2 - High' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( - 'Severity: 1 - Critical' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( - 'Impact: 3 - Moderate' - ); + const nodes = wrapper.find('[data-test-subj="card-list-item"]').hostNodes(); + + expect(nodes.at(0).text()).toEqual('Urgency: 2 - High'); + expect(nodes.at(1).text()).toEqual('Severity: 1 - Critical'); + expect(nodes.at(2).text()).toEqual('Impact: 3 - Moderate'); }); it('transforms the categories to options correctly', async () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx index ee8681c7487dc8..c66d40eb6827b1 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -75,29 +75,17 @@ describe('ServiceNowSIR Fields', () => { act(() => { onChoicesSuccess(mockChoices); }); - wrapper.update(); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( - 'Destination IPs: Yes' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( - 'Source IPs: Yes' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( - 'Malware URLs: Yes' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(3).text()).toEqual( - 'Malware Hashes: Yes' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(4).text()).toEqual( - 'Priority: 1 - Critical' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(5).text()).toEqual( - 'Category: Denial of Service' - ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(6).text()).toEqual( - 'Subcategory: Single or distributed (DoS or DDoS)' - ); + wrapper.update(); + const nodes = wrapper.find('[data-test-subj="card-list-item"]').hostNodes(); + + expect(nodes.at(0).text()).toEqual('Destination IPs: Yes'); + expect(nodes.at(1).text()).toEqual('Source IPs: Yes'); + expect(nodes.at(2).text()).toEqual('Malware URLs: Yes'); + expect(nodes.at(3).text()).toEqual('Malware Hashes: Yes'); + expect(nodes.at(4).text()).toEqual('Priority: 1 - Critical'); + expect(nodes.at(5).text()).toEqual('Category: Denial of Service'); + expect(nodes.at(6).text()).toEqual('Subcategory: Single or distributed (DoS or DDoS)'); }); test('it transforms the categories to options correctly', async () => { 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 d95f2640896437..8aa50a42704144 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -22,11 +22,11 @@ import { CreateCaseForm } from './form'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { TestProviders } from '../../common/mock'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/use_get_tags'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); jest.mock('../app/use_available_owners', () => ({ @@ -34,7 +34,7 @@ jest.mock('../app/use_available_owners', () => ({ })); const useGetTagsMock = useGetTags as jest.Mock; -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const initialCaseValue: FormProps = { diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 37971667da37b1..192de92777fb63 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -41,7 +41,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import userEvent from '@testing-library/user-event'; import { connectorsMock } from '../../common/mock/connectors'; import type { CaseAttachments } from '../../types'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { waitForComponentToUpdate } from '../../common/test_utils'; import { userProfiles } from '../../containers/user_profiles/api.mock'; @@ -51,7 +51,7 @@ jest.mock('../../containers/use_post_case'); jest.mock('../../containers/use_create_attachments'); jest.mock('../../containers/use_post_push_to_service'); jest.mock('../../containers/use_get_tags'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../connectors/resilient/use_get_incident_types'); jest.mock('../connectors/resilient/use_get_severity'); @@ -64,7 +64,7 @@ jest.mock('../../common/lib/kibana'); jest.mock('../../containers/user_profiles/api'); jest.mock('../../common/use_license'); -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const usePostCaseMock = usePostCase as jest.Mock; const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; @@ -407,7 +407,7 @@ describe('Create case', () => { subcategory: null, }, id: 'servicenow-1', - name: 'My Connector', + name: 'My SN connector', type: '.servicenow', }, }); 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 3a1c99f6d6218b..ba859b21d7b0f9 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -22,7 +22,7 @@ import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesFeatures } from '../../common/use_cases_features'; import { getConnectorById } from '../utils'; import type { CaseAttachmentsWithoutOwner } from '../../types'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useCreateCaseWithAttachmentsTransaction } from '../../common/apm/use_cases_transactions'; const initialCaseValue: FormProps = { @@ -55,7 +55,8 @@ export const FormContext: React.FC = ({ attachments, initialValue, }) => { - const { data: connectors = [], isLoading: isLoadingConnectors } = useGetConnectors(); + const { data: connectors = [], isLoading: isLoadingConnectors } = + useGetSupportedActionConnectors(); const { owner, appId } = useCasesContext(); const { isSyncAlertsEnabled } = useCasesFeatures(); const { postCase } = usePostCase(); diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx index 24798c114fede3..07a705ef41a90d 100644 --- a/x-pack/plugins/cases/public/components/create/index.test.tsx +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -30,13 +30,13 @@ import { useGetFieldsByIssueTypeResponse, } from './mock'; import { CreateCase } from '.'; -import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/api'); jest.mock('../../containers/user_profiles/api'); jest.mock('../../containers/use_get_tags'); -jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../connectors/resilient/use_get_incident_types'); jest.mock('../connectors/resilient/use_get_severity'); @@ -45,7 +45,7 @@ jest.mock('../connectors/jira/use_get_fields_by_issue_type'); jest.mock('../connectors/jira/use_get_single_issue'); jest.mock('../connectors/jira/use_get_issues'); -const useGetConnectorsMock = useGetConnectors as jest.Mock; +const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts deleted file mode 100644 index a3c13c1213e640..00000000000000 --- a/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ConnectorUserAction } from '../../../common/api'; -import { Actions, ConnectorTypes } from '../../../common/api'; -import type { CaseUserActions } from '../../containers/types'; -import { getConnectorFieldsFromUserActions } from './helpers'; - -const defaultJiraFields = { - issueType: '1', - parent: null, - priority: null, -}; - -describe('helpers', () => { - describe('getConnectorFieldsFromUserActions', () => { - it('returns null when it cannot find the connector id', () => { - expect(getConnectorFieldsFromUserActions('a', [])).toBeNull(); - }); - - it('returns null when it cannot find the connector id in a non empty array', () => { - expect( - getConnectorFieldsFromUserActions('a', [ - createConnectorUserAction({ - // @ts-expect-error payload missing fields - payload: { a: '1' }, - }), - ]) - ).toBeNull(); - }); - - it('returns the fields when it finds the connector id', () => { - expect(getConnectorFieldsFromUserActions('a', [createConnectorUserAction()])).toEqual( - defaultJiraFields - ); - }); - - it('returns the fields when it finds the connector id in the second user action', () => { - const expectedFields = { ...defaultJiraFields, issueType: '5' }; - - expect( - getConnectorFieldsFromUserActions('id-to-find', [ - createConnectorUserAction({}), - createConnectorUserAction({ - payload: { - connector: { - id: 'id-to-find', - name: 'test', - fields: expectedFields, - type: ConnectorTypes.jira, - }, - }, - }), - ]) - ).toEqual(expectedFields); - }); - - it('returns null when the action is not a connector', () => { - expect( - getConnectorFieldsFromUserActions('id-to-find', [ - createConnectorUserAction({ - // @ts-expect-error - type: 'not-a-connector', - }), - ]) - ).toBeNull(); - }); - }); -}); - -function createConnectorUserAction(attributes: Partial = {}): CaseUserActions { - return { - action: Actions.update, - createdBy: { username: 'user', fullName: null, email: null }, - createdAt: '2021-12-08T11:28:32.623Z', - type: 'connector', - id: '', - commentId: '', - payload: { - connector: { id: 'a', name: 'test', fields: defaultJiraFields, type: ConnectorTypes.jira }, - }, - ...attributes, - } as CaseUserActions; -} diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts deleted file mode 100644 index 84d6984f35bbca..00000000000000 --- a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts +++ /dev/null @@ -1,27 +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 { isConnectorUserAction, isCreateCaseUserAction } from '../../../common/utils/user_actions'; -import type { ConnectorTypeFields } from '../../../common/api'; -import type { CaseUserActions } from '../../containers/types'; - -export const getConnectorFieldsFromUserActions = ( - id: string, - userActions: CaseUserActions[] -): ConnectorTypeFields['fields'] => { - for (const action of [...userActions].reverse()) { - if (isConnectorUserAction(action) || isCreateCaseUserAction(action)) { - const connector = action.payload.connector; - - if (connector && id === connector.id) { - return connector.fields; - } - } - } - - return null; -}; diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index 401f43ae9afff6..fe24145b10d68e 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { mount } from 'enzyme'; import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; @@ -20,47 +19,58 @@ import { noPushCasesPermissions, TestProviders, } from '../../common/mock'; -import { basicCase, basicPush, caseUserActions, connectorsMock } from '../../containers/mock'; +import { basicCase, connectorsMock } from '../../containers/mock'; import type { CaseConnector } from '../../containers/configure/types'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; const onSubmit = jest.fn(); -const caseServices = { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [], - hasDataToPush: true, - }, -}; -const getDefaultProps = (): EditConnectorProps => { - return { - caseData: basicCase, - caseServices, - connectorName: connectorsMock[0].name, - connectors: connectorsMock, - hasDataToPush: true, - isLoading: false, - isValidConnector: true, - onSubmit, - userActions: caseUserActions, - }; +const caseConnectors = getCaseConnectorsMockResponse(); + +const defaultProps: EditConnectorProps = { + caseData: basicCase, + supportedActionConnectors: connectorsMock, + isLoading: false, + caseConnectors, + onSubmit, }; describe('EditConnector ', () => { let appMockRender: AppMockRenderer; + beforeEach(() => { jest.clearAllMocks(); appMockRender = createAppMockRenderer(); }); + it('Renders the none connector', async () => { + render( + + + + ); + + expect( + await screen.findByText( + 'To create and update a case in an external system, select a connector.' + ) + ).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('connector-edit-button')); + + await waitFor(() => { + expect(screen.getAllByTestId('dropdown-connector-no-connector').length).toBeGreaterThan(0); + }); + }); + it('Renders servicenow connector from case initially', async () => { - const defaultProps = getDefaultProps(); const serviceNowProps = { ...defaultProps, caseData: { ...defaultProps.caseData, - connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' }, + connector: { + ...defaultProps.caseData.connector, + id: 'servicenow-1', + }, }, }; @@ -70,54 +80,76 @@ describe('EditConnector ', () => { ); - expect(await screen.findByText('My Connector')).toBeInTheDocument(); + expect(await screen.findByText('My SN connector')).toBeInTheDocument(); }); it('Renders no connector, and then edit', async () => { - const defaultProps = getDefaultProps(); - const wrapper = mount( + render( ); - expect(wrapper.find(`[data-test-subj="has-data-to-push-button"]`).exists()).toBeTruthy(); - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); - expect( - wrapper.find(`span[data-test-subj="dropdown-connector-no-connector"]`).last().exists() - ).toBeTruthy(); + userEvent.click(screen.getByTestId('connector-edit-button')); + + await waitFor(() => { + expect(screen.getByTestId('caseConnectors')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByTestId('dropdown-connectors')); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - await waitFor(() => wrapper.update()); + await waitFor(() => { + expect(screen.getByTestId('dropdown-connector-resilient-2')).toBeInTheDocument(); + }); - expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); + userEvent.click(screen.getByTestId('dropdown-connector-resilient-2')); + + await waitFor(() => { + expect(screen.getByTestId('edit-connectors-submit')).toBeInTheDocument(); + }); }); it('Edit external service on submit', async () => { - const defaultProps = getDefaultProps(); - const wrapper = mount( + render( ); - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - wrapper.update(); + userEvent.click(screen.getByTestId('connector-edit-button')); + userEvent.click(screen.getByTestId('dropdown-connectors')); - expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); + await waitFor(() => { + expect(screen.getByTestId('dropdown-connector-resilient-2')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByTestId('dropdown-connector-resilient-2'), undefined, { + skipPointerEventsCheck: true, + }); - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe('resilient-2')); + expect(screen.getByTestId('edit-connectors-submit')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('edit-connectors-submit')); + + await waitFor(() => + expect(onSubmit).toHaveBeenCalledWith( + { + fields: { + incidentTypes: null, + severityCode: null, + }, + id: 'resilient-2', + name: 'My Resilient connector', + type: '.resilient', + }, + expect.anything(), + expect.anything() + ) + ); }); it('Revert to initial external service on error', async () => { - const defaultProps = getDefaultProps(); - onSubmit.mockImplementation((connector, onSuccess, onError) => { + onSubmit.mockImplementation((connector, onError, onSuccess) => { onError(new Error('An error has occurred')); }); @@ -125,43 +157,46 @@ describe('EditConnector ', () => { ...defaultProps, caseData: { ...defaultProps.caseData, - connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' }, + connector: { + ...defaultProps.caseData.connector, + id: 'servicenow-1', + }, }, }; - const wrapper = mount( + render( ); - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + userEvent.click(screen.getByTestId('connector-edit-button')); + userEvent.click(screen.getByTestId('dropdown-connectors')); + + await waitFor(() => { + expect(screen.getByTestId('dropdown-connector-resilient-2')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByTestId('dropdown-connector-resilient-2'), undefined, { + skipPointerEventsCheck: true, + }); + + userEvent.click(screen.getByTestId('edit-connectors-submit')); + await waitFor(() => { - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - wrapper.update(); - expect( - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists() - ).toBeTruthy(); - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + expect(screen.queryByTestId('edit-connectors-submit')).not.toBeInTheDocument(); }); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).exists()).toBeFalsy(); + expect(onSubmit).toHaveBeenCalled(); }); - /** - * If an error is being throw on submit the selected connector should - * be reverted to the initial one. In our test the initial one is the .servicenow-1 - * connector. The title of the .servicenow-1 connector is My Connector. - */ - expect(wrapper.text().includes('My Connector')).toBeTruthy(); + await waitFor(() => { + expect(screen.getByText('My SN connector')).toBeInTheDocument(); + }); }); it('Resets selector on cancel', async () => { - const defaultProps = getDefaultProps(); const props = { ...defaultProps, caseData: { @@ -173,57 +208,76 @@ describe('EditConnector ', () => { }, }; - const wrapper = mount( + render( ); - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - wrapper.update(); - wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click'); + userEvent.click(screen.getByTestId('connector-edit-button')); + userEvent.click(screen.getByTestId('dropdown-connectors')); await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).exists()).toBeFalsy(); + expect(screen.getByTestId('dropdown-connector-resilient-2')).toBeInTheDocument(); }); - expect(wrapper.text().includes('My Connector')).toBeTruthy(); + userEvent.click(screen.getByTestId('dropdown-connector-resilient-2')); + userEvent.click(screen.getByTestId('edit-connectors-cancel')); + + await waitFor(() => { + expect(screen.queryByTestId('edit-connectors-submit')).not.toBeInTheDocument(); + }); + + expect(screen.getByText('My SN connector')).toBeInTheDocument(); }); - it('Renders loading spinner', async () => { - const defaultProps = getDefaultProps(); + it('disabled the edit button when is loading', async () => { const props = { ...defaultProps, isLoading: true }; - const wrapper = mount( + + render( ); - await waitFor(() => - expect(wrapper.find(`[data-test-subj="connector-loading"]`).last().exists()).toBeTruthy() + + await waitFor(() => { + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); + }); + }); + + it('does not shows the callouts when is loading', async () => { + const props = { ...defaultProps, isLoading: true }; + + render( + + + ); + + await waitFor(() => { + expect(screen.queryByTestId('push-callouts')).not.toBeInTheDocument(); + }); }); it('does not allow the connector to be edited when the user does not have write permissions', async () => { - const wrapper = mount( + render( - + ); - await waitFor(() => - expect(wrapper.find(`[data-test-subj="connector-edit"]`).exists()).toBeFalsy() - ); - expect(wrapper.find(`[data-test-subj="has-data-to-push-button"]`).exists()).toBeFalsy(); + await waitFor(() => { + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.queryByTestId('push-to-external-service')).not.toBeInTheDocument(); + }); }); it('display the callout message when none is selected', async () => { - const defaultProps = getDefaultProps(); - const props = { ...defaultProps, connectors: [] }; - const result = appMockRender.render(); + // default props has the none connector as selected + const result = appMockRender.render(); await waitFor(() => { expect(result.getByTestId('push-callouts')).toBeInTheDocument(); @@ -231,7 +285,6 @@ describe('EditConnector ', () => { }); it('disables the save button until changes are done ', async () => { - const defaultProps = getDefaultProps(); const serviceNowProps = { ...defaultProps, caseData: { @@ -261,18 +314,17 @@ describe('EditConnector ', () => { // simulate changing the connector userEvent.click(result.getByTestId('dropdown-connectors')); + await waitForEuiPopoverOpen(); + userEvent.click(result.getAllByTestId('dropdown-connector-no-connector')[0]); - expect(result.getByTestId('edit-connectors-submit')).toBeEnabled(); - // this strange assertion is required because of existing race conditions inside the EditConnector component await waitFor(() => { - expect(true).toBeTruthy(); + expect(result.getByTestId('edit-connectors-submit')).toBeEnabled(); }); }); it('disables the save button when no connector is the default', async () => { - const defaultProps = getDefaultProps(); const noneConnector = { ...defaultProps, caseData: { @@ -295,18 +347,17 @@ describe('EditConnector ', () => { // simulate changing the connector userEvent.click(result.getByTestId('dropdown-connectors')); + await waitForEuiPopoverOpen(); + userEvent.click(result.getAllByTestId('dropdown-connector-resilient-2')[0]); - expect(result.getByTestId('edit-connectors-submit')).toBeEnabled(); - // this strange assertion is required because of existing race conditions inside the EditConnector component await waitFor(() => { - expect(true).toBeTruthy(); + expect(result.getByTestId('edit-connectors-submit')).toBeEnabled(); }); }); it('shows the actions permission message if the user does not have read access to actions', async () => { - const defaultProps = getDefaultProps(); appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: false, show: false }, @@ -319,7 +370,6 @@ describe('EditConnector ', () => { }); it('does not show the actions permission message if the user has read access to actions', async () => { - const defaultProps = getDefaultProps(); appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: true, show: true }, @@ -332,7 +382,6 @@ describe('EditConnector ', () => { }); it('does not show the callout if the user does not have read access to actions', async () => { - const defaultProps = getDefaultProps(); const props = { ...defaultProps, connectors: [] }; appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, @@ -347,7 +396,6 @@ describe('EditConnector ', () => { }); it('does not show the push button if the user does not have read access to actions', async () => { - const defaultProps = getDefaultProps(); appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: false, show: false }, @@ -355,44 +403,43 @@ describe('EditConnector ', () => { const result = appMockRender.render(); await waitFor(() => { - expect(result.queryByTestId('has-data-to-push-button')).toBe(null); + expect(result.queryByTestId('push-to-external-service')).toBe(null); }); }); it('does not show the push button if the user does not have push permissions', async () => { - const defaultProps = getDefaultProps(); - appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() }); const result = appMockRender.render(); + await waitFor(() => { - expect(result.queryByTestId('has-data-to-push-button')).toBe(null); + expect(result.queryByTestId('push-to-external-service')).toBe(null); }); }); it('does not show the edit connectors pencil if the user does not have read access to actions', async () => { - const defaultProps = getDefaultProps(); const props = { ...defaultProps, connectors: [] }; appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, actions: { save: false, show: false }, }; - const result = appMockRender.render(); + appMockRender.render(); + await waitFor(() => { - expect(result.getByTestId('connector-edit-header')).toBeInTheDocument(); - expect(result.queryByTestId('connector-edit')).toBe(null); + expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); }); }); it('does not show the edit connectors pencil if the user does not have push permissions', async () => { - const defaultProps = getDefaultProps(); const props = { ...defaultProps, connectors: [] }; appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() }); - const result = appMockRender.render(); + appMockRender.render(); + await waitFor(() => { - expect(result.getByTestId('connector-edit-header')).toBeInTheDocument(); - expect(result.queryByTestId('connector-edit')).toBe(null); + expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-edit-button')).toBe(null); }); }); }); 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 25fc2a69ac4dcf..8a63c9d02ad266 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -4,7 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; + +/* eslint-disable complexity */ + +import React, { useCallback, useReducer } from 'react'; import deepEqual from 'fast-deep-equal'; import { EuiText, @@ -13,64 +16,34 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, - EuiLoadingSpinner, EuiButtonIcon, } from '@elastic/eui'; -import styled from 'styled-components'; import { isEmpty, noop } from 'lodash/fp'; 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 type { Case, CaseConnectors } from '../../../common/ui/types'; +import type { ActionConnector, CaseConnector, ConnectorTypeFields } from '../../../common/api'; import { NONE_CONNECTOR_ID } from '../../../common/api'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; -import type { CaseUserActions } from '../../containers/types'; import { schema } from './schema'; -import { getConnectorFieldsFromUserActions } from './helpers'; import * as i18n from './translations'; import { getConnectorById, getConnectorsFormValidators } from '../utils'; import { usePushToService } from '../use_push_to_service'; -import type { CaseServices } from '../../containers/use_find_case_user_actions'; import { useApplicationCapabilities } from '../../common/lib/kibana'; -import { useCasesContext } from '../cases_context/use_cases_context'; +import { PushButton } from './push_button'; +import { PushCallouts } from './push_callouts'; +import { normalizeActionConnector, getNoneConnector } from '../configure_cases/utils'; export interface EditConnectorProps { caseData: Case; - caseServices: CaseServices; - connectorName: string; - connectors: ActionConnector[]; - hasDataToPush: boolean; + caseConnectors: CaseConnectors; + supportedActionConnectors: ActionConnector[]; isLoading: boolean; - isValidConnector: boolean; - onSubmit: ( - connectorId: string, - connectorFields: ConnectorTypeFields['fields'], - onError: () => void, - onSuccess: () => void - ) => void; - userActions: CaseUserActions[]; + onSubmit: (connector: CaseConnector, onError: () => void, onSuccess: () => void) => void; } -const MyFlexGroup = styled(EuiFlexGroup)` - ${({ theme }) => ` - p { - font-size: ${theme.eui.euiSizeM}; - } - `} -`; -const DisappearingFlexItem = styled(EuiFlexItem)` - ${({ $isHidden }: { $isHidden: boolean }) => - $isHidden && - ` - margin: 0 !important; - & .euiFlexItem { - margin: 0 !important; - } - `} -`; - interface State { currentConnector: ActionConnector | null; fields: ConnectorTypeFields['fields']; @@ -81,6 +54,7 @@ type Action = | { type: 'SET_CURRENT_CONNECTOR'; payload: State['currentConnector'] } | { type: 'SET_FIELDS'; payload: State['fields'] } | { type: 'SET_EDIT_CONNECTOR'; payload: State['editConnector'] }; + const editConnectorReducer = (state: State, action: Action) => { switch (action.type) { case 'SET_CURRENT_CONNECTOR': @@ -112,18 +86,15 @@ const initialState = { export const EditConnector = React.memo( ({ caseData, - caseServices, - connectorName, - connectors, - hasDataToPush, + caseConnectors, + supportedActionConnectors, isLoading, - isValidConnector, onSubmit, - userActions, }: EditConnectorProps) => { - const { permissions } = useCasesContext(); const caseFields = caseData.connector.fields; const selectedConnector = caseData.connector.id; + const actionConnector = getConnectorById(caseData.connector.id, supportedActionConnectors); + const isValidConnector = !!actionConnector; const { form } = useForm({ defaultValue: { connectorId: selectedConnector }, @@ -133,79 +104,46 @@ export const EditConnector = React.memo( const { actions } = useApplicationCapabilities(); const actionsReadCapabilities = actions.read; - // by default save if disabled - const [enableSave, setEnableSave] = useState(false); - const { setFieldValue, submit } = form; const [{ currentConnector, fields, editConnector }, dispatch] = useReducer( editConnectorReducer, - { ...initialState, fields: caseFields } - ); - - // only enable the save button if changes were made to the previous selected - // connector or its fields - useEffect(() => { - // null and none are equivalent to `no connector`. - // This makes sure we don't enable the button when the "no connector" option is selected - // by default. e.g. when a case is created without a selector - const isNoConnectorDeafultValue = - currentConnector === null && selectedConnector === NONE_CONNECTOR_ID; - const enable = - (!isNoConnectorDeafultValue && currentConnector?.id !== selectedConnector) || - !deepEqual(fields, caseFields); - - setEnableSave(enable); - }, [caseFields, currentConnector, fields, selectedConnector]); - - useEffect(() => { - // Initialize the current connector with the connector information attached to the case if we can find that - // connector in the retrieved connectors from the API call - if (!isLoading) { - dispatch({ - type: 'SET_CURRENT_CONNECTOR', - payload: getConnectorById(caseData.connector.id, connectors), - }); - - // Set the fields initially to whatever is present in the case, this should match with - // the latest user action for an update connector as well - dispatch({ - type: 'SET_FIELDS', - payload: caseFields, - }); + { + ...initialState, + fields: caseFields, + currentConnector: actionConnector, } - }, [caseData.connector.id, connectors, isLoading, caseFields]); + ); /** - * There is a race condition with this callback. At some point during the initial mounting of this component, this - * callback will be called. There are a couple problems with this: - * - * 1. If the call occurs before the above useEffect does its dispatches (aka while the connectors are still loading) this will - * result in setting the current connector to null when in fact we might have a valid connector. It could also - * cause issues when setting the fields because if there are no user actions then the getConnectorFieldsFromUserActions - * will return null even when the caseData.connector.fields is valid and populated. - * - * 2. If the call occurs after the above useEffect then the currentConnector should === newConnectorId - * - * As far as I know dispatch is synchronous so if the useEffect runs first it should successfully set currentConnector. If - * onChangeConnector runs first and sets stuff to null, then when useEffect runs it'll switch everything back to what we need it to be - * initially. + * only enable the save button if changes were made to the previous selected + * connector or its fields + * null and none are equivalent to `no connector`. + * This makes sure we don't enable the button when the "no connector" option is selected + * by default. e.g. when a case is created without a connector */ + const isDefaultNoneConnectorSelected = + currentConnector === null && selectedConnector === NONE_CONNECTOR_ID; + + const enableSave = + (!isDefaultNoneConnectorSelected && currentConnector?.id !== selectedConnector) || + !deepEqual(fields, caseFields); + const onChangeConnector = useCallback( (newConnectorId) => { // change connector on dropdown action if (currentConnector?.id !== newConnectorId) { dispatch({ type: 'SET_CURRENT_CONNECTOR', - payload: getConnectorById(newConnectorId, connectors), + payload: getConnectorById(newConnectorId, supportedActionConnectors), }); dispatch({ type: 'SET_FIELDS', - payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []), + payload: caseConnectors[newConnectorId]?.fields ?? null, }); } }, - [currentConnector, userActions, connectors] + [currentConnector, caseConnectors, supportedActionConnectors] ); const onFieldsChange = useCallback( @@ -220,36 +158,51 @@ export const EditConnector = React.memo( [fields, dispatch] ); - const onError = useCallback(() => { + const resetConnector = useCallback(() => { setFieldValue('connectorId', selectedConnector); + dispatch({ - type: 'SET_EDIT_CONNECTOR', - payload: false, + type: 'SET_CURRENT_CONNECTOR', + payload: actionConnector, }); - }, [dispatch, setFieldValue, selectedConnector]); - const onCancelConnector = useCallback(() => { - setFieldValue('connectorId', selectedConnector); dispatch({ type: 'SET_FIELDS', payload: caseFields, }); + dispatch({ type: 'SET_EDIT_CONNECTOR', payload: false, }); - }, [dispatch, selectedConnector, setFieldValue, caseFields]); + }, [actionConnector, caseFields, selectedConnector, setFieldValue]); + + const onError = useCallback(() => { + resetConnector(); + }, [resetConnector]); + + const onCancelConnector = useCallback(() => { + resetConnector(); + }, [resetConnector]); const onSubmitConnector = useCallback(async () => { const { isValid, data: newData } = await submit(); + if (isValid && newData.connectorId) { - onSubmit(newData.connectorId, fields, onError, noop); + const connector = getConnectorById(newData.connectorId, supportedActionConnectors); + const connectorToUpdate = connector + ? normalizeActionConnector(connector) + : getNoneConnector(); + + const connectorWithFields = { ...connectorToUpdate, fields } as CaseConnector; + onSubmit(connectorWithFields, onError, noop); + dispatch({ type: 'SET_EDIT_CONNECTOR', payload: false, }); } - }, [dispatch, submit, fields, onSubmit, onError]); + }, [submit, supportedActionConnectors, fields, onSubmit, onError]); const onEditClick = useCallback(() => { dispatch({ @@ -260,27 +213,42 @@ export const EditConnector = React.memo( const connectorIdConfig = getConnectorsFormValidators({ config: schema.connectorId as FieldConfig, - connectors, + connectors: supportedActionConnectors, }); - const { pushButton, pushCallouts } = usePushToService({ - connector: { - ...caseData.connector, - name: isEmpty(connectorName) ? caseData.connector.name : connectorName, - }, - caseServices, + const connectorWithName = { + ...caseData.connector, + name: isEmpty(actionConnector?.name) ? caseData.connector.name : actionConnector?.name ?? '', + }; + + const { + errorsMsg, + needsToBePushed, + hasBeenPushed, + isLoading: isLoadingPushToService, + hasPushPermissions, + hasErrorMessages, + hasLicenseError, + handlePushToService, + } = usePushToService({ + connector: connectorWithName, + caseConnectors, caseId: caseData.id, caseStatus: caseData.status, - connectors, - hasDataToPush, - onEditClick, isValidConnector, }); + const disablePushButton = + isLoadingPushToService || + errorsMsg.length > 0 || + !hasPushPermissions || + !isValidConnector || + !needsToBePushed; + return ( -

{i18n.CONNECTORS}

- {isLoading && } - {!isLoading && !editConnector && permissions.push && actionsReadCapabilities && ( + {!isLoading && !editConnector && hasPushPermissions && actionsReadCapabilities ? ( - )} - + ) : null} +
- - {!isLoading && !editConnector && pushCallouts && actionsReadCapabilities && ( - {pushCallouts} + + {!isLoading && !editConnector && hasErrorMessages && actionsReadCapabilities && ( + + 0} + onEditClick={onEditClick} + /> + )} - -
+ + - -
+ + {!editConnector && !actionsReadCapabilities && ( @@ -372,16 +346,26 @@ export const EditConnector = React.memo(
)} - {pushCallouts == null && + {!hasErrorMessages && !isLoading && !editConnector && - permissions.push && + hasPushPermissions && actionsReadCapabilities && ( - - {pushButton} + + + 0 || !needsToBePushed || !hasPushPermissions} + connectorName={connectorWithName.name} + /> + )} -
+ ); diff --git a/x-pack/plugins/cases/public/components/edit_connector/push_button.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/push_button.test.tsx new file mode 100644 index 00000000000000..fee6fdc8d15572 --- /dev/null +++ b/x-pack/plugins/cases/public/components/edit_connector/push_button.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 React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { PushButton } from './push_button'; + +const pushToService = jest.fn(); + +const defaultProps = { + disabled: false, + isLoading: false, + errorsMsg: [], + hasBeenPushed: false, + showTooltip: false, + connectorName: 'My SN connector', + pushToService, +}; + +describe('PushButton ', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders the button without tooltip', async () => { + appMockRender.render(); + + expect(screen.getByTestId('push-to-external-service')).toBeInTheDocument(); + expect(screen.queryByTestId('push-button-tooltip')).not.toBeInTheDocument(); + }); + + it('renders the correct label when the connector has not been pushed', async () => { + appMockRender.render(); + + expect(screen.getByText('Push as My SN connector incident')).toBeInTheDocument(); + }); + + it('renders the correct label when the connector has been pushed', async () => { + appMockRender.render(); + + expect(screen.getByText('Update My SN connector incident')).toBeInTheDocument(); + }); + + it('pushed correctly', async () => { + appMockRender.render(); + + userEvent.click(screen.getByTestId('push-to-external-service')); + expect(pushToService).toHaveBeenCalled(); + }); + + it('disables the button', async () => { + appMockRender.render(); + + expect(screen.getByTestId('push-to-external-service')).toBeDisabled(); + }); + + it('shows the tooltip context correctly', async () => { + appMockRender.render(); + + userEvent.hover(screen.getByTestId('push-to-external-service')); + + expect(await screen.findByText('My SN connector incident is up to date')).toBeInTheDocument(); + expect(await screen.findByText('No update is required')).toBeInTheDocument(); + }); + + it('shows the tooltip context correctly with custom message', async () => { + appMockRender.render( + + ); + + userEvent.hover(screen.getByTestId('push-to-external-service')); + + expect(await screen.findByText('My title')).toBeInTheDocument(); + expect(await screen.findByText('My desc')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/edit_connector/push_button.tsx b/x-pack/plugins/cases/public/components/edit_connector/push_button.tsx new file mode 100644 index 00000000000000..d004d69da85cf3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/edit_connector/push_button.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import type { ErrorMessage } from '../use_push_to_service/callout/types'; +import * as i18n from './translations'; + +interface PushButtonProps { + isLoading: boolean; + disabled: boolean; + errorsMsg: ErrorMessage[]; + hasBeenPushed: boolean; + showTooltip: boolean; + connectorName: string; + pushToService: () => Promise; +} + +const PushButtonComponent: React.FC = ({ + disabled, + errorsMsg, + isLoading, + hasBeenPushed, + connectorName, + showTooltip, + pushToService, +}) => { + const button = ( + + {hasBeenPushed ? i18n.UPDATE_INCIDENT(connectorName) : i18n.PUSH_INCIDENT(connectorName)} + + ); + + return showTooltip ? ( + 0 ? errorsMsg[0].title : i18n.PUSH_LOCKED_TITLE(connectorName)} + content={

{errorsMsg.length > 0 ? errorsMsg[0].description : i18n.PUSH_LOCKED_DESC}

} + data-test-subj="push-button-tooltip" + > + {button} +
+ ) : ( + <>{button} + ); +}; + +PushButtonComponent.displayName = 'PushButton'; + +export const PushButton = React.memo(PushButtonComponent); diff --git a/x-pack/plugins/cases/public/components/edit_connector/push_callouts.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/push_callouts.test.tsx new file mode 100644 index 00000000000000..46e2d56fbce9a6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/edit_connector/push_callouts.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { screen } from '@testing-library/react'; + +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { PushCallouts } from './push_callouts'; + +const onEditClick = jest.fn(); + +const defaultProps = { + hasConnectors: false, + hasLicenseError: false, + errorsMsg: [{ id: 'test-id', title: 'My title', description: 'My desc' }], + onEditClick, +}; + +describe('PushCallouts ', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('renders', async () => { + appMockRender.render(); + + expect(await screen.findByText('My title')).toBeInTheDocument(); + expect(await screen.findByText('My desc')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/edit_connector/push_callouts.tsx b/x-pack/plugins/cases/public/components/edit_connector/push_callouts.tsx new file mode 100644 index 00000000000000..65ea0f20a7e827 --- /dev/null +++ b/x-pack/plugins/cases/public/components/edit_connector/push_callouts.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 { CaseCallOut } from '../use_push_to_service/callout'; +import type { ErrorMessage } from '../use_push_to_service/callout/types'; + +interface PushCalloutsProps { + hasConnectors: boolean; + hasLicenseError: boolean; + errorsMsg: ErrorMessage[]; + onEditClick: () => void; +} + +const PushCalloutsComponent: React.FC = ({ + hasConnectors, + hasLicenseError, + errorsMsg, + onEditClick, +}) => { + return ( + + ); +}; + +PushCalloutsComponent.displayName = 'PushCalloutsComponent'; + +export const PushCallouts = React.memo(PushCalloutsComponent); diff --git a/x-pack/plugins/cases/public/components/edit_connector/translations.ts b/x-pack/plugins/cases/public/components/edit_connector/translations.ts index ab69c943217035..532e0bb70e093a 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/translations.ts +++ b/x-pack/plugins/cases/public/components/edit_connector/translations.ts @@ -8,6 +8,12 @@ import { i18n } from '@kbn/i18n'; export * from '../../common/translations'; +export { + UPDATE_INCIDENT, + PUSH_INCIDENT, + PUSH_LOCKED_TITLE, + PUSH_LOCKED_DESC, +} from '../use_push_to_service/translations'; export const EDIT_CONNECTOR_ARIA = i18n.translate( 'xpack.cases.editConnector.editConnectorLinkAria', diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx index 08192a1efc68fc..07f4ab2768c0bb 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/index.tsx @@ -67,25 +67,26 @@ const CaseCallOutComponent = ({ [messages] ); + const groupedByTypeErrorMessagesKeys = Object.keys(groupedByTypeErrorMessages) as Array< + keyof ErrorMessage['errorType'] + >; return ( <> - {(Object.keys(groupedByTypeErrorMessages) as Array).map( - (type: NonNullable) => { - const id = createCalloutId(groupedByTypeErrorMessages[type].messagesId); - return ( - - - - - ); - } - )} + {groupedByTypeErrorMessagesKeys.map((type: NonNullable, index) => { + const id = createCalloutId(groupedByTypeErrorMessages[type].messagesId); + return ( + + + {index !== groupedByTypeErrorMessagesKeys.length - 1 ? : null} + + ); + })} ); }; diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/helpers.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/helpers.tsx index 0a58678da6d0e3..f2f19a91d11e5a 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/helpers.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/helpers.tsx @@ -42,12 +42,15 @@ export const getKibanaConfigError = () => ({ title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, description: ( - {'coming soon...'} + + {i18n.LINK_ACTIONS_CONFIGURATION} ), }} diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx index 2cd381e7035b2d..52598a80f000e7 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx @@ -7,18 +7,19 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; -import { render, screen } from '@testing-library/react'; import '../../common/mock/match_media'; import type { ReturnUsePushToService, UsePushToService } from '.'; import { usePushToService } from '.'; import { noPushCasesPermissions, readCasesPermissions, TestProviders } from '../../common/mock'; +import type { CaseConnector } from '../../../common/api'; import { CaseStatuses, ConnectorTypes } from '../../../common/api'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; -import { basicPush, actionLicenses, connectorsMock } from '../../containers/mock'; +import { actionLicenses } from '../../containers/mock'; import { CLOSED_CASE_PUSH_ERROR_ID } from './callout/types'; -import * as i18n from './translations'; import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; +import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; jest.mock('../../containers/use_get_action_license', () => { return { @@ -28,80 +29,60 @@ jest.mock('../../containers/use_get_action_license', () => { jest.mock('../../containers/use_post_push_to_service'); jest.mock('../../containers/configure/api'); jest.mock('../../common/navigation/hooks'); +jest.mock('../case_view/use_on_refresh_case_view_page'); const useFetchActionLicenseMock = useGetActionLicense as jest.Mock; +const usePostPushToServiceMock = usePostPushToService as jest.Mock; describe('usePushToService', () => { const caseId = '12345'; - const onEditClick = jest.fn(); - const pushCaseToExternalService = jest.fn(); + const pushCaseToExternalService = jest.fn().mockReturnValue({}); const mockPostPush = { isLoading: false, pushCaseToExternalService, }; - const mockConnector = connectorsMock[0]; + const caseConnectors = getCaseConnectorsMockResponse(); + const mockConnector = caseConnectors['jira-1']; const actionLicense = actionLicenses[0]; - const caseServices = { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [], - hasDataToPush: true, - }, - }; const defaultArgs = { - actionsErrors: [], + caseId, + caseStatus: CaseStatuses.open, connector: { id: mockConnector.id, name: mockConnector.name, - type: ConnectorTypes.serviceNowITSM, - fields: null, - }, - caseId, - caseServices, - caseStatus: CaseStatuses.open, - configureCasesNavigation: { - href: 'href', - onClick: jest.fn(), - }, - connectors: connectorsMock, - hasDataToPush: true, - onEditClick, + type: mockConnector.type, + fields: mockConnector.fields, + } as CaseConnector, + caseConnectors, isValidConnector: true, }; beforeEach(() => { jest.clearAllMocks(); - (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); - useFetchActionLicenseMock.mockImplementation(() => ({ + usePostPushToServiceMock.mockReturnValue(mockPostPush); + useFetchActionLicenseMock.mockReturnValue({ isLoading: false, data: actionLicense, - })); + }); }); - it('push case button posts the push with correct args', async () => { + it('calls pushCaseToExternalService with correct arguments', async () => { + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - result.current.pushButton.props.children.props.onClick(); - expect(pushCaseToExternalService).toBeCalledWith({ - caseId, - connector: { - fields: null, - id: 'servicenow-1', - name: 'My Connector', - type: ConnectorTypes.serviceNowITSM, - }, - }); - expect(result.current.pushCallouts).toBeNull(); + await result.current.handlePushToService(); + }); + + expect(pushCaseToExternalService).toBeCalledWith({ + caseId, + connector: defaultArgs.connector, }); }); @@ -113,18 +94,18 @@ describe('usePushToService', () => { enabledInLicense: false, }, })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].id).toEqual('license-error'); - }); + + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + + const errorsMsg = result.current.errorsMsg; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].id).toEqual('license-error'); + expect(result.current.hasErrorMessages).toBe(true); }); it('Displays message when user does not have case enabled in config', async () => { @@ -136,28 +117,188 @@ describe('usePushToService', () => { }, })); + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + + const errorsMsg = result.current.errorsMsg; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].id).toEqual('kibana-config-error'); + expect(result.current.hasErrorMessages).toBe(true); + }); + + it('Displays message when user has select none as connector', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + const errorsMsg = result.current.errorsMsg; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].id).toEqual('connector-missing-error'); + expect(result.current.hasErrorMessages).toBe(true); + }); + + it('Displays message when connector is deleted', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connector: { + id: 'not-exist', + name: 'not-exist', + type: ConnectorTypes.none, + fields: null, + }, + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + const errorsMsg = result.current.errorsMsg; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].id).toEqual('connector-deleted-error'); + expect(result.current.hasErrorMessages).toBe(true); + }); + + it('Displays message when case is closed', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + caseStatus: CaseStatuses.closed, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + const errorsMsg = result.current.errorsMsg; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].id).toEqual(CLOSED_CASE_PUSH_ERROR_ID); + expect(result.current.hasErrorMessages).toBe(true); + }); + + it('should not call pushCaseToExternalService when the selected connector is none', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await act(async () => { - const { result, waitForNextUpdate } = renderHook( + await result.current.handlePushToService(); + }); + + expect(pushCaseToExternalService).not.toBeCalled(); + }); + + it('refresh case view page after push', async () => { + const { result, waitFor } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + + await act(async () => { + await result.current.handlePushToService(); + }); + + await waitFor(() => { + expect(useRefreshCaseViewPage()).toHaveBeenCalled(); + }); + }); + + describe('user does not have write or push permissions', () => { + it('returns correct information about push permissions', async () => { + const { result } = renderHook( () => usePushToService(defaultArgs), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => ( + {children} + ), } ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].id).toEqual('kibana-config-error'); + expect(result.current.hasPushPermissions).toBe(false); }); - }); - it('Displays message when user does not have any connector configured', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( + it('does not display a message when user does not have a premium license', async () => { + useFetchActionLicenseMock.mockImplementation(() => ({ + isLoading: false, + data: { + ...actionLicense, + enabledInLicense: false, + }, + })); + + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(result.current.errorsMsg).toEqual([]); + expect(result.current.hasErrorMessages).toBe(false); + }); + + it('does not display a message when user does not have case enabled in config', async () => { + useFetchActionLicenseMock.mockImplementation(() => ({ + isLoading: false, + data: { + ...actionLicense, + enabledInConfig: false, + }, + })); + + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(result.current.errorsMsg).toEqual([]); + expect(result.current.hasErrorMessages).toBe(false); + }); + + it('does not display a message when user does not have any connector configured', async () => { + const { result } = renderHook( () => usePushToService({ ...defaultArgs, - connectors: [], connector: { id: 'none', name: 'none', @@ -166,24 +307,18 @@ describe('usePushToService', () => { }, }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => ( + {children} + ), } ); - await waitForNextUpdate(); - - render(result.current.pushCallouts ?? <>); - // getByText will thrown an error if the element is not found. - screen.getByText(i18n.CONFIGURE_CONNECTOR); - - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); + expect(result.current.errorsMsg).toEqual([]); + expect(result.current.hasErrorMessages).toBe(false); }); - }); - it('Displays message when user does have a connector but is configured to none', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( + it('does not display a message when user does have a connector but is configured to none', async () => { + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -195,24 +330,18 @@ describe('usePushToService', () => { }, }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => ( + {children} + ), } ); - await waitForNextUpdate(); - - render(result.current.pushCallouts ?? <>); - // getByText will thrown an error if the element is not found. - screen.getByText(i18n.CONFIGURE_CONNECTOR); - - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); + expect(result.current.errorsMsg).toEqual([]); + expect(result.current.hasErrorMessages).toBe(false); }); - }); - it('Displays message when connector is deleted', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( + it('does not display a message when connector is deleted', async () => { + const { result } = renderHook( () => usePushToService({ ...defaultArgs, @@ -225,23 +354,125 @@ describe('usePushToService', () => { isValidConnector: false, }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => ( + {children} + ), } ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].id).toEqual('connector-deleted-error'); + + expect(result.current.errorsMsg).toEqual([]); + expect(result.current.hasErrorMessages).toBe(false); + }); + + it('does not display a message when case is closed', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + caseStatus: CaseStatuses.closed, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(result.current.errorsMsg).toEqual([]); + expect(result.current.hasErrorMessages).toBe(false); }); }); - it('Displays message when connector is deleted with empty connectors', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( + describe('returned values', () => { + it('initial', async () => { + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + + const { handlePushToService, errorsMsg, ...rest } = result.current; + + expect(rest).toEqual({ + hasBeenPushed: true, + hasErrorMessages: false, + hasLicenseError: false, + hasPushPermissions: true, + isLoading: false, + needsToBePushed: false, + }); + }); + + it('isLoading is true when usePostPushToService is loading', async () => { + usePostPushToServiceMock.mockReturnValue({ ...mockPostPush, isLoading: true }); + + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.isLoading).toBe(true); + }); + + it('isLoading is true when loading license', async () => { + useFetchActionLicenseMock.mockReturnValue({ + isLoading: true, + data: actionLicense, + }); + + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.isLoading).toBe(true); + }); + + it('hasErrorMessages=true if there are error messages', async () => { + const { result } = renderHook( + () => usePushToService({ ...defaultArgs, caseStatus: CaseStatuses.closed }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.hasErrorMessages).toBe(true); + }); + + it('needsToBePushed=true if the connector needs to be pushed', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + caseConnectors: { + ...caseConnectors, + [mockConnector.id]: { + ...caseConnectors[mockConnector.id], + push: { + ...caseConnectors[mockConnector.id].push, + needsToBePushed: true, + }, + }, + }, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.needsToBePushed).toBe(true); + }); + + it('needsToBePushed=false if the connector does not exist', async () => { + const { result } = renderHook( () => usePushToService({ ...defaultArgs, - connectors: [], connector: { id: 'not-exist', name: 'not-exist', @@ -254,212 +485,106 @@ describe('usePushToService', () => { wrapper: ({ children }) => {children}, } ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].id).toEqual('connector-deleted-error'); + + expect(result.current.needsToBePushed).toBe(false); }); - }); - it('Displays message when case is closed', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( + it('hasBeenPushed=false if the connector has been pushed', async () => { + const { result } = renderHook( () => usePushToService({ ...defaultArgs, - caseStatus: CaseStatuses.closed, + caseConnectors: { + ...caseConnectors, + [mockConnector.id]: { + ...caseConnectors[mockConnector.id], + push: { + ...caseConnectors[mockConnector.id].push, + hasBeenPushed: false, + }, + }, + }, }), { wrapper: ({ children }) => {children}, } ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].id).toEqual(CLOSED_CASE_PUSH_ERROR_ID); + + expect(result.current.hasBeenPushed).toBe(false); }); - }); - describe('user does not have write permissions', () => { - it('disables the push button when the user does not have push permissions', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - - const { getByTestId } = render(result.current.pushButton); - - expect(getByTestId('push-to-external-service')).toBeDisabled(); - }); + it('hasBeenPushed=false if the connector does not exist', async () => { + const { result } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connector: { + id: 'not-exist', + name: 'not-exist', + type: ConnectorTypes.none, + fields: null, + }, + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + expect(result.current.hasBeenPushed).toBe(false); }); - it('does not display a message when user does not have a premium license', async () => { - useFetchActionLicenseMock.mockImplementation(() => ({ - isLoading: false, - data: { - ...actionLicense, - enabledInLicense: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); + it('hasPushPermissions=false if it does not have push permission', async () => { + useFetchActionLicenseMock.mockReturnValue({ + isLoading: true, + data: actionLicense, }); + + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(result.current.hasPushPermissions).toBe(false); }); - it('does not display a message when user does not have case enabled in config', async () => { + it('hasLicenseError=true if enabledInLicense=false', async () => { useFetchActionLicenseMock.mockImplementation(() => ({ isLoading: false, data: { ...actionLicense, - enabledInConfig: false, + enabledInLicense: false, }, })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); - }); - }); - it('does not display a message when user does not have any connector configured', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - connectors: [], - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); - }); - }); + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); - it('does not display a message when user does have a connector but is configured to none', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); - }); + expect(result.current.hasLicenseError).toBe(true); }); - it('does not display a message when connector is deleted', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - connector: { - id: 'not-exist', - name: 'not-exist', - type: ConnectorTypes.none, - fields: null, - }, - isValidConnector: false, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); - }); - }); + it('hasLicenseError=false if data=undefined', async () => { + useFetchActionLicenseMock.mockImplementation(() => ({ + isLoading: false, + data: undefined, + })); - it('does not display a message when connector is deleted with empty connectors', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - connectors: [], - connector: { - id: 'not-exist', - name: 'not-exist', - type: ConnectorTypes.none, - fields: null, - }, - isValidConnector: false, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); - }); - }); + const { result } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); - it('does not display a message when case is closed', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - caseStatus: CaseStatuses.closed, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - await waitForNextUpdate(); - expect(result.current.pushCallouts).toBeNull(); - }); + expect(result.current.hasLicenseError).toBe(false); }); }); }); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx index 71a6184c7286f8..b31a4faa80524d 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; -import { CaseCallOut } from './callout'; import { getLicenseError, getKibanaConfigError, @@ -17,47 +15,48 @@ import { getDeletedConnectorError, getCaseClosedInfo, } from './helpers'; -import * as i18n from './translations'; -import type { CaseConnector, ActionConnector } from '../../../common/api'; +import type { CaseConnector } from '../../../common/api'; import { CaseStatuses } from '../../../common/api'; -import type { CaseServices } from '../../containers/use_find_case_user_actions'; import type { ErrorMessage } from './callout/types'; import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { useCasesContext } from '../cases_context/use_cases_context'; +import type { CaseConnectors } from '../../containers/types'; export interface UsePushToService { + caseConnectors: CaseConnectors; caseId: string; - caseServices: CaseServices; caseStatus: string; connector: CaseConnector; - connectors: ActionConnector[]; - hasDataToPush: boolean; isValidConnector: boolean; - onEditClick: () => void; } export interface ReturnUsePushToService { - pushButton: JSX.Element; - pushCallouts: JSX.Element | null; + errorsMsg: ErrorMessage[]; + hasBeenPushed: boolean; + needsToBePushed: boolean; + hasPushPermissions: boolean; + isLoading: boolean; + hasErrorMessages: boolean; + hasLicenseError: boolean; + handlePushToService: () => Promise; } export const usePushToService = ({ caseId, - caseServices, caseStatus, + caseConnectors, connector, - connectors, - hasDataToPush, isValidConnector, - onEditClick, }: UsePushToService): ReturnUsePushToService => { const { permissions } = useCasesContext(); const { isLoading, pushCaseToExternalService } = usePostPushToService(); + const refreshCaseViewPage = useRefreshCaseViewPage(); - const { isLoading: loadingLicense, data: actionLicense = null } = useGetActionLicense(); + const { isLoading: isLoadingLicense, data: actionLicense = null } = useGetActionLicense(); const hasLicenseError = actionLicense != null && !actionLicense.enabledInLicense; - const refreshCaseViewPage = useRefreshCaseViewPage(); + const needsToBePushed = !!caseConnectors[connector.id]?.push.needsToBePushed; + const hasBeenPushed = !!caseConnectors[connector.id]?.push.hasBeenPushed; const handlePushToService = useCallback(async () => { if (connector.id != null && connector.id !== 'none') { @@ -88,7 +87,7 @@ export const usePushToService = ({ * By priority of importance: * 1. Show license error. * 2. Show configuration error. - * 3. Show connector configuration error if the connector is set to none or no connectors have been created. + * 3. Show connector missing information if the connector is set to none. * 4. Show an error message if the connector has been deleted or the user does not have access to it. * 5. Show case closed message. */ @@ -101,11 +100,11 @@ export const usePushToService = ({ return [getKibanaConfigError()]; } - if (connector.id === 'none' && !loadingLicense && !hasLicenseError) { + if (connector.id === 'none' && !isLoadingLicense && !hasLicenseError) { return [getConnectorMissingInfo()]; } - if (!isValidConnector && !loadingLicense && !hasLicenseError) { + if (!isValidConnector && !isLoadingLicense && !hasLicenseError) { return [getDeletedConnectorError()]; } @@ -114,79 +113,24 @@ export const usePushToService = ({ } return errors; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, permissions.update]); - - const pushToServiceButton = useMemo( - () => ( - 0 || - !permissions.push || - !isValidConnector || - !hasDataToPush - } - isLoading={isLoading} - > - {caseServices[connector.id] - ? i18n.UPDATE_THIRD(connector.name) - : i18n.PUSH_THIRD(connector.name)} - - ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - connector, - connectors, - errorsMsg, - handlePushToService, - hasDataToPush, - isLoading, - loadingLicense, - permissions.push, - isValidConnector, - ] - ); - - const objToReturn = useMemo(() => { - const hidePushButton = errorsMsg.length > 0 || !hasDataToPush || !permissions.push; - - return { - pushButton: hidePushButton ? ( - 0 ? errorsMsg[0].title : i18n.PUSH_LOCKED_TITLE(connector.name)} - content={

{errorsMsg.length > 0 ? errorsMsg[0].description : i18n.PUSH_LOCKED_DESC}

} - > - {pushToServiceButton} -
- ) : ( - <>{pushToServiceButton} - ), - pushCallouts: - errorsMsg.length > 0 ? ( - 0} - hasLicenseError={hasLicenseError} - messages={errorsMsg} - onEditClick={onEditClick} - /> - ) : null, - }; }, [ - connector.name, - connectors.length, - errorsMsg, - hasDataToPush, + actionLicense, + caseStatus, + connector.id, hasLicenseError, - onEditClick, - pushToServiceButton, - permissions.push, + isValidConnector, + isLoadingLicense, + permissions.update, ]); - return objToReturn; + return { + errorsMsg, + hasErrorMessages: errorsMsg.length > 0, + needsToBePushed, + hasBeenPushed, + isLoading: isLoading || isLoadingLicense, + hasPushPermissions: permissions.push, + hasLicenseError, + handlePushToService, + }; }; diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/translations.ts b/x-pack/plugins/cases/public/components/use_push_to_service/translations.ts index 1214b9f790e0aa..0f7e98a3845f2d 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/translations.ts +++ b/x-pack/plugins/cases/public/components/use_push_to_service/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export * from '../../common/translations'; -export const PUSH_THIRD = (thirdParty: string) => { +export const PUSH_INCIDENT = (thirdParty: string) => { if (thirdParty === 'none') { return i18n.translate('xpack.cases.caseView.pushThirdPartyIncident', { defaultMessage: 'Push as external incident', @@ -22,7 +22,7 @@ export const PUSH_THIRD = (thirdParty: string) => { }); }; -export const UPDATE_THIRD = (thirdParty: string) => { +export const UPDATE_INCIDENT = (thirdParty: string) => { if (thirdParty === 'none') { return i18n.translate('xpack.cases.caseView.updateThirdPartyIncident', { defaultMessage: 'Update external incident', @@ -76,3 +76,10 @@ export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate( export const LINK_CLOUD_DEPLOYMENT = i18n.translate('xpack.cases.caseView.cloudDeploymentLink', { defaultMessage: 'cloud deployment', }); + +export const LINK_ACTIONS_CONFIGURATION = i18n.translate( + 'xpack.cases.caseView.actionsConfigurationLink', + { + defaultMessage: 'Alerting and action settings in Kibana', + } +); diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx index f5ec9ec742d55c..c63f53f13bbe6f 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/comment.tsx @@ -86,7 +86,7 @@ const getCreateCommentUserAction = ({ comment: Comment; } & Omit< UserActionBuilderArgs, - 'caseServices' | 'comments' | 'index' | 'handleOutlineComment' | 'currentUserProfile' + 'comments' | 'index' | 'handleOutlineComment' | 'currentUserProfile' >): EuiCommentProps[] => { switch (comment.type) { case CommentType.user: @@ -185,6 +185,7 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ handleManageQuote, handleOutlineComment, actionsNavigation, + caseConnectors, }) => ({ build: () => { const commentUserAction = userAction as UserActionResponse; @@ -226,6 +227,7 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ handleDeleteComment, handleManageQuote, actionsNavigation, + caseConnectors, }); return commentAction; diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 4ba2143254065e..b9e8dac46ec17d 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -14,7 +14,6 @@ import routeData from 'react-router'; import { useUpdateComment } from '../../containers/use_update_comment'; import { basicCase, - basicPush, getUserAction, getHostIsolationUserAction, hostIsolationComment, @@ -24,6 +23,7 @@ import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer, TestProviders } from '../../common/mock'; import { Actions } from '../../../common/api'; import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; +import { connectorsMock, getCaseConnectorsMockResponse } from '../../common/mock/connectors'; const fetchUserActions = jest.fn(); const onUpdateField = jest.fn(); @@ -31,11 +31,11 @@ const updateCase = jest.fn(); const onShowAlertDetails = jest.fn(); const defaultProps = { - caseServices: {}, + caseConnectors: getCaseConnectorsMockResponse(), caseUserActions: [], userProfiles: new Map(), currentUserProfile: undefined, - connectors: [], + connectors: connectorsMock, actionsNavigation: { href: jest.fn(), onClick: jest.fn() }, getRuleDetailsHref: jest.fn(), onRuleDetailsClick: jest.fn(), @@ -95,29 +95,26 @@ describe(`UserActions`, () => { }); it('Renders service now update line with top and bottom when push is required', async () => { + const caseConnectors = getCaseConnectorsMockResponse({ needsToBePushed: true }); + const ourActions = [ - getUserAction('pushed', 'push_to_service'), - getUserAction('comment', Actions.update), + getUserAction('pushed', 'push_to_service', { + createdAt: '2023-01-17T09:46:29.813Z', + }), ]; const props = { ...defaultProps, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [`${ourActions[ourActions.length - 1].commentId}`], - hasDataToPush: true, - }, - }, + caseConnectors, caseUserActions: ourActions, }; + const wrapper = mount( ); + await waitFor(() => { expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toEqual(true); expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toEqual(true); @@ -125,19 +122,15 @@ describe(`UserActions`, () => { }); it('Renders service now update line with top only when push is up to date', async () => { - const ourActions = [getUserAction('pushed', 'push_to_service')]; + const ourActions = [ + getUserAction('pushed', 'push_to_service', { + createdAt: '2023-01-17T09:46:29.813Z', + }), + ]; + const props = { ...defaultProps, caseUserActions: ourActions, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, }; const wrapper = mount( @@ -150,6 +143,7 @@ describe(`UserActions`, () => { expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toEqual(false); }); }); + it('Outlines comment when update move to link is clicked', async () => { const ourActions = [ getUserAction('comment', Actions.create), diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx index bf6aaf6ea2a3a8..74c5cca881c2d4 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx @@ -80,7 +80,7 @@ const MyEuiCommentList = styled(EuiCommentList)` export const UserActions = React.memo( ({ - caseServices, + caseConnectors, caseUserActions, userProfiles, currentUserProfile, @@ -190,12 +190,12 @@ export const UserActions = React.memo( const userActionBuilder = builder({ appId, caseData, + caseConnectors, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, userAction, userProfiles, currentUserProfile, - caseServices, comments: caseData.comments, index, commentRefs, @@ -220,6 +220,7 @@ export const UserActions = React.memo( ), [ appId, + caseConnectors, caseUserActions, userProfiles, currentUserProfile, @@ -227,7 +228,6 @@ export const UserActions = React.memo( persistableStateAttachmentTypeRegistry, descriptionCommentListObj, caseData, - caseServices, commentRefs, manageMarkdownEditIds, selectedOutlineCommentId, diff --git a/x-pack/plugins/cases/public/components/user_actions/mock.ts b/x-pack/plugins/cases/public/components/user_actions/mock.ts index 33eae2b43fbaec..87034e4ccf191e 100644 --- a/x-pack/plugins/cases/public/components/user_actions/mock.ts +++ b/x-pack/plugins/cases/public/components/user_actions/mock.ts @@ -9,22 +9,14 @@ import { Actions } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; -import { basicCase, basicPush, getUserAction } from '../../containers/mock'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; +import { basicCase, getUserAction } from '../../containers/mock'; import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import type { UserActionBuilderArgs } from './types'; export const getMockBuilderArgs = (): UserActionBuilderArgs => { const userAction = getUserAction('title', Actions.update); const commentRefs = { current: {} }; - const caseServices = { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [], - hasDataToPush: true, - }, - }; const alertData = { 'alert-id-1': { @@ -51,6 +43,8 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => { }, }; + const caseConnectors = getCaseConnectorsMockResponse(); + const getRuleDetailsHref = jest.fn().mockReturnValue('https://example.com'); const onRuleDetailsClick = jest.fn(); const onShowAlertDetails = jest.fn(); @@ -70,7 +64,6 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => { persistableStateAttachmentTypeRegistry, caseData: basicCase, comments: basicCase.comments, - caseServices, index: 0, alertData, commentRefs, @@ -78,6 +71,7 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => { selectedOutlineCommentId: '', loadingCommentIds: [], loadingAlertData: false, + caseConnectors, getRuleDetailsHref, onRuleDetailsClick, onShowAlertDetails, diff --git a/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx b/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx index 219a7a6d2c7c8e..5404486afb5c04 100644 --- a/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/pushed.test.tsx @@ -14,13 +14,13 @@ import { getUserAction } from '../../containers/mock'; import { TestProviders } from '../../common/mock'; import { createPushedUserActionBuilder } from './pushed'; import { getMockBuilderArgs } from './mock'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/navigation/hooks'); describe('createPushedUserActionBuilder ', () => { const builderArgs = getMockBuilderArgs(); - const caseServices = builderArgs.caseServices; beforeEach(() => { jest.clearAllMocks(); @@ -31,8 +31,6 @@ describe('createPushedUserActionBuilder ', () => { const builder = createPushedUserActionBuilder({ ...builderArgs, userAction, - caseServices, - index: 0, }); const createdUserAction = builder.build(); @@ -42,20 +40,20 @@ describe('createPushedUserActionBuilder ', () => { ); - expect(screen.getByText('pushed as new incident connector name')).toBeInTheDocument(); + expect(screen.getByText('pushed as new incident My SN connector')).toBeInTheDocument(); expect(screen.getByText('external title').closest('a')).toHaveAttribute( 'href', 'basicPush.com' ); }); - it('renders correctly when updating an external service', async () => { + it('renders correctly if oldestUserActionPushDate is not defined', async () => { const userAction = getUserAction('pushed', Actions.push_to_service); + const caseConnectors = getCaseConnectorsMockResponse({ oldestUserActionPushDate: undefined }); const builder = createPushedUserActionBuilder({ ...builderArgs, + caseConnectors, userAction, - caseServices, - index: 1, }); const createdUserAction = builder.build(); @@ -65,22 +63,19 @@ describe('createPushedUserActionBuilder ', () => { ); - expect(screen.getByText('updated incident connector name')).toBeInTheDocument(); + expect(screen.getByText('pushed as new incident My SN connector')).toBeInTheDocument(); }); - it('renders the pushing indicators correctly', async () => { + it('renders correctly when updating an external service', async () => { const userAction = getUserAction('pushed', Actions.push_to_service); + const caseConnectors = getCaseConnectorsMockResponse({ + oldestUserActionPushDate: '2023-01-16T09:46:29.813Z', + }); + const builder = createPushedUserActionBuilder({ ...builderArgs, + caseConnectors, userAction, - caseServices: { - ...caseServices, - '123': { - ...caseServices['123'], - lastPushIndex: 1, - }, - }, - index: 1, }); const createdUserAction = builder.build(); @@ -90,24 +85,17 @@ describe('createPushedUserActionBuilder ', () => { ); - expect(screen.getByText('Already pushed to connector name incident')).toBeInTheDocument(); - expect(screen.getByText('Requires update to connector name incident')).toBeInTheDocument(); + expect(screen.getByText('updated incident My SN connector')).toBeInTheDocument(); }); - it('shows only the already pushed indicator if has no data to push', async () => { - const userAction = getUserAction('pushed', Actions.push_to_service); + it('shows only the top footer if it is the latest push and there is nothing to push', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service, { + createdAt: '2023-01-17T09:46:29.813Z', + }); + const builder = createPushedUserActionBuilder({ ...builderArgs, userAction, - caseServices: { - ...caseServices, - '123': { - ...caseServices['123'], - lastPushIndex: 1, - hasDataToPush: false, - }, - }, - index: 1, }); const createdUserAction = builder.build(); @@ -117,14 +105,99 @@ describe('createPushedUserActionBuilder ', () => { ); - expect(screen.getByText('Already pushed to connector name incident')).toBeInTheDocument(); + expect(screen.getByText('Already pushed to My SN connector incident')).toBeInTheDocument(); expect( - screen.queryByText('Requires update to connector name incident') + screen.queryByText('Requires update to My SN connector incident') + ).not.toBeInTheDocument(); + }); + + it('shows both footers if the connectors needs to be pushed and is the latest push', async () => { + const caseConnectors = getCaseConnectorsMockResponse({ + needsToBePushed: true, + }); + + const userAction = getUserAction('pushed', Actions.push_to_service, { + createdAt: '2023-01-17T09:46:29.813Z', + }); + const builder = createPushedUserActionBuilder({ + ...builderArgs, + caseConnectors, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect(screen.getByText('Already pushed to My SN connector incident')).toBeInTheDocument(); + expect(screen.getByText('Requires update to My SN connector incident')).toBeInTheDocument(); + }); + + it('does not show the footers if it is not the latest push', async () => { + const userAction = getUserAction('pushed', Actions.push_to_service, { + createdAt: '2020-01-17T09:46:29.813Z', + }); + + const builder = createPushedUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect( + screen.queryByText('Already pushed to My SN connector incident') + ).not.toBeInTheDocument(); + + expect( + screen.queryByText('Requires update to My SN connector incident') + ).not.toBeInTheDocument(); + }); + + it('does not show the footers if latestUserActionPushDate is not defined', async () => { + const caseConnectors = getCaseConnectorsMockResponse({ + needsToBePushed: true, + latestUserActionPushDate: undefined, + }); + + const userAction = getUserAction('pushed', Actions.push_to_service, { + createdAt: '2023-01-17T09:46:29.813Z', + }); + + const builder = createPushedUserActionBuilder({ + ...builderArgs, + caseConnectors, + userAction, + }); + + const createdUserAction = builder.build(); + render( + + + + ); + + expect( + screen.queryByText('Already pushed to My SN connector incident') + ).not.toBeInTheDocument(); + + expect( + screen.queryByText('Requires update to My SN connector incident') ).not.toBeInTheDocument(); }); it('does not show the push information if the connector is none', async () => { + const caseConnectors = getCaseConnectorsMockResponse({ needsToBePushed: true }); const userAction = getUserAction('pushed', Actions.push_to_service, { + createdAt: '2023-01-17T09:46:29.813Z', payload: { externalService: { connectorId: NONE_CONNECTOR_ID, connectorName: 'none connector' }, }, @@ -132,15 +205,8 @@ describe('createPushedUserActionBuilder ', () => { const builder = createPushedUserActionBuilder({ ...builderArgs, + caseConnectors, userAction, - caseServices: { - ...caseServices, - '123': { - ...caseServices['123'], - lastPushIndex: 1, - }, - }, - index: 1, }); const createdUserAction = builder.build(); @@ -152,9 +218,11 @@ describe('createPushedUserActionBuilder ', () => { expect(screen.queryByText('pushed as new incident none connector')).not.toBeInTheDocument(); expect(screen.queryByText('updated incident none connector')).not.toBeInTheDocument(); - expect(screen.queryByText('Already pushed to connector name incident')).not.toBeInTheDocument(); expect( - screen.queryByText('Requires update to connector name incident') + screen.queryByText('Already pushed to My SN connector incident') + ).not.toBeInTheDocument(); + expect( + screen.queryByText('Requires update to My SN connector incident') ).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx index e0352d7d96ccbc..e0f62684c01990 100644 --- a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx @@ -10,29 +10,52 @@ import type { EuiCommentProps } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import type { PushedUserAction } from '../../../common/api'; -import { Actions, NONE_CONNECTOR_ID } from '../../../common/api'; +import { Actions } from '../../../common/api'; import type { UserActionBuilder, UserActionResponse } from './types'; import { createCommonUpdateUserActionBuilder } from './common'; import * as i18n from './translations'; -import type { CaseServices } from '../../containers/use_find_case_user_actions'; -import type { CaseExternalService } from '../../containers/types'; - -const getPushInfo = ( - caseServices: CaseServices, - externalService: CaseExternalService | undefined, - index: number -) => - externalService != null && externalService.connectorId !== NONE_CONNECTOR_ID - ? { - firstPush: caseServices[externalService.connectorId]?.firstPushIndex === index, - parsedConnectorId: externalService.connectorId, - parsedConnectorName: externalService.connectorName, - } - : { - firstPush: false, - parsedConnectorId: NONE_CONNECTOR_ID, - parsedConnectorName: NONE_CONNECTOR_ID, - }; +import type { CaseConnectors } from '../../containers/types'; + +const getPushDates = ( + userActionPushedAt: string, + connectorPushedAt: string | undefined +): { userActionDate: Date; connectorDate: Date } | undefined => { + if (!connectorPushedAt) { + return; + } + + const pushedDate = new Date(userActionPushedAt); + const connectorDate = new Date(connectorPushedAt); + + if (isNaN(pushedDate.getTime()) || isNaN(connectorDate.getTime())) { + return; + } + + return { + userActionDate: pushedDate, + connectorDate, + }; +}; + +const isLatestPush = (pushedAt: string, latestPush: string | undefined) => { + const dates = getPushDates(pushedAt, latestPush); + + if (!dates) { + return false; + } + + return dates.userActionDate.getTime() >= dates.connectorDate.getTime(); +}; + +const isFirstPush = (pushedAt: string, oldestPush: string | undefined) => { + const dates = getPushDates(pushedAt, oldestPush); + + if (!dates) { + return true; + } + + return dates.userActionDate.getTime() <= dates.connectorDate.getTime(); +}; const getLabelTitle = (action: UserActionResponse, firstPush: boolean) => { const externalService = action.payload.externalService; @@ -60,50 +83,36 @@ const getLabelTitle = (action: UserActionResponse, firstPush: const getFooters = ({ userAction, - caseServices, - connectorId, - connectorName, - index, + connectorInfo, }: { userAction: UserActionResponse; - caseServices: CaseServices; - connectorId: string; - connectorName: string; - index: number; + connectorInfo: CaseConnectors[string]; }): EuiCommentProps[] => { - const showTopFooter = - userAction.action === Actions.push_to_service && - index === caseServices[connectorId]?.lastPushIndex; - - const showBottomFooter = - userAction.action === Actions.push_to_service && - index === caseServices[connectorId]?.lastPushIndex && - caseServices[connectorId].hasDataToPush; + const footers: EuiCommentProps[] = []; + const latestPush = isLatestPush( + userAction.createdAt, + connectorInfo.push.latestUserActionPushDate + ); - let footers: EuiCommentProps[] = []; + const showTopFooter = userAction.action === Actions.push_to_service && latestPush; + const showBottomFooter = showTopFooter && connectorInfo.push.needsToBePushed; if (showTopFooter) { - footers = [ - ...footers, - { - username: '', - event: i18n.ALREADY_PUSHED_TO_SERVICE(`${connectorName}`), - timelineAvatar: 'sortUp', - 'data-test-subj': 'top-footer', - }, - ]; + footers.push({ + username: '', + event: i18n.ALREADY_PUSHED_TO_SERVICE(`${connectorInfo.name}`), + timelineAvatar: 'sortUp', + 'data-test-subj': 'top-footer', + }); } if (showBottomFooter) { - footers = [ - ...footers, - { - username: '', - event: i18n.REQUIRED_UPDATE_TO_SERVICE(`${connectorName}`), - timelineAvatar: 'sortDown', - 'data-test-subj': 'bottom-footer', - }, - ]; + footers.push({ + username: '', + event: i18n.REQUIRED_UPDATE_TO_SERVICE(`${connectorInfo.name}`), + timelineAvatar: 'sortDown', + 'data-test-subj': 'bottom-footer', + }); } return footers; @@ -112,31 +121,25 @@ const getFooters = ({ export const createPushedUserActionBuilder: UserActionBuilder = ({ userAction, userProfiles, - caseServices, - index, + caseConnectors, handleOutlineComment, }) => ({ build: () => { const pushedUserAction = userAction as UserActionResponse; - const { firstPush, parsedConnectorId, parsedConnectorName } = getPushInfo( - caseServices, - pushedUserAction.payload.externalService, - index - ); + const connectorId = pushedUserAction.payload.externalService.connectorId; + const connectorInfo = caseConnectors[connectorId]; - if (parsedConnectorId === NONE_CONNECTOR_ID) { + if (!connectorInfo) { return []; } - const footers = getFooters({ - userAction: pushedUserAction, - caseServices, - connectorId: parsedConnectorId, - connectorName: parsedConnectorName, - index, - }); - + const firstPush = isFirstPush( + userAction.createdAt, + connectorInfo.push.oldestUserActionPushDate + ); + const footers = getFooters({ userAction: pushedUserAction, connectorInfo }); const label = getLabelTitle(pushedUserAction, firstPush); + const commonBuilder = createCommonUpdateUserActionBuilder({ userProfiles, userAction, diff --git a/x-pack/plugins/cases/public/components/user_actions/types.ts b/x-pack/plugins/cases/public/components/user_actions/types.ts index 9bf750437d29dd..92001d633e9123 100644 --- a/x-pack/plugins/cases/public/components/user_actions/types.ts +++ b/x-pack/plugins/cases/public/components/user_actions/types.ts @@ -9,8 +9,13 @@ import type { EuiCommentProps } from '@elastic/eui'; import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import type { SnakeToCamelCase } from '../../../common/types'; import type { ActionTypes, UserActionWithResponse } from '../../../common/api'; -import type { Case, CaseUserActions, Comment, UseFetchAlertData } from '../../containers/types'; -import type { CaseServices } from '../../containers/use_find_case_user_actions'; +import type { + Case, + CaseConnectors, + CaseUserActions, + Comment, + UseFetchAlertData, +} from '../../containers/types'; import type { AddCommentRefObject } from '../add_comment'; import type { UserActionMarkdownRefObject } from './markdown_form'; import type { CasesNavigation } from '../links'; @@ -21,7 +26,7 @@ import type { PersistableStateAttachmentTypeRegistry } from '../../client/attach import type { CurrentUserProfile } from '../types'; export interface UserActionTreeProps { - caseServices: CaseServices; + caseConnectors: CaseConnectors; caseUserActions: CaseUserActions[]; userProfiles: Map; currentUserProfile: CurrentUserProfile; @@ -47,8 +52,8 @@ export interface UserActionBuilderArgs { currentUserProfile: CurrentUserProfile; externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; + caseConnectors: CaseConnectors; userAction: CaseUserActions; - caseServices: CaseServices; comments: Comment[]; index: number; commentRefs: React.MutableRefObject< diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 431ce6626e2eae..ea329f67ad791f 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -27,7 +27,7 @@ import { tags, findCaseUserActionsResponse, } from '../mock'; -import type { CaseUpdateRequest, ResolvedCase } from '../../../common/ui/types'; +import type { CaseConnectors, CaseUpdateRequest, ResolvedCase } from '../../../common/ui/types'; import { SeverityAll } from '../../../common/ui/types'; import type { CasePatchRequest, @@ -39,6 +39,7 @@ import { CaseStatuses } from '../../../common/api'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; import type { UserProfile } from '@kbn/security-plugin/common'; import { userProfiles } from '../user_profiles/api.mock'; +import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; export const getCase = async ( caseId: string, @@ -140,3 +141,8 @@ export const getFeatureIds = async ( _query: { registrationContext: string[] }, _signal: AbortSignal ): Promise => Promise.resolve(['siem', 'observability']); + +export const getCaseConnectors = async ( + caseId: string, + signal: AbortSignal +): Promise => Promise.resolve(getCaseConnectorsMockResponse()); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index c744fc36335cc8..f7a57f8e65aff2 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -11,6 +11,7 @@ import { KibanaServices } from '../common/lib/kibana'; import { ConnectorTypes, CommentType, CaseStatuses, CaseSeverity } from '../../common/api'; import { + CASES_INTERNAL_URL, CASES_URL, INTERNAL_BULK_CREATE_ATTACHMENTS_URL, SECURITY_SOLUTION_OWNER, @@ -34,6 +35,7 @@ import { resolveCase, getFeatureIds, postComment, + getCaseConnectors, } from './api'; import { @@ -54,10 +56,12 @@ import { caseWithRegisteredAttachmentsSnake, caseWithRegisteredAttachments, caseUserActionsWithRegisteredAttachmentsSnake, + basicPushSnake, } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import { getCasesStatus } from '../api'; +import { getCaseConnectorsMockResponse } from '../common/mock/connectors'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -846,4 +850,33 @@ describe('Cases API', () => { expect(resp).toEqual(caseWithRegisteredAttachments); }); }); + + describe('getCaseConnectors', () => { + const caseConnectors = getCaseConnectorsMockResponse(); + const connectorCamelCase = caseConnectors['servicenow-1']; + + const snakeCaseConnector = { + ...connectorCamelCase, + push: { ...connectorCamelCase.push, externalService: basicPushSnake }, + }; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue({ 'servicenow-1': snakeCaseConnector }); + }); + + it('should be called with correct check url, method, signal', async () => { + await getCaseConnectors(basicCase.id, abortCtrl.signal); + + expect(fetchMock).toHaveBeenCalledWith(`${CASES_INTERNAL_URL}/${basicCase.id}/_connectors`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + it('should return correct response', async () => { + const resp = await getCaseConnectors(basicCase.id, abortCtrl.signal); + expect(resp).toEqual({ 'servicenow-1': connectorCamelCase }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 9c4f1a07ee62fb..aabe48638efa7a 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -8,6 +8,7 @@ import type { ValidFeatureId } from '@kbn/rule-data-utils'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants'; import type { + CaseConnectors, Cases, CaseUpdateRequest, FetchCasesProps, @@ -27,6 +28,7 @@ import type { User, SingleCaseMetricsResponse, CasesFindResponse, + GetCaseConnectorsResponse, } from '../../common/api'; import { CommentType, @@ -36,6 +38,7 @@ import { getCasePushUrl, getCaseFindUserActionsUrl, getCaseCommentDeleteUrl, + getCaseConnectorsUrl, } from '../../common/api'; import { CASE_REPORTERS_URL, @@ -378,3 +381,28 @@ export const getFeatureIds = async ( } ); }; + +export const getCaseConnectors = async ( + caseId: string, + signal: AbortSignal +): Promise => { + const res = await KibanaServices.get().http.fetch( + getCaseConnectorsUrl(caseId), + { + method: 'GET', + signal, + } + ); + + return Object.keys(res).reduce( + (acc, connectorId) => ({ + ...acc, + [connectorId]: { + ...convertToCamelCase( + res[connectorId] + ), + }, + }), + {} + ); +}; diff --git a/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts index 0d85e75478be36..607f6d01191ff2 100644 --- a/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts @@ -17,8 +17,9 @@ import type { CaseConfigure } from '../types'; import { caseConfigurationCamelCaseResponseMock } from '../mock'; import { actionTypesMock, connectorsMock } from '../../../common/mock/connectors'; -export const fetchConnectors = async ({ signal }: ApiProps): Promise => - Promise.resolve(connectorsMock); +export const getSupportedActionConnectors = async ({ + signal, +}: ApiProps): Promise => Promise.resolve(connectorsMock); export const getCaseConfigure = async ({ signal }: ApiProps): Promise => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/plugins/cases/public/containers/configure/api.test.ts b/x-pack/plugins/cases/public/containers/configure/api.test.ts index 0b1f0c8d172ea5..9099f908a78713 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.test.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.test.ts @@ -6,7 +6,7 @@ */ import { - fetchConnectors, + getSupportedActionConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure, @@ -37,7 +37,7 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { - await fetchConnectors({ signal: abortCtrl.signal }); + await getSupportedActionConnectors({ signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { method: 'GET', signal: abortCtrl.signal, @@ -45,7 +45,7 @@ describe('Case Configuration API', () => { }); test('happy path', async () => { - const resp = await fetchConnectors({ signal: abortCtrl.signal }); + const resp = await getSupportedActionConnectors({ signal: abortCtrl.signal }); expect(resp).toEqual(connectorsMock); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index 72702e27fbb56b..b0f2b8df4a0931 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -24,7 +24,9 @@ import type { ApiProps } from '../types'; import { decodeCaseConfigurationsResponse, decodeCaseConfigureResponse } from '../utils'; import type { CaseConfigure } from './types'; -export const fetchConnectors = async ({ signal }: ApiProps): Promise => { +export const getSupportedActionConnectors = async ({ + signal, +}: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch( `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { method: 'GET', signal } diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx similarity index 85% rename from x-pack/plugins/cases/public/containers/configure/use_connectors.tsx rename to x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx index f9c19ee1776bd8..0bba69ca47df10 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx @@ -6,13 +6,13 @@ */ import { useQuery } from '@tanstack/react-query'; -import { fetchConnectors } from './api'; +import { getSupportedActionConnectors } from './api'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; import * as i18n from './translations'; import { casesQueriesKeys } from '../constants'; import type { ServerError } from '../../types'; -export function useGetConnectors() { +export function useGetSupportedActionConnectors() { const toasts = useToasts(); const { actions } = useApplicationCapabilities(); return useQuery( @@ -22,7 +22,7 @@ export function useGetConnectors() { return []; } const abortCtrl = new AbortController(); - return fetchConnectors({ signal: abortCtrl.signal }); + return getSupportedActionConnectors({ signal: abortCtrl.signal }); }, { onError: (error: ServerError) => { diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx similarity index 78% rename from x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx rename to x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx index 076e1a8408482a..36cbd9417e375e 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx @@ -10,7 +10,7 @@ import { renderHook } from '@testing-library/react-hooks'; import * as api from './api'; import { TestProviders } from '../../common/mock'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; -import { useGetConnectors } from './use_connectors'; +import { useGetSupportedActionConnectors } from './use_get_supported_action_connectors'; const useApplicationCapabilitiesMock = useApplicationCapabilities as jest.Mocked< typeof useApplicationCapabilities @@ -25,8 +25,8 @@ describe('useConnectors', () => { }); it('fetches connectors', async () => { - const spy = jest.spyOn(api, 'fetchConnectors'); - const { waitForNextUpdate } = renderHook(() => useGetConnectors(), { + const spy = jest.spyOn(api, 'getSupportedActionConnectors'); + const { waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }) => {children}, }); @@ -39,12 +39,12 @@ describe('useConnectors', () => { const addError = jest.fn(); (useToasts as jest.Mock).mockReturnValue({ addError }); - const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + const spyOnfetchConnectors = jest.spyOn(api, 'getSupportedActionConnectors'); spyOnfetchConnectors.mockImplementation(() => { throw new Error('Something went wrong'); }); - const { waitForNextUpdate } = renderHook(() => useGetConnectors(), { + const { waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }) => {children}, }); await waitForNextUpdate(); @@ -53,10 +53,10 @@ describe('useConnectors', () => { }); it('does not fetch connectors when the user does not has access to actions', async () => { - const spyOnFetchConnectors = jest.spyOn(api, 'fetchConnectors'); + const spyOnFetchConnectors = jest.spyOn(api, 'getSupportedActionConnectors'); useApplicationCapabilitiesMock().actions = { crud: false, read: false }; - const { result, waitForNextUpdate } = renderHook(() => useGetConnectors(), { + const { result, waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { wrapper: ({ children }) => {children}, }); diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 2e57a17be08ecb..10d890629311ee 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -24,8 +24,8 @@ export const casesQueriesKeys = { case: (id: string) => [...casesQueriesKeys.caseView(), id] as const, caseMetrics: (id: string, features: SingleCaseMetricsFeature[]) => [...casesQueriesKeys.case(id), 'metrics', features] as const, - userActions: (id: string, connectorId: string) => - [...casesQueriesKeys.case(id), 'user-actions', connectorId] as const, + caseConnectors: (id: string) => [...casesQueriesKeys.case(id), 'connectors'], + userActions: (id: string) => [...casesQueriesKeys.case(id), 'user-actions'] as const, userProfiles: () => [...casesQueriesKeys.users, 'user-profiles'] as const, userProfilesList: (ids: string[]) => [...casesQueriesKeys.userProfiles(), ids] as const, currentUser: () => [...casesQueriesKeys.users, 'current-user'] as const, diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 5cadec4818457d..4abe077f378db0 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -53,12 +53,14 @@ export { connectorsMock } from '../common/mock/connectors'; export const basicCaseId = 'basic-case-id'; export const caseWithAlertsId = 'case-with-alerts-id'; export const caseWithAlertsSyncOffId = 'case-with-alerts-syncoff-id'; +export const pushConnectorId = 'servicenow-1'; const basicCommentId = 'basic-comment-id'; const basicCreatedAt = '2020-02-19T23:06:33.798Z'; const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; const basicClosedAt = '2020-02-21T15:02:57.995Z'; -const laterTime = '2020-02-28T15:02:57.995Z'; +const basicPushedAt = '2023-01-17T09:46:29.813Z'; +const laterTime = '2023-01-18T09:46:29.813Z'; export const elasticUser = { fullName: 'Leslie Knope', @@ -370,21 +372,21 @@ export const casesMetrics: CasesMetrics = { }; export const basicPush = { - connectorId: '123', - connectorName: 'connector name', + connectorId: pushConnectorId, + connectorName: 'My SN connector', externalId: 'external_id', externalTitle: 'external title', externalUrl: 'basicPush.com', - pushedAt: basicUpdatedAt, + pushedAt: basicPushedAt, pushedBy: elasticUser, }; export const pushedCase: Case = { ...basicCase, connector: { - id: '123', - name: 'My Connector', - type: ConnectorTypes.jira, + id: pushConnectorId, + name: 'My SN connector', + type: ConnectorTypes.serviceNowITSM, fields: null, }, externalService: basicPush, @@ -539,10 +541,9 @@ export const casesStatusSnake: CasesStatusResponse = { count_open_cases: 20, }; -export const pushConnectorId = '123'; export const pushSnake = { connector_id: pushConnectorId, - connector_name: 'connector name', + connector_name: 'My SN connector', external_id: 'external_id', external_title: 'external title', external_url: 'basicPush.com', @@ -550,16 +551,16 @@ export const pushSnake = { export const basicPushSnake = { ...pushSnake, - pushed_at: basicUpdatedAt, + pushed_at: basicPushedAt, pushed_by: elasticUserSnake, }; export const pushedCaseSnake = { ...basicCaseSnake, connector: { - id: '123', - name: 'My Connector', - type: ConnectorTypes.jira, + id: pushConnectorId, + name: 'My SN connector', + type: ConnectorTypes.serviceNowITSM, fields: null, }, external_service: { ...basicPushSnake, connector_id: pushConnectorId }, @@ -605,11 +606,11 @@ export const getUserAction = ( const externalService = { connectorId: pushConnectorId, - connectorName: 'connector name', + connectorName: 'My SN connector', externalId: 'external_id', externalTitle: 'external title', externalUrl: 'basicPush.com', - pushedAt: basicUpdatedAt, + pushedAt: basicPushedAt, pushedBy: elasticUser, }; @@ -667,6 +668,7 @@ export const getUserAction = ( case ActionTypes.pushed: return { ...commonProperties, + createdAt: basicPushedAt, type: ActionTypes.pushed, payload: { externalService, diff --git a/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx b/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx index 5b98b71468c6fb..4330323d7a8b20 100644 --- a/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_find_case_user_actions.test.tsx @@ -5,24 +5,20 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; import type { UseFindCaseUserActions } from './use_find_case_user_actions'; -import { getPushedInfo, useFindCaseUserActions } from './use_find_case_user_actions'; +import { useFindCaseUserActions } from './use_find_case_user_actions'; import { basicCase, - basicPush, caseUserActions, elasticUser, - getJiraConnector, - getUserAction, - jiraFields, findCaseUserActionsResponse, + getUserAction, } from './mock'; import { Actions } from '../../common/api'; import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import { testQueryClient } from '../common/mock'; -import { waitFor } from '@testing-library/dom'; import * as api from './api'; import { useToasts } from '../common/lib/kibana'; @@ -39,36 +35,33 @@ const wrapper: React.FC = ({ children }) => ( {children} ); -describe('useFindCaseUserActions', () => { +describe('UseFindCaseUserActions', () => { beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); it('returns proper state on findCaseUserActions', async () => { - await act(async () => { - const { result } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), - { wrapper } - ); - await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ - ...initialData, - data: { - caseServices: {}, - caseUserActions: [...findCaseUserActionsResponse.userActions], - hasDataToPush: true, - participants: [elasticUser], - profileUids: new Set(), - }, - isError: false, - isLoading: false, - isFetching: false, - }) - ); - }); - }); + const { result, waitForNextUpdate } = renderHook( + () => useFindCaseUserActions(basicCase.id), + { wrapper } + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual( + expect.objectContaining({ + ...initialData, + data: { + caseUserActions: [...findCaseUserActionsResponse.userActions], + participants: [elasticUser], + profileUids: new Set(), + }, + isError: false, + isLoading: false, + isFetching: false, + }) + ); }); it('shows a toast error when the API returns an error', async () => { @@ -78,10 +71,12 @@ describe('useFindCaseUserActions', () => { (useToasts as jest.Mock).mockReturnValue({ addError }); const { waitForNextUpdate } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), + () => useFindCaseUserActions(basicCase.id), { wrapper } ); + await waitForNextUpdate(); + expect(spy).toHaveBeenCalledWith(basicCase.id, expect.any(AbortSignal)); expect(addError).toHaveBeenCalled(); }); @@ -97,20 +92,18 @@ describe('useFindCaseUserActions', () => { }) ); - await act(async () => { - const { result } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), - { wrapper } - ); + const { result, waitForNextUpdate } = renderHook( + () => useFindCaseUserActions(basicCase.id), + { wrapper } + ); + + await waitForNextUpdate(); - await waitFor(() => { - expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` Set { "456", } `); - }); - }); }); it('aggregates the uids from a push', async () => { @@ -127,20 +120,18 @@ describe('useFindCaseUserActions', () => { }) ); - await act(async () => { - const { result } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), - { wrapper } - ); + const { result, waitForNextUpdate } = renderHook( + () => useFindCaseUserActions(basicCase.id), + { wrapper } + ); + + await waitForNextUpdate(); - await waitFor(() => { - expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` Set { "123", } `); - }); - }); }); it('aggregates the uids from an assignment add user action', async () => { @@ -153,21 +144,19 @@ describe('useFindCaseUserActions', () => { }) ); - await act(async () => { - const { result } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), - { wrapper } - ); + const { result, waitForNextUpdate } = renderHook( + () => useFindCaseUserActions(basicCase.id), + { wrapper } + ); + + await waitForNextUpdate(); - await waitFor(() => { - expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` Set { "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", } `); - }); - }); }); it('ignores duplicate uids', async () => { @@ -184,21 +173,19 @@ describe('useFindCaseUserActions', () => { }) ); - await act(async () => { - const { result } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), - { wrapper } - ); + const { result, waitForNextUpdate } = renderHook( + () => useFindCaseUserActions(basicCase.id), + { wrapper } + ); + + await waitForNextUpdate(); - await waitFor(() => { - expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` Set { "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", } `); - }); - }); }); it('aggregates the uids from an assignment delete user action', async () => { @@ -211,562 +198,19 @@ describe('useFindCaseUserActions', () => { }) ); - await act(async () => { - const { result } = renderHook( - () => useFindCaseUserActions(basicCase.id, basicCase.connector.id), - { wrapper } - ); + const { result, waitForNextUpdate } = renderHook( + () => useFindCaseUserActions(basicCase.id), + { wrapper } + ); + + await waitForNextUpdate(); - await waitFor(() => { - expect(result.current.data?.profileUids).toMatchInlineSnapshot(` + expect(result.current.data?.profileUids).toMatchInlineSnapshot(` Set { "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0", "u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0", } `); - }); - }); - }); - }); - - describe('getPushedInfo', () => { - it('Correctly marks first/last index - hasDataToPush: false', () => { - const userActions = [...caseUserActions, getUserAction('pushed', Actions.push_to_service)]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Correctly marks first/last index and comment id - hasDataToPush: true', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.create), - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [userActions[userActions.length - 1].commentId], - hasDataToPush: true, - }, - }, - }); - }); - - it('Correctly marks first/last index and multiple comment ids, both needs push', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.create), - { ...getUserAction('comment', Actions.create), commentId: 'muahaha' }, - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [ - userActions[userActions.length - 2].commentId, - userActions[userActions.length - 1].commentId, - ], - hasDataToPush: true, - }, - }, - }); - }); - - it('Correctly marks first/last index and multiple comment ids, one needs push', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.create), - getUserAction('pushed', Actions.push_to_service), - { ...getUserAction('comment', Actions.create), commentId: 'muahaha' }, - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 5, - commentsToUpdate: [userActions[userActions.length - 1].commentId], - hasDataToPush: true, - }, - }, - }); - }); - - it('Correctly marks first/last index and multiple comment ids, one needs push and one needs update', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.create), - getUserAction('pushed', Actions.push_to_service), - { ...getUserAction('comment', Actions.create), commentId: 'muahaha' }, - getUserAction('comment', Actions.update), - getUserAction('comment', Actions.update), - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 5, - commentsToUpdate: [ - userActions[userActions.length - 3].commentId, - userActions[userActions.length - 1].commentId, - ], - hasDataToPush: true, - }, - }, - }); - }); - - it('Does not count connector update as a reason to push', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('connector', Actions.update), - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Correctly handles multiple push actions', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.create), - getUserAction('pushed', Actions.push_to_service), - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 5, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Correctly handles comment update with multiple push actions', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.create), - getUserAction('pushed', Actions.push_to_service), - getUserAction('comment', Actions.update), - ]; - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 5, - commentsToUpdate: [userActions[userActions.length - 1].commentId], - hasDataToPush: true, - }, - }, - }); - }); - - it('Multiple connector tracking - hasDataToPush: true', () => { - const pushAction123 = getUserAction('pushed', Actions.push_to_service); - const push456 = { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - }; - - const pushAction456 = getUserAction('pushed', Actions.push_to_service, { - payload: { externalService: push456 }, - }); - - const userActions = [ - ...caseUserActions, - pushAction123, - getUserAction('comment', Actions.create), - pushAction456, - ]; - - const result = getPushedInfo(userActions, '123'); - - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [userActions[userActions.length - 2].commentId], - hasDataToPush: true, - }, - '456': { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - firstPushIndex: 5, - lastPushIndex: 5, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Multiple connector tracking - hasDataToPush: false', () => { - const pushAction123 = getUserAction('pushed', Actions.push_to_service); - const push456 = { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - }; - - const pushAction456 = getUserAction('pushed', Actions.push_to_service, { - payload: { externalService: push456 }, - }); - - const userActions = [ - ...caseUserActions, - pushAction123, - getUserAction('comment', Actions.create), - pushAction456, - ]; - - const result = getPushedInfo(userActions, '456'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [userActions[userActions.length - 2].commentId], - hasDataToPush: true, - }, - '456': { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - firstPushIndex: 5, - lastPushIndex: 5, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Change fields of current connector - hasDataToPush: true', () => { - const userActions = [ - ...caseUserActions, - createUpdate123HighPriorityConnector(), - getUserAction('pushed', Actions.push_to_service), - createUpdate123LowPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 4, - lastPushIndex: 4, - commentsToUpdate: [], - hasDataToPush: true, - }, - }, - }); - }); - - it('Change current connector - hasDataToPush: true', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - createUpdate456HighPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Change connector and back - hasDataToPush: true', () => { - const userActions = [ - ...caseUserActions, - getUserAction('pushed', Actions.push_to_service), - createUpdate456HighPriorityConnector(), - createUpdate123HighPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 3, - lastPushIndex: 3, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Change fields and connector after push - hasDataToPush: true', () => { - const userActions = [ - ...caseUserActions, - createUpdate123HighPriorityConnector(), - getUserAction('pushed', Actions.push_to_service), - createUpdate456HighPriorityConnector(), - createUpdate123LowPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 4, - lastPushIndex: 4, - commentsToUpdate: [], - hasDataToPush: true, - }, - }, - }); - }); - - it('Change only connector after push - hasDataToPush: false', () => { - const userActions = [ - ...caseUserActions, - createUpdate123HighPriorityConnector(), - getUserAction('pushed', Actions.push_to_service), - createUpdate456HighPriorityConnector(), - createUpdate123HighPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 4, - lastPushIndex: 4, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Change connectors and fields - multiple pushes', () => { - const pushAction123 = getUserAction('pushed', Actions.push_to_service); - const push456 = { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - }; - - const pushAction456 = getUserAction('pushed', Actions.push_to_service, { - payload: { externalService: push456 }, - }); - - const userActions = [ - ...caseUserActions, - createUpdate123HighPriorityConnector(), - pushAction123, - createUpdate456HighPriorityConnector(), - pushAction456, - createUpdate123LowPriorityConnector(), - createUpdate456HighPriorityConnector(), - createUpdate123LowPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: true, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 4, - lastPushIndex: 4, - commentsToUpdate: [], - hasDataToPush: true, - }, - '456': { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - firstPushIndex: 6, - lastPushIndex: 6, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('pushing other connectors does not count as an update', () => { - const pushAction123 = getUserAction('pushed', Actions.push_to_service); - const push456 = { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - }; - - const pushAction456 = getUserAction('pushed', Actions.push_to_service, { - payload: { externalService: push456 }, - }); - - const userActions = [ - ...caseUserActions, - createUpdate123HighPriorityConnector(), - pushAction123, - createUpdate456HighPriorityConnector(), - pushAction456, - createUpdate123HighPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 4, - lastPushIndex: 4, - commentsToUpdate: [], - hasDataToPush: false, - }, - '456': { - ...basicPush, - connectorId: '456', - connectorName: 'other connector name', - externalId: 'other_external_id', - firstPushIndex: 6, - lastPushIndex: 6, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); - }); - - it('Changing other connectors fields does not count as an update', () => { - const userActions = [ - ...caseUserActions, - createUpdate123HighPriorityConnector(), - getUserAction('pushed', Actions.push_to_service), - createUpdate456HighPriorityConnector(), - createUpdate456HighPriorityConnector(), - ]; - - const result = getPushedInfo(userActions, '123'); - expect(result).toEqual({ - hasDataToPush: false, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 4, - lastPushIndex: 4, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }); }); }); }); - -const jira123HighPriorityFields = { - fields: { ...jiraFields.fields, priority: 'High' }, -}; - -const jira123LowPriorityFields = { - fields: { ...jiraFields.fields, priority: 'Low' }, -}; - -const jira456Fields = { - fields: { issueType: '10', parent: null, priority: null }, -}; - -const jira456HighPriorityFields = { - id: '456', - fields: { ...jira456Fields.fields, priority: 'High' }, -}; - -const createUpdate123HighPriorityConnector = () => - getUserAction('connector', Actions.update, { - payload: { connector: getJiraConnector(jira123HighPriorityFields) }, - }); - -const createUpdate123LowPriorityConnector = () => - getUserAction('connector', Actions.update, { - payload: { connector: getJiraConnector(jira123LowPriorityFields) }, - }); - -const createUpdate456HighPriorityConnector = () => - getUserAction('connector', Actions.update, { - payload: { connector: getJiraConnector(jira456HighPriorityFields) }, - }); diff --git a/x-pack/plugins/cases/public/containers/use_find_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_find_case_user_actions.tsx index 46236ed36be3d9..8470ea439d6fa2 100644 --- a/x-pack/plugins/cases/public/containers/use_find_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_find_case_user_actions.tsx @@ -6,209 +6,17 @@ */ import { isEmpty, uniqBy } from 'lodash/fp'; -import deepEqual from 'fast-deep-equal'; import { useQuery } from '@tanstack/react-query'; -import type { CaseUserActions, CaseExternalService } from '../../common/ui/types'; -import type { CaseConnector } from '../../common/api'; -import { ActionTypes, NONE_CONNECTOR_ID } from '../../common/api'; +import type { CaseUserActions } from '../../common/ui/types'; +import { ActionTypes } from '../../common/api'; import { findCaseUserActions } from './api'; -import { - isPushedUserAction, - isConnectorUserAction, - isCreateCaseUserAction, -} from '../../common/utils/user_actions'; +import { isPushedUserAction } from '../../common/utils/user_actions'; import type { ServerError } from '../types'; import { useToasts } from '../common/lib/kibana'; import { ERROR_TITLE } from './translations'; import { casesQueriesKeys } from './constants'; -export interface CaseService extends CaseExternalService { - firstPushIndex: number; - lastPushIndex: number; - commentsToUpdate: string[]; - hasDataToPush: boolean; -} - -export interface CaseServices { - [key: string]: CaseService; -} - -const groupConnectorFields = ( - userActions: CaseUserActions[] -): Record> => - userActions.reduce((acc, mua) => { - if ( - (isConnectorUserAction(mua) || isCreateCaseUserAction(mua)) && - mua.payload?.connector?.id !== NONE_CONNECTOR_ID - ) { - const connector = mua.payload.connector; - - return { - ...acc, - [connector.id]: [...(acc[connector.id] || []), connector.fields], - }; - } - - return acc; - }, {} as Record>); - -const connectorHasChangedFields = ({ - connectorFieldsBeforePush, - connectorFieldsAfterPush, - connectorId, -}: { - connectorFieldsBeforePush: Record> | null; - connectorFieldsAfterPush: Record> | null; - connectorId: string; -}): boolean => { - if (connectorFieldsAfterPush == null || connectorFieldsAfterPush[connectorId] == null) { - return false; - } - - const fieldsAfterPush = connectorFieldsAfterPush[connectorId]; - - if (connectorFieldsBeforePush != null && connectorFieldsBeforePush[connectorId] != null) { - const fieldsBeforePush = connectorFieldsBeforePush[connectorId]; - return !deepEqual( - fieldsBeforePush[fieldsBeforePush.length - 1], - fieldsAfterPush[fieldsAfterPush.length - 1] - ); - } - - if (fieldsAfterPush.length >= 2) { - return !deepEqual( - fieldsAfterPush[fieldsAfterPush.length - 2], - fieldsAfterPush[fieldsAfterPush.length - 1] - ); - } - - return false; -}; - -interface CommentsAndIndex { - commentId: string; - commentIndex: number; -} - -export const getPushedInfo = ( - caseUserActions: CaseUserActions[], - caseConnectorId: string -): { - caseServices: CaseServices; - hasDataToPush: boolean; -} => { - const hasDataToPushForConnector = (connectorId: string): boolean => { - const caseUserActionsReversed = [...caseUserActions].reverse(); - const lastPushOfConnectorReversedIndex = caseUserActionsReversed.findIndex( - (mua) => - isPushedUserAction<'camelCase'>(mua) && - mua.payload.externalService.connectorId === connectorId - ); - - if (lastPushOfConnectorReversedIndex === -1) { - return true; - } - - const lastPushOfConnectorIndex = - caseUserActionsReversed.length - lastPushOfConnectorReversedIndex - 1; - - const actionsBeforePush = caseUserActions.slice(0, lastPushOfConnectorIndex); - const actionsAfterPush = caseUserActions.slice( - lastPushOfConnectorIndex + 1, - caseUserActionsReversed.length - ); - - const connectorFieldsBeforePush = groupConnectorFields(actionsBeforePush); - const connectorFieldsAfterPush = groupConnectorFields(actionsAfterPush); - - const connectorHasChanged = connectorHasChangedFields({ - connectorFieldsBeforePush, - connectorFieldsAfterPush, - connectorId, - }); - - return ( - actionsAfterPush.some( - (mua) => mua.type !== ActionTypes.connector && mua.type !== ActionTypes.pushed - ) || connectorHasChanged - ); - }; - - const commentsAndIndex = caseUserActions.reduce( - (bacc, mua, index) => - mua.type === ActionTypes.comment && mua.commentId != null - ? [ - ...bacc, - { - commentId: mua.commentId, - commentIndex: index, - }, - ] - : bacc, - [] - ); - - let caseServices = caseUserActions.reduce((acc, cua, i) => { - if (!isPushedUserAction<'camelCase'>(cua)) { - return acc; - } - - const externalService = cua.payload.externalService; - if (externalService === null) { - return acc; - } - - return { - ...acc, - ...(acc[externalService.connectorId] != null - ? { - [externalService.connectorId]: { - ...acc[externalService.connectorId], - ...externalService, - lastPushIndex: i, - commentsToUpdate: [], - }, - } - : { - [externalService.connectorId]: { - ...externalService, - firstPushIndex: i, - lastPushIndex: i, - hasDataToPush: hasDataToPushForConnector(externalService.connectorId), - commentsToUpdate: [], - }, - }), - }; - }, {}); - - caseServices = Object.keys(caseServices).reduce((acc, key) => { - return { - ...acc, - [key]: { - ...caseServices[key], - // if the comment happens after the lastUpdateToCaseIndex, it should be included in commentsToUpdate - commentsToUpdate: commentsAndIndex.reduce( - (bacc, currentComment) => - currentComment.commentIndex > caseServices[key].lastPushIndex - ? bacc.indexOf(currentComment.commentId) > -1 - ? [...bacc.filter((e) => e !== currentComment.commentId), currentComment.commentId] - : [...bacc, currentComment.commentId] - : bacc, - [] - ), - }, - }; - }, {}); - - const hasDataToPush = - caseServices[caseConnectorId] != null ? caseServices[caseConnectorId].hasDataToPush : true; - return { - hasDataToPush, - caseServices, - }; -}; - export const getProfileUids = (userActions: CaseUserActions[]) => { const uids = userActions.reduce>((acc, userAction) => { if (userAction.type === ActionTypes.assignees) { @@ -235,29 +43,25 @@ export const getProfileUids = (userActions: CaseUserActions[]) => { return uids; }; -export const useFindCaseUserActions = (caseId: string, caseConnectorId: string) => { +export const useFindCaseUserActions = (caseId: string) => { const toasts = useToasts(); const abortCtrlRef = new AbortController(); return useQuery( - casesQueriesKeys.userActions(caseId, caseConnectorId), + casesQueriesKeys.userActions(caseId), async () => { const response = await findCaseUserActions(caseId, abortCtrlRef.signal); const participants = !isEmpty(response.userActions) ? uniqBy('createdBy.username', response.userActions).map((cau) => cau.createdBy) : []; - const caseUserActions: CaseUserActions[] = !isEmpty(response.userActions) - ? response.userActions - : []; - const pushedInfo = getPushedInfo(caseUserActions, caseConnectorId); + const caseUserActions = !isEmpty(response.userActions) ? response.userActions : []; const profileUids = getProfileUids(caseUserActions); return { caseUserActions, participants, profileUids, - ...pushedInfo, }; }, { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx new file mode 100644 index 00000000000000..220d37deefb5bd --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_case_connectors.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import * as api from './api'; +import type { AppMockRenderer } from '../common/mock'; +import { createAppMockRenderer } from '../common/mock'; +import { useToasts } from '../common/lib/kibana'; +import { useGetCaseConnectors } from './use_get_case_connectors'; + +jest.mock('./api'); +jest.mock('../common/lib/kibana'); + +describe('useGetCaseConnectors', () => { + const caseId = 'test-id'; + const abortCtrl = new AbortController(); + const addSuccess = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError: jest.fn() }); + + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('calls getCaseConnectors with correct arguments', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCaseConnectors'); + const { waitForNextUpdate } = renderHook(() => useGetCaseConnectors(caseId), { + wrapper: appMockRender.AppWrapper, + }); + + await waitForNextUpdate(); + + expect(spyOnGetCases).toBeCalledWith('test-id', abortCtrl.signal); + }); + + it('shows a toast error message when an error occurs in the response', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCaseConnectors'); + spyOnGetCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + const { waitForNextUpdate } = renderHook(() => useGetCaseConnectors(caseId), { + wrapper: appMockRender.AppWrapper, + }); + + await waitForNextUpdate(); + expect(addError).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_connectors.tsx b/x-pack/plugins/cases/public/containers/use_get_case_connectors.tsx new file mode 100644 index 00000000000000..fa7920a8c35530 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_case_connectors.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 { useQuery } from '@tanstack/react-query'; +import * as i18n from './translations'; +import { getCaseConnectors } from './api'; +import type { ServerError } from '../types'; +import { casesQueriesKeys } from './constants'; +import { useCasesToast } from '../common/use_cases_toast'; +import type { CaseConnectors } from './types'; + +// 30 seconds +const STALE_TIME = 1000 * 30; + +export const useGetCaseConnectors = (caseId: string) => { + const { showErrorToast } = useCasesToast(); + + return useQuery( + casesQueriesKeys.caseConnectors(caseId), + () => { + const abortCtrlRef = new AbortController(); + return getCaseConnectors(caseId, abortCtrlRef.signal); + }, + { + staleTime: STALE_TIME, + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_TITLE }); + }, + } + ); +}; + +export type UseGetCaseConnectors = ReturnType; diff --git a/x-pack/plugins/cases/server/client/user_actions/connectors.ts b/x-pack/plugins/cases/server/client/user_actions/connectors.ts index d3079d466e6627..e5e3a07830a3d2 100644 --- a/x-pack/plugins/cases/server/client/user_actions/connectors.ts +++ b/x-pack/plugins/cases/server/client/user_actions/connectors.ts @@ -55,6 +55,7 @@ export const getConnectors = async ( connectors, latestUserAction, userActionService, + logger, }); return GetCaseConnectorsResponseRt.encode(results); @@ -121,23 +122,39 @@ const getConnectorsInfo = async ({ latestUserAction, actionsClient, userActionService, + logger, }: { caseId: string; connectors: CaseConnectorActivity[]; latestUserAction?: SavedObject; actionsClient: PublicMethodsOf; userActionService: CaseUserActionService; + logger: CasesClientArgs['logger']; }): Promise => { const connectorIds = connectors.map((connector) => connector.connectorId); const [pushInfo, actionConnectors] = await Promise.all([ getEnrichedPushInfo({ caseId, activity: connectors, userActionService }), - actionsClient.getBulk(connectorIds), + await getActionConnectors(actionsClient, logger, connectorIds), ]); return createConnectorInfoResult({ actionConnectors, connectors, pushInfo, latestUserAction }); }; +const getActionConnectors = async ( + actionsClient: PublicMethodsOf, + logger: CasesClientArgs['logger'], + ids: string[] +): Promise => { + try { + return await actionsClient.getBulk(ids); + } catch (error) { + // silent error and log it + logger.error(`Failed to retrieve action connectors in the get case connectors route: ${error}`); + return []; + } +}; + interface PushDetails { connectorId: string; externalService: CaseExternalServiceBasic; @@ -253,10 +270,12 @@ const createConnectorInfoResult = ({ latestUserAction?: SavedObject; }) => { const results: GetCaseConnectorsResponse = {}; + const actionConnectorsMap = new Map( + actionConnectors.map((actionConnector) => [actionConnector.id, { ...actionConnector }]) + ); - for (let i = 0; i < connectors.length; i++) { - const connectorDetails = actionConnectors[i]; - const aggregationConnector = connectors[i]; + for (const aggregationConnector of connectors) { + const connectorDetails = actionConnectorsMap.get(aggregationConnector.connectorId); const connector = getConnectorInfoFromSavedObject(aggregationConnector.fields); const latestUserActionCreatedAt = getDate(latestUserAction?.attributes.created_at); @@ -271,7 +290,7 @@ const createConnectorInfoResult = ({ results[connector.id] = { ...connector, - name: connectorDetails.name, + name: connectorDetails?.name ?? connector.name, push: { needsToBePushed, hasBeenPushed: hasBeenPushed(enrichedPushInfo), diff --git a/x-pack/plugins/security_solution/cypress/e2e/cases/connector_options.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/cases/connector_options.cy.ts index 03d7d9634f830c..6d07ba6acf5636 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/cases/connector_options.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/cases/connector_options.cy.ts @@ -73,9 +73,7 @@ describe('Cases connector incident fields', () => { cy.get(CONNECTOR_TITLE).should('have.text', getIbmResilientConnectorOptions().title); cy.get(CONNECTOR_CARD_DETAILS).should( 'have.text', - `${ - getIbmResilientConnectorOptions().title - }Incident Types: ${getIbmResilientConnectorOptions().incidentTypes.join(', ')}Severity: ${ + `Incident Types: ${getIbmResilientConnectorOptions().incidentTypes.join(', ')}Severity: ${ getIbmResilientConnectorOptions().severity }` ); diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 1874d7f816eda2..fcd8b60557fc17 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -43,9 +43,9 @@ export const CASES_TAGS = (tagName: string) => { export const CASE_USER_ACTION = '[data-test-subj="user-action-markdown"]'; -export const CONNECTOR_CARD_DETAILS = '[data-test-subj="connector-card"]'; +export const CONNECTOR_CARD_DETAILS = '[data-test-subj="connector-card-details"]'; -export const CONNECTOR_TITLE = '[data-test-subj="connector-card"] p.euiTitle'; +export const CONNECTOR_TITLE = '[data-test-subj="connector-card-title"]'; export const DELETE_CASE_CONFIRM_BUTTON = '[data-test-subj="confirmModalConfirmButton"]'; diff --git a/x-pack/test/cases_api_integration/common/lib/connectors.ts b/x-pack/test/cases_api_integration/common/lib/connectors.ts index 4948495ddf665e..3d0d7b31bc7d1b 100644 --- a/x-pack/test/cases_api_integration/common/lib/connectors.ts +++ b/x-pack/test/cases_api_integration/common/lib/connectors.ts @@ -9,10 +9,7 @@ import getPort from 'get-port'; import http from 'http'; import type SuperTest from 'supertest'; -import { - CASES_INTERNAL_URL, - CASE_CONFIGURE_CONNECTORS_URL, -} from '@kbn/cases-plugin/common/constants'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '@kbn/cases-plugin/common/constants'; import { CasesConfigureResponse, CaseConnector, @@ -20,6 +17,7 @@ import { CasePostRequest, CaseResponse, GetCaseConnectorsResponse, + getCaseConnectorsUrl, } from '@kbn/cases-plugin/common/api'; import { ActionResult, FindActionResult } from '@kbn/actions-plugin/server/types'; import { User } from './authentication/types'; @@ -316,7 +314,7 @@ export const getConnectors = async ({ auth?: { user: User; space: string | null }; }): Promise => { const { body: connectors } = await supertest - .get(`${getSpaceUrlPrefix(auth.space)}${CASES_INTERNAL_URL}/${caseId}/_connectors`) + .get(`${getSpaceUrlPrefix(auth.space)}${getCaseConnectorsUrl(caseId)}`) .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_connectors.ts index 3d1b59a2018abd..e5b8dac8d77e49 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_connectors.ts @@ -8,7 +8,12 @@ import http from 'http'; import expect from '@kbn/expect'; -import { ActionTypes, CaseSeverity, ConnectorTypes } from '@kbn/cases-plugin/common/api'; +import { + ActionTypes, + CaseSeverity, + CaseStatuses, + ConnectorTypes, +} from '@kbn/cases-plugin/common/api'; import { globalRead, noKibanaPrivileges, @@ -246,7 +251,7 @@ export default ({ getService }: FtrProviderContext): void => { connectorId: connector.id, }); - const pachedCase = await createComment({ + const patched = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, @@ -269,8 +274,8 @@ export default ({ getService }: FtrProviderContext): void => { params: { cases: [ { - id: pachedCase.id, - version: pachedCase.version, + id: patched.id, + version: patched.version, connector: { id: serviceNow2.id, name: 'ServiceNow 2 Connector', @@ -290,7 +295,7 @@ export default ({ getService }: FtrProviderContext): void => { await pushCase({ supertest, - caseId: pachedCase.id, + caseId: patched.id, connectorId: serviceNow2.id, }); @@ -315,11 +320,14 @@ export default ({ getService }: FtrProviderContext): void => { expect(connectors[serviceNow2.id].push.externalService?.connector_name).to.not.eql( connector.name ); + expect(connectors[serviceNow2.id].push.externalService?.connector_id).to.not.eql( + connector.id + ); }); }); - describe('latestPushDate', () => { - it('does not set latestPushDate or oldestPushDate when the connector has not been used to push', async () => { + describe('latestUserActionPushDate', () => { + it('does not set latestUserActionPushDate or oldestPushDate when the connector has not been used to push', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, serviceNowSimulatorURL, @@ -334,7 +342,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(connectors[connector.id].push.oldestUserActionPushDate).to.be(undefined); }); - it('sets latestPushDate to the most recent push date and oldestPushDate to the first push date', async () => { + it('sets latestUserActionPushDate to the most recent push date and oldestPushDate to the first push date', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, serviceNowSimulatorURL, @@ -506,6 +514,39 @@ export default ({ getService }: FtrProviderContext): void => { expect(connectors[connector.id].push.needsToBePushed).to.be(false); }); + it('sets needs to push to false when the status of a case was changed after the last push', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + serviceNowSimulatorURL, + actionsRemover, + }); + + const pushedCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: pushedCase.id, + version: pushedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const connectors = await getConnectors({ caseId: postedCase.id, supertest }); + + expect(Object.keys(connectors).length).to.be(1); + expect(connectors[connector.id].id).to.be(connector.id); + expect(connectors[connector.id].push.needsToBePushed).to.be(false); + }); + it('sets needs to push to false the service now connector and true for jira', async () => { const { postedCase, connector: serviceNowConnector } = await createCaseWithConnector({ supertest, From 4e6c3f65406a38be4473c80e1c707e3a98e181f0 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Tue, 31 Jan 2023 16:35:01 +0000 Subject: [PATCH 02/59] [Fleet] Bugfix: Apply namespace from agent policy if there is one when adding integration (#149949) ## Summary Closes #149919 Apply the default namespace of the agent policy if there is one when adding an integration to a policy. https://user-images.githubusercontent.com/3315046/215801897-60754173-167e-4522-91ab-0566e6c83eb5.mp4 --- .../single_page_layout/hooks/form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 24277e464582cb..5bb69a73b3014d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -204,7 +204,7 @@ export function useOnSubmit({ packageToPackagePolicy( packageInfo, agentPolicy?.id || '', - DEFAULT_PACKAGE_POLICY.namespace, + agentPolicy?.namespace || DEFAULT_PACKAGE_POLICY.namespace, DEFAULT_PACKAGE_POLICY.name || incrementedName, DEFAULT_PACKAGE_POLICY.description, integrationToEnable From fc4d90dd21399219bd2e3033b89f7027bb31426a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Tue, 31 Jan 2023 17:47:47 +0100 Subject: [PATCH 03/59] [Security Solution] Cypress tests for Artifact Tabs in Policy Details page RBAC (#149324) ## Summary Adds Cypress tests for RBAC functionality on Artifact Tabs in the Policy Details page. ![image](https://user-images.githubusercontent.com/39014407/214069189-40b61a0d-2159-4048-a59f-4697d89507de.png) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../e2e/artifact_tabs_in_policy_details.cy.ts | 246 ++++++++++++++++++ .../management/cypress/e2e/artifacts.cy.ts | 66 +---- .../cypress/fixtures/artifacts_page.ts | 133 ++++++++-- .../management/cypress/tasks/artifacts.ts | 93 +++++++ .../cypress/tasks/load_endpoint_data.ts | 28 ++ .../public/management/cypress/tasks/login.ts | 61 +++-- .../cypress/tasks/perform_user_actions.ts | 38 +++ .../public/management/cypress/tsconfig.json | 3 +- .../view/components/blocklist_form.tsx | 1 + .../endpoint_security_policy_manager.ts | 10 +- 10 files changed, 565 insertions(+), 114 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/artifact_tabs_in_policy_details.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/load_endpoint_data.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/perform_user_actions.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifact_tabs_in_policy_details.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifact_tabs_in_policy_details.cy.ts new file mode 100644 index 00000000000000..a74eba6274034e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifact_tabs_in_policy_details.cy.ts @@ -0,0 +1,246 @@ +/* + * 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 { getEndpointSecurityPolicyManager } from '../../../../scripts/endpoint/common/roles_users/endpoint_security_policy_manager'; +import { getArtifactsListTestsData } from '../fixtures/artifacts_page'; +import { + createPerPolicyArtifact, + createArtifactList, + removeAllArtifacts, + removeExceptionsList, + yieldFirstPolicyID, +} from '../tasks/artifacts'; +import { loadEndpointDataForEventFiltersIfNeeded } from '../tasks/load_endpoint_data'; +import { login, loginWithCustomRole, loginWithRole, ROLE } from '../tasks/login'; +import { performUserActions } from '../tasks/perform_user_actions'; + +const loginWithPrivilegeAll = () => { + loginWithRole(ROLE.endpoint_security_policy_manager); +}; + +const loginWithPrivilegeRead = (privilegePrefix: string) => { + const roleWithArtifactReadPrivilege = getRoleWithArtifactReadPrivilege(privilegePrefix); + loginWithCustomRole('roleWithArtifactReadPrivilege', roleWithArtifactReadPrivilege); +}; + +const loginWithPrivilegeNone = (privilegePrefix: string) => { + const roleWithoutArtifactPrivilege = getRoleWithoutArtifactPrivilege(privilegePrefix); + loginWithCustomRole('roleWithoutArtifactPrivilege', roleWithoutArtifactPrivilege); +}; + +const getRoleWithArtifactReadPrivilege = (privilegePrefix: string) => { + const endpointSecurityPolicyManagerRole = getEndpointSecurityPolicyManager(); + + return { + ...endpointSecurityPolicyManagerRole, + kibana: [ + { + ...endpointSecurityPolicyManagerRole.kibana[0], + feature: { + ...endpointSecurityPolicyManagerRole.kibana[0].feature, + siem: [ + ...endpointSecurityPolicyManagerRole.kibana[0].feature.siem.filter( + (privilege) => privilege !== `${privilegePrefix}all` + ), + `${privilegePrefix}read`, + ], + }, + }, + ], + }; +}; + +const getRoleWithoutArtifactPrivilege = (privilegePrefix: string) => { + const endpointSecurityPolicyManagerRole = getEndpointSecurityPolicyManager(); + + return { + ...endpointSecurityPolicyManagerRole, + kibana: [ + { + ...endpointSecurityPolicyManagerRole.kibana[0], + feature: { + ...endpointSecurityPolicyManagerRole.kibana[0].feature, + siem: endpointSecurityPolicyManagerRole.kibana[0].feature.siem.filter( + (privilege) => privilege !== `${privilegePrefix}all` + ), + }, + }, + ], + }; +}; + +const visitArtifactTab = (tabId: string) => { + visitPolicyDetailsPage(); + cy.get(`#${tabId}`).click(); +}; + +const visitPolicyDetailsPage = () => { + cy.visit('/app/security/administration/policy'); + cy.getBySel('policyNameCellLink').eq(0).click({ force: true }); + cy.getBySel('policyDetailsPage').should('exist'); + cy.get('#settings').should('exist'); // waiting for Policy Settings tab +}; + +describe('Artifact tabs in Policy Details page', () => { + before(() => { + login(); + loadEndpointDataForEventFiltersIfNeeded(); + }); + + after(() => { + login(); + removeAllArtifacts(); + }); + + for (const testData of getArtifactsListTestsData()) { + beforeEach(() => { + login(); + removeExceptionsList(testData.createRequestBody.list_id); + }); + + describe(`${testData.title} tab`, () => { + it(`[NONE] User cannot see the tab for ${testData.title}`, () => { + loginWithPrivilegeNone(testData.privilegePrefix); + visitPolicyDetailsPage(); + + cy.get(`#${testData.tabId}`).should('not.exist'); + }); + + context(`Given there are no ${testData.title} entries`, () => { + it(`[READ] User CANNOT add ${testData.title} artifact`, () => { + loginWithPrivilegeRead(testData.privilegePrefix); + visitArtifactTab(testData.tabId); + + cy.getBySel('policy-artifacts-empty-unexisting').should('exist'); + + cy.getBySel('unexisting-manage-artifacts-button').should('not.exist'); + }); + + it(`[ALL] User can add ${testData.title} artifact`, () => { + loginWithPrivilegeAll(); + visitArtifactTab(testData.tabId); + + cy.getBySel('policy-artifacts-empty-unexisting').should('exist'); + + cy.getBySel('unexisting-manage-artifacts-button').should('exist').click(); + + const { formActions, checkResults } = testData.create; + + performUserActions(formActions); + + // Add a per policy artifact - but not assign it to any policy + cy.get('[data-test-subj$="-perPolicy"]').click(); // test-subjects are generated in different formats, but all ends with -perPolicy + cy.getBySel(`${testData.pagePrefix}-flyout-submitButton`).click(); + + // Check new artifact is in the list + for (const checkResult of checkResults) { + cy.getBySel(checkResult.selector).should('have.text', checkResult.value); + } + + cy.getBySel('policyDetailsPage').should('not.exist'); + cy.getBySel('backToOrigin').contains(/^Back to .+ policy$/); + + cy.getBySel('backToOrigin').click(); + cy.getBySel('policyDetailsPage').should('exist'); + }); + }); + + context(`Given there are no assigned ${testData.title} entries`, () => { + beforeEach(() => { + login(); + createArtifactList(testData.createRequestBody.list_id); + createPerPolicyArtifact(testData.artifactName, testData.createRequestBody); + }); + + it(`[READ] User CANNOT Manage or Assign ${testData.title} artifacts`, () => { + loginWithPrivilegeRead(testData.privilegePrefix); + visitArtifactTab(testData.tabId); + + cy.getBySel('policy-artifacts-empty-unassigned').should('exist'); + + cy.getBySel('unassigned-manage-artifacts-button').should('not.exist'); + cy.getBySel('unassigned-assign-artifacts-button').should('not.exist'); + }); + + it(`[ALL] User can Manage and Assign ${testData.title} artifacts`, () => { + loginWithPrivilegeAll(); + visitArtifactTab(testData.tabId); + + cy.getBySel('policy-artifacts-empty-unassigned').should('exist'); + + // Manage artifacts + cy.getBySel('unassigned-manage-artifacts-button').should('exist').click(); + cy.location('pathname').should( + 'equal', + `/app/security/administration/${testData.urlPath}` + ); + cy.getBySel('backToOrigin').click(); + + // Assign artifacts + cy.getBySel('unassigned-assign-artifacts-button').should('exist').click(); + + cy.getBySel('artifacts-assign-flyout').should('exist'); + cy.getBySel('artifacts-assign-confirm-button').should('be.disabled'); + + cy.getBySel(`${testData.artifactName}_checkbox`).click(); + cy.getBySel('artifacts-assign-confirm-button').click(); + }); + }); + + context(`Given there are assigned ${testData.title} entries`, () => { + beforeEach(() => { + login(); + createArtifactList(testData.createRequestBody.list_id); + yieldFirstPolicyID().then((policyID) => { + createPerPolicyArtifact(testData.artifactName, testData.createRequestBody, policyID); + }); + }); + + it(`[READ] User can see ${testData.title} artifacts but CANNOT assign or remove from policy`, () => { + loginWithPrivilegeRead(testData.privilegePrefix); + visitArtifactTab(testData.tabId); + + // List of artifacts + cy.getBySel('artifacts-collapsed-list-card').should('have.length', 1); + cy.getBySel('artifacts-collapsed-list-card-header-titleHolder').contains( + testData.artifactName + ); + + // Cannot assign artifacts + cy.getBySel('artifacts-assign-button').should('not.exist'); + + // Cannot remove from policy + cy.getBySel('artifacts-collapsed-list-card-header-actions-button').click(); + cy.getBySel('remove-from-policy-action').should('not.exist'); + }); + + it(`[ALL] User can see ${testData.title} artifacts and can assign or remove artifacts from policy`, () => { + loginWithPrivilegeAll(); + visitArtifactTab(testData.tabId); + + // List of artifacts + cy.getBySel('artifacts-collapsed-list-card').should('have.length', 1); + cy.getBySel('artifacts-collapsed-list-card-header-titleHolder').contains( + testData.artifactName + ); + + // Assign artifacts + cy.getBySel('artifacts-assign-button').should('exist').click(); + cy.getBySel('artifacts-assign-flyout').should('exist'); + cy.getBySel('artifacts-assign-cancel-button').click(); + + // Remove from policy + cy.getBySel('artifacts-collapsed-list-card-header-actions-button').click(); + cy.getBySel('remove-from-policy-action').click(); + cy.getBySel('confirmModalConfirmButton').click(); + + cy.contains('Successfully removed'); + }); + }); + }); + } +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts.cy.ts index a094c7a4988972..166d5065145536 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts.cy.ts @@ -5,27 +5,12 @@ * 2.0. */ -import { isEmpty } from 'lodash'; -import { - ENDPOINT_ARTIFACT_LIST_IDS, - EXCEPTION_LIST_URL, -} from '@kbn/securitysolution-list-constants'; -import { BASE_ENDPOINT_ROUTE } from '../../../../common/endpoint/constants'; import { login, loginWithRole, ROLE } from '../tasks/login'; -import { type FormAction, getArtifactsListTestsData } from '../fixtures/artifacts_page'; -import { runEndpointLoaderScript } from '../tasks/run_endpoint_loader'; - -const removeAllArtifacts = () => { - for (const listId of ENDPOINT_ARTIFACT_LIST_IDS) { - cy.request({ - method: 'DELETE', - url: `${EXCEPTION_LIST_URL}?list_id=${listId}&namespace_type=agnostic`, - headers: { 'kbn-xsrf': 'kibana' }, - failOnStatusCode: false, - }); - } -}; +import { getArtifactsListTestsData } from '../fixtures/artifacts_page'; +import { removeAllArtifacts } from '../tasks/artifacts'; +import { performUserActions } from '../tasks/perform_user_actions'; +import { loadEndpointDataForEventFiltersIfNeeded } from '../tasks/load_endpoint_data'; const loginWithWriteAccess = (url: string) => { loginWithRole(ROLE.analyst_hunter); @@ -42,41 +27,6 @@ const loginWithoutAccess = (url: string) => { cy.visit(url); }; -// Checks for Endpoint data and creates it if needed -const loadEndpointDataForEventFiltersIfNeeded = () => { - cy.request({ - method: 'POST', - url: `${BASE_ENDPOINT_ROUTE}/suggestions/eventFilters`, - body: { - field: 'agent.type', - query: '', - }, - headers: { 'kbn-xsrf': 'kibana' }, - failOnStatusCode: false, - }).then(({ body }) => { - if (isEmpty(body)) { - runEndpointLoaderScript(); - } - }); -}; - -const runAction = (action: FormAction) => { - let element; - if (action.customSelector) { - element = cy.get(action.customSelector); - } else { - element = cy.getBySel(action.selector || ''); - } - - if (action.type === 'click') { - element.click(); - } else if (action.type === 'input') { - element.type(action.value || ''); - } else if (action.type === 'clear') { - element.clear(); - } -}; - describe('Artifacts pages', () => { before(() => { login(); @@ -117,9 +67,7 @@ describe('Artifacts pages', () => { // Opens add flyout cy.getBySel(`${testData.pagePrefix}-emptyState-addButton`).click(); - for (const formAction of testData.create.formActions) { - runAction(formAction); - } + performUserActions(testData.create.formActions); // Submit create artifact form cy.getBySel(`${testData.pagePrefix}-flyout-submitButton`).click(); @@ -153,9 +101,7 @@ describe('Artifacts pages', () => { cy.getBySel(`${testData.pagePrefix}-card-header-actions-button`).click(); cy.getBySel(`${testData.pagePrefix}-card-cardEditAction`).click(); - for (const formAction of testData.update.formActions) { - runAction(formAction); - } + performUserActions(testData.update.formActions); // Submit edit artifact form cy.getBySel(`${testData.pagePrefix}-flyout-submitButton`).click(); diff --git a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts index 5577879af0241b..ec99c404cc62e7 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts @@ -5,31 +5,49 @@ * 2.0. */ -import type { ArtifactElasticsearchProperties } from '@kbn/fleet-plugin/server/services'; -import type { TranslatedExceptionListItem } from '../../../../server/endpoint/schemas'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import type { FormAction } from '../tasks/perform_user_actions'; -export interface ArtifactResponseType { - _index: string; - _id: string; - _score: number; - _source: ArtifactElasticsearchProperties; -} +interface FormEditingDescription { + formActions: FormAction[]; -export interface ArtifactBodyType { - entries: TranslatedExceptionListItem[]; + checkResults: Array<{ + selector: string; + value: string; + }>; } -export interface FormAction { - type: string; - selector?: string; - customSelector?: string; - value?: string; +interface ArtifactsFixtureType { + title: string; + pagePrefix: string; + tabId: string; + artifactName: string; + privilegePrefix: string; + urlPath: string; + emptyState: string; + + create: FormEditingDescription; + update: FormEditingDescription; + + delete: { + confirmSelector: string; + card: string; + }; + + createRequestBody: { + list_id: string; + entries: object[]; + os_types: string[]; + }; } -export const getArtifactsListTestsData = () => [ +export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ { title: 'Trusted applications', pagePrefix: 'trustedAppsListPage', + tabId: 'trustedApps', + artifactName: 'Trusted application name', + privilegePrefix: 'trusted_applications_', create: { formActions: [ { @@ -122,13 +140,40 @@ export const getArtifactsListTestsData = () => [ confirmSelector: 'trustedAppsListPage-deleteModal-submitButton', card: 'trustedAppsListPage-card', }, - pageObject: 'trustedApplications', urlPath: 'trusted_apps', emptyState: 'trustedAppsListPage-emptyState', + + createRequestBody: { + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, + entries: [ + { + entries: [ + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'abcd', + }, + ], + field: 'process.Ext.code_signature', + type: 'nested', + }, + ], + os_types: ['windows'], + }, }, { title: 'Event Filters', pagePrefix: 'EventFiltersListPage', + tabId: 'eventFilters', + artifactName: 'Event filter name', + privilegePrefix: 'event_filters_', create: { formActions: [ { @@ -222,13 +267,28 @@ export const getArtifactsListTestsData = () => [ confirmSelector: 'EventFiltersListPage-deleteModal-submitButton', card: 'EventFiltersListPage-card', }, - pageObject: 'eventFilters', urlPath: 'event_filters', emptyState: 'EventFiltersListPage-emptyState', + + createRequestBody: { + list_id: ENDPOINT_ARTIFACT_LISTS.eventFilters.id, + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: '1.2.3.4', + }, + ], + os_types: ['windows'], + }, }, { title: 'Blocklist', pagePrefix: 'blocklistPage', + tabId: 'blocklists', + artifactName: 'Blocklist name', + privilegePrefix: 'blocklist_', create: { formActions: [ { @@ -330,13 +390,34 @@ export const getArtifactsListTestsData = () => [ confirmSelector: 'blocklistDeletionConfirm', card: 'blocklistCard', }, - pageObject: 'blocklist', urlPath: 'blocklist', emptyState: 'blocklistPage-emptyState', + + createRequestBody: { + list_id: ENDPOINT_ARTIFACT_LISTS.blocklists.id, + entries: [ + { + field: 'file.Ext.code_signature', + entries: [ + { + field: 'subject_name', + value: ['wegwergwegw'], + type: 'match_any', + operator: 'included', + }, + ], + type: 'nested', + }, + ], + os_types: ['windows'], + }, }, { title: 'Host isolation exceptions', pagePrefix: 'hostIsolationExceptionsListPage', + tabId: 'hostIsolationExceptions', + artifactName: 'Host Isolation exception name', + privilegePrefix: 'host_isolation_exceptions_', create: { formActions: [ { @@ -411,8 +492,20 @@ export const getArtifactsListTestsData = () => [ confirmSelector: 'hostIsolationExceptionsDeletionConfirm', card: 'hostIsolationExceptionsCard', }, - pageObject: 'hostIsolationExceptions', urlPath: 'host_isolation_exceptions', emptyState: 'hostIsolationExceptionsListPage-emptyState', + + createRequestBody: { + list_id: ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id, + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: '1.2.3.4', + }, + ], + os_types: ['windows', 'linux', 'macos'], + }, }, ]; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts new file mode 100644 index 00000000000000..53b191fbe3cfbd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts @@ -0,0 +1,93 @@ +/* + * 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 { PACKAGE_POLICY_API_ROOT } from '@kbn/fleet-plugin/common'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { + ENDPOINT_ARTIFACT_LISTS, + ENDPOINT_ARTIFACT_LIST_IDS, + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '@kbn/securitysolution-list-constants'; + +const API_HEADER = { 'kbn-xsrf': 'kibana' }; + +export const removeAllArtifacts = () => { + for (const listId of ENDPOINT_ARTIFACT_LIST_IDS) { + removeExceptionsList(listId); + } +}; + +export const removeExceptionsList = (listId: string) => { + cy.request({ + method: 'DELETE', + url: `${EXCEPTION_LIST_URL}?list_id=${listId}&namespace_type=agnostic`, + headers: API_HEADER, + failOnStatusCode: false, + }).then(({ status }) => { + expect(status).to.be.oneOf([200, 404]); // should either be success or not found + }); +}; + +const ENDPOINT_ARTIFACT_LIST_TYPES = { + [ENDPOINT_ARTIFACT_LISTS.trustedApps.id]: ExceptionListTypeEnum.ENDPOINT, + [ENDPOINT_ARTIFACT_LISTS.eventFilters.id]: ExceptionListTypeEnum.ENDPOINT_EVENTS, + [ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id]: + ExceptionListTypeEnum.ENDPOINT_HOST_ISOLATION_EXCEPTIONS, + [ENDPOINT_ARTIFACT_LISTS.blocklists.id]: ExceptionListTypeEnum.ENDPOINT_BLOCKLISTS, +}; + +export const createArtifactList = (listId: string) => { + cy.request({ + method: 'POST', + url: EXCEPTION_LIST_URL, + headers: API_HEADER, + body: { + name: listId, + description: 'This is a test list', + list_id: listId, + type: ENDPOINT_ARTIFACT_LIST_TYPES[listId], + namespace_type: 'agnostic', + }, + }).then((response) => { + expect(response.status).to.eql(200); + expect(response.body.list_id).to.eql(listId); + expect(response.body.type).to.eql(ENDPOINT_ARTIFACT_LIST_TYPES[listId]); + }); +}; + +export const createPerPolicyArtifact = (name: string, body: object, policyId?: 'all' | string) => { + cy.request({ + method: 'POST', + url: EXCEPTION_LIST_ITEM_URL, + + headers: API_HEADER, + body: { + name, + description: '', + type: 'simple', + namespace_type: 'agnostic', + ...body, + ...(policyId ? { tags: [`policy:${policyId}`] } : {}), + }, + }).then((response) => { + expect(response.status).to.eql(200); + expect(response.body.name).to.eql(name); + }); +}; + +export const yieldFirstPolicyID = () => { + return cy + .request({ + method: 'GET', + url: `${PACKAGE_POLICY_API_ROOT}?page=1&perPage=1&kuery=ingest-package-policies.package.name: endpoint`, + }) + .then(({ body }) => { + expect(body.items.length).to.be.least(1); + return body.items[0].id; + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/load_endpoint_data.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/load_endpoint_data.ts new file mode 100644 index 00000000000000..5e6650404e29a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/load_endpoint_data.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { BASE_ENDPOINT_ROUTE } from '../../../../common/endpoint/constants'; +import { runEndpointLoaderScript } from './run_endpoint_loader'; + +// Checks for Endpoint data and creates it if needed +export const loadEndpointDataForEventFiltersIfNeeded = () => { + cy.request({ + method: 'POST', + url: `${BASE_ENDPOINT_ROUTE}/suggestions/eventFilters`, + body: { + field: 'agent.type', + query: '', + }, + headers: { 'kbn-xsrf': 'kibana' }, + failOnStatusCode: false, + }).then(({ body }) => { + if (isEmpty(body)) { + runEndpointLoaderScript(); + } + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts index 2278e9dad6ac2f..7d13090ac0ff8c 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts @@ -19,6 +19,7 @@ import { getSocManager } from '../../../../scripts/endpoint/common/roles_users/s import { getPlatformEngineer } from '../../../../scripts/endpoint/common/roles_users/platform_engineer'; import { getEndpointOperationsAnalyst } from '../../../../scripts/endpoint/common/roles_users/endpoint_operations_analyst'; import { getEndpointSecurityPolicyManager } from '../../../../scripts/endpoint/common/roles_users/endpoint_security_policy_manager'; +import { getDetectionsEngineer } from '../../../../scripts/endpoint/common/roles_users/detections_engineer'; export enum ROLE { t1_analyst = 't1Analyst', @@ -32,7 +33,7 @@ export enum ROLE { endpoint_security_policy_manager = 'endpointSecurityPolicyManager', } -export const rolesMapping: { [id: string]: Omit } = { +export const rolesMapping: { [key in ROLE]: Omit } = { t1Analyst: getT1Analyst(), t2Analyst: getT2Analyst(), hunter: getHunter(), @@ -41,6 +42,7 @@ export const rolesMapping: { [id: string]: Omit } = { platformEngineer: getPlatformEngineer(), endpointOperationsAnalyst: getEndpointOperationsAnalyst(), endpointSecurityPolicyManager: getEndpointSecurityPolicyManager(), + detectionsEngineer: getDetectionsEngineer(), }; /** * Credentials in the `kibana.dev.yml` config file will be used to authenticate @@ -77,6 +79,13 @@ const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; */ const LOGIN_API_ENDPOINT = '/internal/security/login'; +const API_AUTH = { + user: Cypress.env(ELASTICSEARCH_USERNAME), + pass: Cypress.env(ELASTICSEARCH_PASSWORD), +}; + +const API_HEADERS = { 'kbn-xsrf': 'cypress' }; + /** * cy.visit will default to the baseUrl which uses the default kibana test user * This function will override that functionality in cy.visit by building the baseUrl @@ -85,7 +94,7 @@ const LOGIN_API_ENDPOINT = '/internal/security/login'; * @param role string role/user to log in with * @param route string route to visit */ -export const getUrlWithRoute = (role: ROLE, route: string) => { +export const getUrlWithRoute = (role: string, route: string) => { const url = Cypress.config().baseUrl; const kibana = new URL(String(url)); const theUrl = `${Url.format({ @@ -138,38 +147,32 @@ export const getCurlScriptEnvVars = () => ({ }); export const createRoleAndUser = (role: ROLE) => { + createCustomRoleAndUser(role, rolesMapping[role]); +}; + +export const createCustomRoleAndUser = (role: string, rolePrivileges: Omit) => { const env = getCurlScriptEnvVars(); // post the role cy.request({ method: 'PUT', url: `${env.KIBANA_URL}/api/security/role/${role}`, - body: rolesMapping[role], - headers: { - 'kbn-xsrf': 'cypress', - }, - auth: { - user: Cypress.env(ELASTICSEARCH_USERNAME), - pass: Cypress.env(ELASTICSEARCH_PASSWORD), - }, + body: rolePrivileges, + headers: API_HEADERS, + auth: API_AUTH, }); // post the user associated with the role to elasticsearch cy.request({ method: 'POST', url: `${env.KIBANA_URL}/internal/security/users/${role}`, - headers: { - 'kbn-xsrf': 'cypress', - }, + headers: API_HEADERS, body: { username: role, password: Cypress.env(ELASTICSEARCH_PASSWORD), roles: [role], }, - auth: { - user: Cypress.env(ELASTICSEARCH_USERNAME), - pass: Cypress.env(ELASTICSEARCH_PASSWORD), - }, + auth: API_AUTH, }); }; @@ -178,24 +181,14 @@ export const deleteRoleAndUser = (role: ROLE) => { cy.request({ method: 'DELETE', - auth: { - user: Cypress.env(ELASTICSEARCH_USERNAME), - pass: Cypress.env(ELASTICSEARCH_PASSWORD), - }, - headers: { - 'kbn-xsrf': 'cypress', - }, + auth: API_AUTH, + headers: API_HEADERS, url: `${env.KIBANA_URL}/internal/security/users/${role}`, }); cy.request({ method: 'DELETE', - auth: { - user: Cypress.env(ELASTICSEARCH_USERNAME), - pass: Cypress.env(ELASTICSEARCH_PASSWORD), - }, - headers: { - 'kbn-xsrf': 'cypress', - }, + auth: API_AUTH, + headers: API_HEADERS, url: `${env.KIBANA_URL}/api/security/role/${role}`, }); }; @@ -220,7 +213,11 @@ export const loginWithUser = (user: User) => { }; export const loginWithRole = async (role: ROLE) => { - createRoleAndUser(role); + loginWithCustomRole(role, rolesMapping[role]); +}; + +export const loginWithCustomRole = async (role: string, rolePrivileges: Omit) => { + createCustomRoleAndUser(role, rolePrivileges); const theUrl = Url.format({ auth: `${role}:changeme`, username: role, diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/perform_user_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/perform_user_actions.ts new file mode 100644 index 00000000000000..69c7bd369cdeb8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/perform_user_actions.ts @@ -0,0 +1,38 @@ +/* + * 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 ActionTypes = 'click' | 'input' | 'clear'; + +export interface FormAction { + type: ActionTypes; + selector?: string; + customSelector?: string; + value?: string; +} + +export const performUserActions = (actions: FormAction[]) => { + for (const action of actions) { + performAction(action); + } +}; + +const performAction = (action: FormAction) => { + let element; + if (action.customSelector) { + element = cy.get(action.customSelector); + } else { + element = cy.getBySel(action.selector || ''); + } + + if (action.type === 'click') { + element.click(); + } else if (action.type === 'input') { + element.type(action.value || ''); + } else if (action.type === 'clear') { + element.clear(); + } +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json index a99e4af76fe2ed..a385fa4c78ec6e 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json @@ -24,6 +24,7 @@ }, "@kbn/security-plugin", "@kbn/securitysolution-list-constants", - "@kbn/fleet-plugin" + "@kbn/fleet-plugin", + "@kbn/securitysolution-io-ts-list-types", ] } diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx index 8b1ab7dee08541..52b9f95ce591b1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx @@ -540,6 +540,7 @@ export const BlockListForm = memo( onChange={handleOnPolicyChange} isLoading={policiesIsLoading} description={POLICY_SELECT_DESCRIPTION} + data-test-subj={getTestId('effectedPolicies')} /> diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts index 5b57f4e2888bb6..0a9679b8dbfd62 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts @@ -17,7 +17,15 @@ export const getEndpointSecurityPolicyManager: () => Omit = () => ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: ['minimal_all'], + siem: [ + 'blocklist_all', + 'endpoint_list_all', + 'event_filters_all', + 'host_isolation_exceptions_all', + 'minimal_all', + 'policy_management_all', + 'trusted_applications_all', + ], }, }, ], From f33568f74e8269c57556d4f4a1651b8a96c395f3 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 31 Jan 2023 17:48:56 +0100 Subject: [PATCH 04/59] [ftr] split 'x-pack/test/functional_basic/config.ts' into small config files (#149617) ## Summary This PR attempts to fix config duration time warning ``` The following "Functional Tests" configs have durations that exceed the maximum amount of time desired for a single CI job. This is not an error, and if you don't own any of these configs then you can ignore this warning.If you own any of these configs please split them up ASAP and ask Operations if you have questions about how to do that. x-pack/test/functional_basic/config.ts: 38.8 minutes ``` image PR initially splits original test suite into 3 config files based on area: permission, data visualizer and transform. - x-pack/test/functional_basic/apps/ml/data_visualizer/config.ts duration: **19m 24s** (left for later) - x-pack/test/functional_basic/apps/transform/config.ts duration: **18m 14s** -> let's split in 5 configs - x-pack/test/functional_basic/apps/ml/permissions/config.ts. duration: 5m 10s 2nd split round: - x-pack/test/functional_basic/apps/transform/feature_controls/config.ts. duration: 2m 4s - x-pack/test/functional_basic/apps/transform/group1/config.ts duration: **8m 16s** -> let's split in 2 configs - x-pack/test/functional_basic/apps/transform/group2/config.ts. duration: 5m 20s - x-pack/test/functional_basic/apps/transform/group3/config.ts. duration: 5m 12s - x-pack/test/functional_basic/apps/ml/permissions/config.ts. duration: 5m 10s -> let's split in 3 configs (1 test file each) 3rd split round: - x-pack/test/functional_basic/apps/ml/permissions/group1/config.ts. duration: 3m 11s - x-pack/test/functional_basic/apps/ml/permissions/group2/config.ts duration: 3m 42s - x-pack/test/functional_basic/apps/ml/permissions/group3/config.ts duration 2m 14s - x-pack/test/functional_basic/apps/transform/group4/config.ts duration: 4m 43s lets split into 3 configs - x-pack/test/functional_basic/apps/ml/data_visualizer/config.ts duration: **19m 24s** 4th split round: - x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.ts duration: 4m 42s - x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.ts duration: 9m 27s - x-pack/test/functional_basic/apps/ml/data_visualizer/group3/config.ts duration: 7m 39s [Build time ](https://buildkite.com/elastic/kibana-pull-request/builds/103355) is 49m 26sec (55 FTR groups) Currently on-merge pipeline for [main](https://buildkite.com/elastic/kibana-on-merge/builds?branch=main) takes around 1h --- .buildkite/ftr_configs.yml | 18 +++++++- .../creation/index_pattern/config.ts | 20 ++++++++ .../index_pattern}/creation_index_pattern.ts | 4 +- .../transform/creation/index_pattern/index.ts | 37 +++++++++++++++ .../runtime_mappings_saved_search/config.ts | 21 +++++++++ .../creation_runtime_mappings.ts | 6 +-- .../creation_saved_search.ts | 4 +- .../runtime_mappings_saved_search/index.ts | 38 +++++++++++++++ .../transform/{ => edit_clone}/cloning.ts | 4 +- .../apps/transform/edit_clone/config.ts | 20 ++++++++ .../transform/{ => edit_clone}/editing.ts | 4 +- .../apps/transform/edit_clone/index.ts | 38 +++++++++++++++ .../apps/transform/feature_controls/config.ts | 20 ++++++++ .../apps/transform/feature_controls/index.ts | 27 ++++++++++- .../apps/transform/{index.ts => helpers.ts} | 38 --------------- .../transform/{ => permissions}/config.ts | 4 +- .../permissions/full_transform_access.ts | 2 +- .../apps/transform/permissions/index.ts | 27 ++++++++++- .../permissions/read_transform_access.ts | 2 +- .../transform/start_reset_delete/config.ts | 20 ++++++++ .../{ => start_reset_delete}/deleting.ts | 4 +- .../transform/start_reset_delete/index.ts | 39 ++++++++++++++++ .../{ => start_reset_delete}/resetting.ts | 4 +- .../{ => start_reset_delete}/starting.ts | 4 +- .../{config.ts => apps/ml/config.base.ts} | 5 +- .../apps/ml/data_visualizer/group1/config.ts | 22 +++++++++ .../ml/{ => data_visualizer/group1}/index.ts | 10 ++-- .../apps/ml/data_visualizer/group2/config.ts | 22 +++++++++ .../apps/ml/data_visualizer/group2/index.ts | 43 +++++++++++++++++ .../apps/ml/data_visualizer/group3/config.ts | 22 +++++++++ .../apps/ml/data_visualizer/group3/index.ts | 46 +++++++++++++++++++ .../index_data_visualizer_actions_panel.ts | 2 +- .../apps/ml/data_visualizer/index.ts | 29 ------------ .../apps/ml/permissions/config.ts | 22 +++++++++ .../apps/ml/permissions/index.ts | 29 +++++++++++- .../apps/transform/config.base.ts | 32 +++++++++++++ .../creation/index_pattern/config.ts | 23 ++++++++++ .../transform/creation/index_pattern/index.ts | 19 ++++++++ .../runtime_mappings_saved_search/config.ts | 23 ++++++++++ .../runtime_mappings_saved_search/index.ts | 21 +++++++++ .../apps/transform/edit_clone/config.ts | 22 +++++++++ .../apps/transform/{ => edit_clone}/index.ts | 4 +- .../apps/transform/feature_controls/config.ts | 22 +++++++++ .../apps/transform/feature_controls/index.ts | 17 +++++++ .../apps/transform/permissions/config.ts | 22 +++++++++ .../apps/{ => transform/permissions}/index.ts | 10 ++-- .../transform/start_reset_delete/config.ts | 23 ++++++++++ .../transform/start_reset_delete/index.ts | 17 +++++++ 48 files changed, 802 insertions(+), 110 deletions(-) create mode 100644 x-pack/test/functional/apps/transform/creation/index_pattern/config.ts rename x-pack/test/functional/apps/transform/{ => creation/index_pattern}/creation_index_pattern.ts (99%) create mode 100644 x-pack/test/functional/apps/transform/creation/index_pattern/index.ts create mode 100644 x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/config.ts rename x-pack/test/functional/apps/transform/{ => creation/runtime_mappings_saved_search}/creation_runtime_mappings.ts (99%) rename x-pack/test/functional/apps/transform/{ => creation/runtime_mappings_saved_search}/creation_saved_search.ts (99%) create mode 100644 x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/index.ts rename x-pack/test/functional/apps/transform/{ => edit_clone}/cloning.ts (99%) create mode 100644 x-pack/test/functional/apps/transform/edit_clone/config.ts rename x-pack/test/functional/apps/transform/{ => edit_clone}/editing.ts (99%) create mode 100644 x-pack/test/functional/apps/transform/edit_clone/index.ts create mode 100644 x-pack/test/functional/apps/transform/feature_controls/config.ts rename x-pack/test/functional/apps/transform/{index.ts => helpers.ts} (65%) rename x-pack/test/functional/apps/transform/{ => permissions}/config.ts (83%) create mode 100644 x-pack/test/functional/apps/transform/start_reset_delete/config.ts rename x-pack/test/functional/apps/transform/{ => start_reset_delete}/deleting.ts (98%) create mode 100644 x-pack/test/functional/apps/transform/start_reset_delete/index.ts rename x-pack/test/functional/apps/transform/{ => start_reset_delete}/resetting.ts (98%) rename x-pack/test/functional/apps/transform/{ => start_reset_delete}/starting.ts (98%) rename x-pack/test/functional_basic/{config.ts => apps/ml/config.base.ts} (90%) create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.ts rename x-pack/test/functional_basic/apps/ml/{ => data_visualizer/group1}/index.ts (76%) create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.ts create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/group2/index.ts create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/group3/config.ts create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index.ts rename x-pack/test/functional_basic/apps/ml/data_visualizer/{ => group3}/index_data_visualizer_actions_panel.ts (97%) delete mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts create mode 100644 x-pack/test/functional_basic/apps/ml/permissions/config.ts create mode 100644 x-pack/test/functional_basic/apps/transform/config.base.ts create mode 100644 x-pack/test/functional_basic/apps/transform/creation/index_pattern/config.ts create mode 100644 x-pack/test/functional_basic/apps/transform/creation/index_pattern/index.ts create mode 100644 x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/config.ts create mode 100644 x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/index.ts create mode 100644 x-pack/test/functional_basic/apps/transform/edit_clone/config.ts rename x-pack/test/functional_basic/apps/transform/{ => edit_clone}/index.ts (76%) create mode 100644 x-pack/test/functional_basic/apps/transform/feature_controls/config.ts create mode 100644 x-pack/test/functional_basic/apps/transform/feature_controls/index.ts create mode 100644 x-pack/test/functional_basic/apps/transform/permissions/config.ts rename x-pack/test/functional_basic/apps/{ => transform/permissions}/index.ts (50%) create mode 100644 x-pack/test/functional_basic/apps/transform/start_reset_delete/config.ts create mode 100644 x-pack/test/functional_basic/apps/transform/start_reset_delete/index.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 721de7917534d6..52c47e1eb272ba 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -161,7 +161,16 @@ enabled: - x-pack/test/fleet_api_integration/config.ts - x-pack/test/fleet_functional/config.ts - x-pack/test/ftr_apis/security_and_spaces/config.ts - - x-pack/test/functional_basic/config.ts + - x-pack/test/functional_basic/apps/ml/permissions/config.ts + - x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.ts + - x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.ts + - x-pack/test/functional_basic/apps/ml/data_visualizer/group3/config.ts + - x-pack/test/functional_basic/apps/transform/creation/index_pattern/config.ts + - x-pack/test/functional_basic/apps/transform/start_reset_delete/config.ts + - x-pack/test/functional_basic/apps/transform/edit_clone/config.ts + - x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/config.ts + - x-pack/test/functional_basic/apps/transform/permissions/config.ts + - x-pack/test/functional_basic/apps/transform/feature_controls/config.ts - x-pack/test/functional_cors/config.ts - x-pack/test/functional_embedded/config.ts - x-pack/test/functional_enterprise_search/without_host_configured.config.ts @@ -216,7 +225,12 @@ enabled: - x-pack/test/functional/apps/snapshot_restore/config.ts - x-pack/test/functional/apps/spaces/config.ts - x-pack/test/functional/apps/status_page/config.ts - - x-pack/test/functional/apps/transform/config.ts + - x-pack/test/functional/apps/transform/creation/index_pattern/config.ts + - x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/config.ts + - x-pack/test/functional/apps/transform/start_reset_delete/config.ts + - x-pack/test/functional/apps/transform/edit_clone/config.ts + - x-pack/test/functional/apps/transform/permissions/config.ts + - x-pack/test/functional/apps/transform/feature_controls/config.ts - x-pack/test/functional/apps/upgrade_assistant/config.ts - x-pack/test/functional/apps/uptime/config.ts - x-pack/test/functional/apps/visualize/config.ts diff --git a/x-pack/test/functional/apps/transform/creation/index_pattern/config.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/config.ts new file mode 100644 index 00000000000000..40617c64ba398a --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - transform - creation - index pattern', + }, + }; +} diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts similarity index 99% rename from x-pack/test/functional/apps/transform/creation_index_pattern.ts rename to x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts index 5c240b2c0403c4..a35e9759c212a1 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts @@ -7,14 +7,14 @@ import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { GroupByEntry, isLatestTransformTestData, isPivotTransformTestData, LatestTransformTestData, PivotTransformTestData, -} from '.'; +} from '../../helpers'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const canvasElement = getService('canvasElement'); diff --git a/x-pack/test/functional/apps/transform/creation/index_pattern/index.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/index.ts new file mode 100644 index 00000000000000..9e09c4e1c51fac --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('transform - creation - index pattern', function () { + this.tags('transform'); + + before(async () => { + await transform.securityCommon.createTransformRoles(); + await transform.securityCommon.createTransformUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + + await transform.securityCommon.cleanTransformUsers(); + await transform.securityCommon.cleanTransformRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./creation_index_pattern')); + }); +} diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/config.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/config.ts new file mode 100644 index 00000000000000..0b51b78265c250 --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/config.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: + 'Chrome X-Pack UI Functional Tests - transform - creation - runtime mappings & saved search', + }, + }; +} diff --git a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts similarity index 99% rename from x-pack/test/functional/apps/transform/creation_runtime_mappings.ts rename to x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts index 3e595695a3da55..2c456cb91e083b 100644 --- a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts @@ -7,9 +7,9 @@ import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import type { HistogramCharts } from '../../services/transform/wizard'; +import type { HistogramCharts } from '../../../../services/transform/wizard'; import { GroupByEntry, @@ -17,7 +17,7 @@ import { isPivotTransformTestData, LatestTransformTestData, PivotTransformTestData, -} from '.'; +} from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts similarity index 99% rename from x-pack/test/functional/apps/transform/creation_saved_search.ts rename to x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts index ee1d0e02899cc8..60ab3f93ac3a5b 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts @@ -7,14 +7,14 @@ import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { GroupByEntry, isLatestTransformTestData, isPivotTransformTestData, LatestTransformTestData, PivotTransformTestData, -} from '.'; +} from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/index.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/index.ts new file mode 100644 index 00000000000000..943fb97200a7b1 --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/index.ts @@ -0,0 +1,38 @@ +/* + * 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 function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('transform - creation - runtime mappings & saved search', function () { + this.tags('transform'); + + before(async () => { + await transform.securityCommon.createTransformRoles(); + await transform.securityCommon.createTransformUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + + await transform.securityCommon.cleanTransformUsers(); + await transform.securityCommon.cleanTransformRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./creation_saved_search')); + loadTestFile(require.resolve('./creation_runtime_mappings')); + }); +} diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/edit_clone/cloning.ts similarity index 99% rename from x-pack/test/functional/apps/transform/cloning.ts rename to x-pack/test/functional/apps/transform/edit_clone/cloning.ts index 8b03c0b26ee652..b823edadaed830 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/edit_clone/cloning.ts @@ -10,8 +10,8 @@ import { isPivotTransform, TransformPivotConfig, } from '@kbn/transform-plugin/common/types/transform'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { getLatestTransformConfig } from '.'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getLatestTransformConfig } from '../helpers'; interface TestData { type: 'pivot' | 'latest'; diff --git a/x-pack/test/functional/apps/transform/edit_clone/config.ts b/x-pack/test/functional/apps/transform/edit_clone/config.ts new file mode 100644 index 00000000000000..9b3a878496a700 --- /dev/null +++ b/x-pack/test/functional/apps/transform/edit_clone/config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - transform - edit & clone', + }, + }; +} diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/edit_clone/editing.ts similarity index 99% rename from x-pack/test/functional/apps/transform/editing.ts rename to x-pack/test/functional/apps/transform/edit_clone/editing.ts index f96052ab28e186..5f767825c7c315 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/edit_clone/editing.ts @@ -11,8 +11,8 @@ import type { TransformPivotConfig, } from '@kbn/transform-plugin/common/types/transform'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { getLatestTransformConfig, getPivotTransformConfig } from '.'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from '../helpers'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/transform/edit_clone/index.ts b/x-pack/test/functional/apps/transform/edit_clone/index.ts new file mode 100644 index 00000000000000..93dbaa51c396eb --- /dev/null +++ b/x-pack/test/functional/apps/transform/edit_clone/index.ts @@ -0,0 +1,38 @@ +/* + * 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 function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('transform - edit & clone', function () { + this.tags('transform'); + + before(async () => { + await transform.securityCommon.createTransformRoles(); + await transform.securityCommon.createTransformUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + + await transform.securityCommon.cleanTransformUsers(); + await transform.securityCommon.cleanTransformRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./editing')); + }); +} diff --git a/x-pack/test/functional/apps/transform/feature_controls/config.ts b/x-pack/test/functional/apps/transform/feature_controls/config.ts new file mode 100644 index 00000000000000..f8ce309ed52e03 --- /dev/null +++ b/x-pack/test/functional/apps/transform/feature_controls/config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - transform - feature controls', + }, + }; +} diff --git a/x-pack/test/functional/apps/transform/feature_controls/index.ts b/x-pack/test/functional/apps/transform/feature_controls/index.ts index a00054c185438b..987bd36172847e 100644 --- a/x-pack/test/functional/apps/transform/feature_controls/index.ts +++ b/x-pack/test/functional/apps/transform/feature_controls/index.ts @@ -7,8 +7,31 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { - describe('feature controls', function () { +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('transform - feature controls', function () { + this.tags('transform'); + + before(async () => { + await transform.securityCommon.createTransformRoles(); + await transform.securityCommon.createTransformUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + + await transform.securityCommon.cleanTransformUsers(); + await transform.securityCommon.cleanTransformRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); + }); + loadTestFile(require.resolve('./transform_security')); }); } diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/helpers.ts similarity index 65% rename from x-pack/test/functional/apps/transform/index.ts rename to x-pack/test/functional/apps/transform/helpers.ts index e87f72ed98880c..c422ea6dfca0fa 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/helpers.ts @@ -9,45 +9,7 @@ import { TransformLatestConfig, TransformPivotConfig, } from '@kbn/transform-plugin/common/types/transform'; -import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const transform = getService('transform'); - - describe('transform', function () { - this.tags('transform'); - - before(async () => { - await transform.securityCommon.createTransformRoles(); - await transform.securityCommon.createTransformUsers(); - }); - - after(async () => { - // NOTE: Logout needs to happen before anything else to avoid flaky behavior - await transform.securityUI.logout(); - - await transform.securityCommon.cleanTransformUsers(); - await transform.securityCommon.cleanTransformRoles(); - - await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); - - await transform.testResources.resetKibanaTimeZone(); - }); - - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./creation_index_pattern')); - loadTestFile(require.resolve('./creation_saved_search')); - loadTestFile(require.resolve('./creation_runtime_mappings')); - loadTestFile(require.resolve('./cloning')); - loadTestFile(require.resolve('./editing')); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./deleting')); - loadTestFile(require.resolve('./resetting')); - loadTestFile(require.resolve('./starting')); - }); -} export interface ComboboxOption { identifier: string; label: string; diff --git a/x-pack/test/functional/apps/transform/config.ts b/x-pack/test/functional/apps/transform/permissions/config.ts similarity index 83% rename from x-pack/test/functional/apps/transform/config.ts rename to x-pack/test/functional/apps/transform/permissions/config.ts index 17a471848867e6..3771f59d47c615 100644 --- a/x-pack/test/functional/apps/transform/config.ts +++ b/x-pack/test/functional/apps/transform/permissions/config.ts @@ -8,13 +8,13 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], junit: { - reportName: 'Chrome X-Pack UI Functional Tests - Transform', + reportName: 'Chrome X-Pack UI Functional Tests - transform - permissions', }, }; } diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index 59ad6d0d8dfa4d..4969832b3600e0 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getPivotTransformConfig } from '..'; +import { getPivotTransformConfig } from '../helpers'; import { FtrProviderContext } from '../../../ftr_provider_context'; diff --git a/x-pack/test/functional/apps/transform/permissions/index.ts b/x-pack/test/functional/apps/transform/permissions/index.ts index 08f4043a62deee..30936edc877ef0 100644 --- a/x-pack/test/functional/apps/transform/permissions/index.ts +++ b/x-pack/test/functional/apps/transform/permissions/index.ts @@ -7,8 +7,31 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { - describe('permissions', function () { +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('transform - permissions', function () { + this.tags('transform'); + + before(async () => { + await transform.securityCommon.createTransformRoles(); + await transform.securityCommon.createTransformUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + + await transform.securityCommon.cleanTransformUsers(); + await transform.securityCommon.cleanTransformRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); + }); + loadTestFile(require.resolve('./full_transform_access')); loadTestFile(require.resolve('./read_transform_access')); }); diff --git a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts index be808d606fdd61..918cd5c144a84e 100644 --- a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getPivotTransformConfig } from '..'; +import { getPivotTransformConfig } from '../helpers'; import { FtrProviderContext } from '../../../ftr_provider_context'; diff --git a/x-pack/test/functional/apps/transform/start_reset_delete/config.ts b/x-pack/test/functional/apps/transform/start_reset_delete/config.ts new file mode 100644 index 00000000000000..edf34d16785c4c --- /dev/null +++ b/x-pack/test/functional/apps/transform/start_reset_delete/config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - transform - start reset & delete', + }, + }; +} diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/start_reset_delete/deleting.ts similarity index 98% rename from x-pack/test/functional/apps/transform/deleting.ts rename to x-pack/test/functional/apps/transform/start_reset_delete/deleting.ts index ba49a5c0ae7f95..e76ef118fde0dc 100644 --- a/x-pack/test/functional/apps/transform/deleting.ts +++ b/x-pack/test/functional/apps/transform/start_reset_delete/deleting.ts @@ -7,8 +7,8 @@ import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { getLatestTransformConfig, getPivotTransformConfig } from '.'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from '../helpers'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/transform/start_reset_delete/index.ts b/x-pack/test/functional/apps/transform/start_reset_delete/index.ts new file mode 100644 index 00000000000000..1a606339eb82a9 --- /dev/null +++ b/x-pack/test/functional/apps/transform/start_reset_delete/index.ts @@ -0,0 +1,39 @@ +/* + * 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 function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('transform - start reset & delete', function () { + this.tags('transform'); + + before(async () => { + await transform.securityCommon.createTransformRoles(); + await transform.securityCommon.createTransformUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + + await transform.securityCommon.cleanTransformUsers(); + await transform.securityCommon.cleanTransformRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + + await transform.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./deleting')); + loadTestFile(require.resolve('./resetting')); + loadTestFile(require.resolve('./starting')); + }); +} diff --git a/x-pack/test/functional/apps/transform/resetting.ts b/x-pack/test/functional/apps/transform/start_reset_delete/resetting.ts similarity index 98% rename from x-pack/test/functional/apps/transform/resetting.ts rename to x-pack/test/functional/apps/transform/start_reset_delete/resetting.ts index 471f8f7681a202..ef62d9986813de 100644 --- a/x-pack/test/functional/apps/transform/resetting.ts +++ b/x-pack/test/functional/apps/transform/start_reset_delete/resetting.ts @@ -7,8 +7,8 @@ import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { getLatestTransformConfig, getPivotTransformConfig } from '.'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from '../helpers'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/transform/starting.ts b/x-pack/test/functional/apps/transform/start_reset_delete/starting.ts similarity index 98% rename from x-pack/test/functional/apps/transform/starting.ts rename to x-pack/test/functional/apps/transform/start_reset_delete/starting.ts index c2940a95aaece4..1fccec9a9192b3 100644 --- a/x-pack/test/functional/apps/transform/starting.ts +++ b/x-pack/test/functional/apps/transform/start_reset_delete/starting.ts @@ -6,8 +6,8 @@ */ import { TRANSFORM_STATE } from '@kbn/transform-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { getLatestTransformConfig, getPivotTransformConfig } from '.'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from '../helpers'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional_basic/config.ts b/x-pack/test/functional_basic/apps/ml/config.base.ts similarity index 90% rename from x-pack/test/functional_basic/config.ts rename to x-pack/test/functional_basic/apps/ml/config.base.ts index f35ece0ce5d165..fc431b27ea4578 100644 --- a/x-pack/test/functional_basic/config.ts +++ b/x-pack/test/functional_basic/apps/ml/config.base.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile( - require.resolve('../functional/config.base.js') + require.resolve('../../../functional/config.base.js') ); return { @@ -24,10 +24,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { 'xpack.security.authc.api_key.enabled=true', ], }, - testFiles: [require.resolve('./apps')], junit: { ...xpackFunctionalConfig.get('junit'), - reportName: 'Chrome X-Pack UI Functional Tests Basic License', + reportName: 'Chrome X-Pack UI Functional Tests Basic License - ml', }, }; } diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.ts new file mode 100644 index 00000000000000..4f6a1ca9ba3dc3 --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('../../config.base.ts')); + + return { + // default to the ml/config.base.ts + ...baseConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...baseConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - ml - data visualizer - group1', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group1/index.ts similarity index 76% rename from x-pack/test/functional_basic/apps/ml/index.ts rename to x-pack/test/functional_basic/apps/ml/data_visualizer/group1/index.ts index dbdab2cc0a4b24..37295dda128ad0 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group1/index.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('machine learning basic license', function () { + describe('machine learning basic license - data visualizer - group 1', function () { this.tags(['skipFirefox', 'ml']); before(async () => { @@ -34,7 +34,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./data_visualizer')); + // The file data visualizer should work the same as with a trial license + loadTestFile( + require.resolve('../../../../../functional/apps/ml/data_visualizer/file_data_visualizer') + ); }); } diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.ts new file mode 100644 index 00000000000000..0e16eaddb3b3f1 --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('../../config.base.ts')); + + return { + // default to the ml/config.base.ts + ...baseConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...baseConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - ml - data visualizer - group2', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/group2/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group2/index.ts new file mode 100644 index 00000000000000..e1ed6d554a398d --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group2/index.ts @@ -0,0 +1,43 @@ +/* + * 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 function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning basic license - data visualizer - group 2', function () { + this.tags(['skipFirefox', 'ml']); + + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await ml.testResources.deleteSavedSearches(); + + await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + await ml.testResources.deleteIndexPatternByTitle('ft_module_sample_ecommerce'); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + // The data visualizer should work the same as with a trial license, except the missing create actions + // That's why the 'basic' version of 'index_data_visualizer_actions_panel' is loaded here + loadTestFile( + require.resolve('../../../../../functional/apps/ml/data_visualizer/index_data_visualizer') + ); + }); +} diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/config.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/config.ts new file mode 100644 index 00000000000000..eb81a957990009 --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('../../config.base.ts')); + + return { + // default to the ml/config.base.ts + ...baseConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...baseConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - ml - data visualizer - group3', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index.ts new file mode 100644 index 00000000000000..18a5dfaec2d609 --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning basic license - data visualizer - group 3', function () { + this.tags(['skipFirefox', 'ml']); + + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await ml.testResources.deleteSavedSearches(); + + await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + await ml.testResources.deleteIndexPatternByTitle('ft_module_sample_ecommerce'); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + // The data visualizer should work the same as with a trial license, except the missing create actions + // That's why the 'basic' version of 'index_data_visualizer_actions_panel' is loaded here + loadTestFile( + require.resolve( + '../../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover' + ) + ); + loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); + }); +} diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index_data_visualizer_actions_panel.ts similarity index 97% rename from x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts rename to x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index_data_visualizer_actions_panel.ts index 2fcdf957f8909d..f8e2c83a1afd7f 100644 --- a/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/group3/index_data_visualizer_actions_panel.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts deleted file mode 100644 index 4d38e6a144a787..00000000000000 --- a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts +++ /dev/null @@ -1,29 +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 { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('data visualizer', function () { - // The file data visualizer should work the same as with a trial license - loadTestFile( - require.resolve('../../../../functional/apps/ml/data_visualizer/file_data_visualizer') - ); - - // The data visualizer should work the same as with a trial license, except the missing create actions - // That's why the 'basic' version of 'index_data_visualizer_actions_panel' is loaded here - loadTestFile( - require.resolve('../../../../functional/apps/ml/data_visualizer/index_data_visualizer') - ); - loadTestFile( - require.resolve( - '../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover' - ) - ); - loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); - }); -} diff --git a/x-pack/test/functional_basic/apps/ml/permissions/config.ts b/x-pack/test/functional_basic/apps/ml/permissions/config.ts new file mode 100644 index 00000000000000..048bbc8f0d1f37 --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/permissions/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + // default to the ml/config.base.ts + ...baseConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...baseConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - ml - permissions', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/ml/permissions/index.ts b/x-pack/test/functional_basic/apps/ml/permissions/index.ts index b8d57bc4a95256..53e78b4d08c155 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/index.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/index.ts @@ -7,8 +7,33 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { - describe('permissions', function () { +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning basic license - permissions', function () { + this.tags(['skipFirefox', 'ml']); + + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await ml.testResources.deleteSavedSearches(); + + await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + await ml.testResources.deleteIndexPatternByTitle('ft_module_sample_ecommerce'); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + loadTestFile(require.resolve('./full_ml_access')); loadTestFile(require.resolve('./read_ml_access')); loadTestFile(require.resolve('./no_ml_access')); diff --git a/x-pack/test/functional_basic/apps/transform/config.base.ts b/x-pack/test/functional_basic/apps/transform/config.base.ts new file mode 100644 index 00000000000000..776fdb2c189003 --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/config.base.ts @@ -0,0 +1,32 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + // default to the xpack functional config + ...xpackFunctionalConfig.getAll(), + esTestCluster: { + ...xpackFunctionalConfig.get('esTestCluster'), + license: 'basic', + serverArgs: [ + 'xpack.license.self_generated.type=basic', + 'xpack.security.enabled=true', + 'xpack.security.authc.api_key.enabled=true', + ], + }, + junit: { + ...xpackFunctionalConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - transform', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/transform/creation/index_pattern/config.ts b/x-pack/test/functional_basic/apps/transform/creation/index_pattern/config.ts new file mode 100644 index 00000000000000..469555d84b78dc --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/creation/index_pattern/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const transformConfig = await readConfigFile(require.resolve('../../config.base.ts')); + + return { + // default to the transform/config.base.ts + ...transformConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...transformConfig.get('junit'), + reportName: + 'Chrome X-Pack UI Functional Tests Basic License - transform - creation - index_pattern', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/transform/creation/index_pattern/index.ts b/x-pack/test/functional_basic/apps/transform/creation/index_pattern/index.ts new file mode 100644 index 00000000000000..d78248e6e72b32 --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/creation/index_pattern/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'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('transform basic license', function () { + this.tags(['skipFirefox', 'transform']); + + // The transform UI should work the same as with a trial license + loadTestFile( + require.resolve('../../../../../functional/apps/transform/creation/index_pattern') + ); + }); +} diff --git a/x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/config.ts b/x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/config.ts new file mode 100644 index 00000000000000..91daf3d0990070 --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const transformConfig = await readConfigFile(require.resolve('../../config.base.ts')); + + return { + // default to the transform/config.base.ts + ...transformConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...transformConfig.get('junit'), + reportName: + 'Chrome X-Pack UI Functional Tests Basic License - transform - creation - runtime mappings & saved search', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/index.ts b/x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/index.ts new file mode 100644 index 00000000000000..13978e3703e340 --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/creation/runtime_mappings_saved_search/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('transform basic license', function () { + this.tags(['skipFirefox', 'transform']); + + // The transform UI should work the same as with a trial license + loadTestFile( + require.resolve( + '../../../../../functional/apps/transform/creation/runtime_mappings_saved_search' + ) + ); + }); +} diff --git a/x-pack/test/functional_basic/apps/transform/edit_clone/config.ts b/x-pack/test/functional_basic/apps/transform/edit_clone/config.ts new file mode 100644 index 00000000000000..35650196f37e58 --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/edit_clone/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const transformConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + // default to the transform/config.base.ts + ...transformConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...transformConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - transform - edit & clone', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/transform/index.ts b/x-pack/test/functional_basic/apps/transform/edit_clone/index.ts similarity index 76% rename from x-pack/test/functional_basic/apps/transform/index.ts rename to x-pack/test/functional_basic/apps/transform/edit_clone/index.ts index f21d0674dcd243..7c55473b49eb54 100644 --- a/x-pack/test/functional_basic/apps/transform/index.ts +++ b/x-pack/test/functional_basic/apps/transform/edit_clone/index.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('transform basic license', function () { this.tags(['skipFirefox', 'transform']); // The transform UI should work the same as with a trial license - loadTestFile(require.resolve('../../../functional/apps/transform')); + loadTestFile(require.resolve('../../../../functional/apps/transform/edit_clone')); }); } diff --git a/x-pack/test/functional_basic/apps/transform/feature_controls/config.ts b/x-pack/test/functional_basic/apps/transform/feature_controls/config.ts new file mode 100644 index 00000000000000..29f57e3be1dbcb --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/feature_controls/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const transformConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + // default to the transform/config.base.ts + ...transformConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...transformConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - transform - feature controls', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/transform/feature_controls/index.ts b/x-pack/test/functional_basic/apps/transform/feature_controls/index.ts new file mode 100644 index 00000000000000..e3790ac777de2e --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/feature_controls/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('transform basic license', function () { + this.tags(['skipFirefox', 'transform']); + + // The transform UI should work the same as with a trial license + loadTestFile(require.resolve('../../../../functional/apps/transform/feature_controls')); + }); +} diff --git a/x-pack/test/functional_basic/apps/transform/permissions/config.ts b/x-pack/test/functional_basic/apps/transform/permissions/config.ts new file mode 100644 index 00000000000000..0babb10724110f --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/permissions/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const transformConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + // default to the transform/config.base.ts + ...transformConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...transformConfig.get('junit'), + reportName: 'Chrome X-Pack UI Functional Tests Basic License - transform - permissions', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/index.ts b/x-pack/test/functional_basic/apps/transform/permissions/index.ts similarity index 50% rename from x-pack/test/functional_basic/apps/index.ts rename to x-pack/test/functional_basic/apps/transform/permissions/index.ts index d765aeaa9e6ef7..c0bf1ec9e3dc62 100644 --- a/x-pack/test/functional_basic/apps/index.ts +++ b/x-pack/test/functional_basic/apps/transform/permissions/index.ts @@ -5,11 +5,13 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('apps', function () { - loadTestFile(require.resolve('./ml')); - loadTestFile(require.resolve('./transform')); + describe('transform basic license', function () { + this.tags(['skipFirefox', 'transform']); + + // The transform UI should work the same as with a trial license + loadTestFile(require.resolve('../../../../functional/apps/transform/permissions')); }); } diff --git a/x-pack/test/functional_basic/apps/transform/start_reset_delete/config.ts b/x-pack/test/functional_basic/apps/transform/start_reset_delete/config.ts new file mode 100644 index 00000000000000..6922e0f70c5a59 --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/start_reset_delete/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const transformConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + // default to the transform/config.base.ts + ...transformConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + ...transformConfig.get('junit'), + reportName: + 'Chrome X-Pack UI Functional Tests Basic License - transform - start reset & delete', + }, + }; +} diff --git a/x-pack/test/functional_basic/apps/transform/start_reset_delete/index.ts b/x-pack/test/functional_basic/apps/transform/start_reset_delete/index.ts new file mode 100644 index 00000000000000..14a9bcbc099c8b --- /dev/null +++ b/x-pack/test/functional_basic/apps/transform/start_reset_delete/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('transform basic license', function () { + this.tags(['skipFirefox', 'transform']); + + // The transform UI should work the same as with a trial license + loadTestFile(require.resolve('../../../../functional/apps/transform/start_reset_delete')); + }); +} From 26a0b8ab060061021f1eecbe4902714695511e6f Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 31 Jan 2023 09:18:16 -0800 Subject: [PATCH 05/59] [DOCS] Clarify preconfigured connectors (#149904) --- docs/management/action-types.asciidoc | 10 ++--- .../pre-configured-connectors-view-screen.png | Bin 62631 -> 0 bytes ... => preconfigured-connectors-managing.png} | Bin docs/management/connectors/index.asciidoc | 5 ++- .../pre-configured-connectors.asciidoc | 42 +++++++++--------- 5 files changed, 28 insertions(+), 29 deletions(-) delete mode 100644 docs/management/connectors/images/pre-configured-connectors-view-screen.png rename docs/management/connectors/images/{pre-configured-connectors-managing.png => preconfigured-connectors-managing.png} (100%) diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index fc18d1e2961db2..288c0d1fe0c666 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -145,6 +145,9 @@ image::images/connector-select-type.png[Connector select type] After you create a connector, it is available for use any time you set up an action in the current space. +For out-of-the-box and standardized connectors, refer to +<>. + [float] [[importing-and-exporting-connectors]] === Importing and exporting connectors @@ -161,13 +164,6 @@ button appears in *{connectors-ui}*. [role="screenshot"] image::images/connectors-with-missing-secrets.png[Connectors with missing secrets] -[float] -[[create-connectors]] -=== Preconfigured connectors - -For out-of-the-box and standardized connectors, you can <> -before {kib} starts. - [float] [[montoring-connectors]] === Monitoring connectors diff --git a/docs/management/connectors/images/pre-configured-connectors-view-screen.png b/docs/management/connectors/images/pre-configured-connectors-view-screen.png deleted file mode 100644 index b2d00b307000e28b40402e51f0f52519a77d8bc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62631 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 T`!}G951!V2J@sl;yQu#HK`k&? diff --git a/docs/management/connectors/images/pre-configured-connectors-managing.png b/docs/management/connectors/images/preconfigured-connectors-managing.png similarity index 100% rename from docs/management/connectors/images/pre-configured-connectors-managing.png rename to docs/management/connectors/images/preconfigured-connectors-managing.png diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index 90b41298d338fb..3315631fd94daa 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -12,8 +12,9 @@ include::action-types/servicenow-itom.asciidoc[leveloffset=+1] include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/tines.asciidoc[leveloffset=+1] +include::action-types/torq.asciidoc[] include::action-types/webhook.asciidoc[] include::action-types/cases-webhook.asciidoc[leveloffset=+1] include::action-types/xmatters.asciidoc[] -include::action-types/torq.asciidoc[] -include::pre-configured-connectors.asciidoc[] +include::pre-configured-connectors.asciidoc[leveloffset=+1] + diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index ad580d87e712ba..43643f0f611ba8 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -1,24 +1,28 @@ -[role="xpack"] [[pre-configured-connectors]] -=== Preconfigured connectors +== Preconfigured connectors -You can preconfigure a connector to have all the information it needs prior to -startup by adding it to the `kibana.yml` file. +If you are running {kib} on-prem, you can preconfigure a connector to have all +the information it needs prior to startup by adding it to the `kibana.yml` file. + +NOTE: {ess} provides a preconfigured email connector but you cannot create +additional preconfigured connectors. Preconfigured connectors offer the following benefits: -- Require no setup. Configuration and credentials needed to execute an -action are predefined, including the connector name and ID. +- Require no setup. Configuration and credentials needed to run an action are +predefined, including the connector name and ID. - Appear in all spaces because they are not saved objects. - Cannot be edited or deleted. [float] -[[preconfigured-connector-example]] -==== Preconfigured connectors example +[[create-preconfigured-connectors]] +=== Create preconfigured connectors + +Add `xpack.actions.preconfigured` settings to your `kibana.yml` file. The +settings vary depending on which type of connector you're adding. -This example shows a valid configuration for -two out-of-the box connectors: <> and -<>. +This example shows a valid configuration for a Slack connector and a Webhook +connector: ```js xpack.actions.preconfigured: @@ -50,31 +54,29 @@ two out-of-the box connectors: <> and [NOTE] ============================================== Sensitive properties, such as passwords, can also be stored in the -<>. +<>. ============================================== [float] [[build-in-preconfigured-connectors]] -==== Built-in preconfigured connectors +=== Built-in preconfigured connectors {kib} provides the following built-in preconfigured connectors: -* <> -* <> +* <> +* <> [float] [[managing-pre-configured-connectors]] -==== View preconfigured connectors +=== View preconfigured connectors When you open the main menu, click *{stack-manage-app} > {connectors-ui}*. Preconfigured connectors appear regardless of which space you are in. They are tagged as “preconfigured”, and you cannot delete them. [role="screenshot"] -image::images/pre-configured-connectors-managing.png[Connectors managing tab with pre-configured] +image::images/preconfigured-connectors-managing.png[Connectors managing tab with pre-configured] Clicking a preconfigured connector shows the description, but not the -configuration. A message indicates that this is a preconfigured connector. +configuration. -[role="screenshot"] -image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] From 8a1b0648dd8c6045b79a9158d7accc92a278d0df Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 31 Jan 2023 11:55:25 -0600 Subject: [PATCH 06/59] [RAM] Return multiple action validation errors when present (#149887) ## Summary Closes #149415 `validateActions` will now throw a summary of all errors when actions have multiple problems. Error message will look like this: ``` Failed to validate actions due to the following 2 errors: - Actions missing frequency parameters: group3 - Action throttle cannot be shorter than the schedule interval of 3h: default (1h), group2 (3m)" ``` ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../rules_client/lib/validate_actions.ts | 26 ++++-- .../server/rules_client/tests/create.test.ts | 86 +++++++++++++++++-- .../server/rules_client/tests/update.test.ts | 12 +-- 3 files changed, 108 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts index 20a5623edd392d..3c97080e2d655d 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts @@ -26,6 +26,8 @@ export async function validateActions( return; } + const errors = []; + // check for actions using connectors with missing secrets const actionsClient = await context.getActionsClient(); const actionIds = [...new Set(actions.map((action) => action.id))]; @@ -35,7 +37,7 @@ export async function validateActions( ); if (actionsUsingConnectorsWithMissingSecrets.length) { - throw Boom.badRequest( + errors.push( i18n.translate('xpack.alerting.rulesClient.validateActions.misconfiguredConnector', { defaultMessage: 'Invalid connectors: {groups}', values: { @@ -55,7 +57,7 @@ export async function validateActions( (group) => !availableAlertTypeActionGroups.has(group) ); if (invalidActionGroups.length) { - throw Boom.badRequest( + errors.push( i18n.translate('xpack.alerting.rulesClient.validateActions.invalidGroups', { defaultMessage: 'Invalid action groups: {groups}', values: { @@ -69,7 +71,7 @@ export async function validateActions( if (hasRuleLevelNotifyWhen || hasRuleLevelThrottle) { const actionsWithFrequency = actions.filter((action) => Boolean(action.frequency)); if (actionsWithFrequency.length) { - throw Boom.badRequest( + errors.push( i18n.translate('xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams', { defaultMessage: 'Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: {groups}', @@ -82,7 +84,7 @@ export async function validateActions( } else { const actionsWithoutFrequency = actions.filter((action) => !action.frequency); if (actionsWithoutFrequency.length) { - throw Boom.badRequest( + errors.push( i18n.translate('xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq', { defaultMessage: 'Actions missing frequency parameters: {groups}', values: { @@ -101,7 +103,7 @@ export async function validateActions( parseDuration(action.frequency.throttle!) < scheduleInterval ); if (actionsWithInvalidThrottles.length) { - throw Boom.badRequest( + errors.push( i18n.translate('xpack.alerting.rulesClient.validateActions.actionsWithInvalidThrottles', { defaultMessage: 'Action throttle cannot be shorter than the schedule interval of {scheduleIntervalText}: {groups}', @@ -114,4 +116,18 @@ export async function validateActions( }) ); } + + // Finalize and throw any errors present + if (errors.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.rulesClient.validateActions.errorSummary', { + defaultMessage: + 'Failed to validate actions due to the following {errorNum, plural, one {error:} other {# errors:\n-}} {errorList}', + values: { + errorNum: errors.length, + errorList: errors.join('\n- '), + }, + }) + ); + } } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 7963ebd885a77f..c11dc8be21ca41 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -2602,7 +2602,7 @@ describe('create()', () => { }, ]); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid connectors: email connector"` + `"Failed to validate actions due to the following error: Invalid connectors: email connector"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2760,7 +2760,7 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default, group2"` + `"Failed to validate actions due to the following error: Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default, group2"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2790,7 +2790,7 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data: data2 })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default"` + `"Failed to validate actions due to the following error: Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2833,7 +2833,7 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Actions missing frequency parameters: default"` + `"Failed to validate actions due to the following error: Actions missing frequency parameters: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2891,7 +2891,7 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Actions missing frequency parameters: group2"` + `"Failed to validate actions due to the following error: Actions missing frequency parameters: group2"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2968,9 +2968,83 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Action throttle cannot be shorter than the schedule interval of 3h: default (1h), group2 (3m)"` + `"Failed to validate actions due to the following error: Action throttle cannot be shorter than the schedule interval of 3h: default (1h), group2 (3m)"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); + + test('throws multiple errors when actions have multiple problems', async () => { + rulesClient = new RulesClient({ + ...rulesClientParams, + minimumScheduleInterval: { value: '1m', enforce: true }, + }); + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'group2', name: 'Action Group 2' }, + { id: 'group3', name: 'Action Group 3' }, + ], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() { + return { state: {} }; + }, + producer: 'alerts', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: jest.fn(), + }, + })); + + const data = getMockData({ + notifyWhen: undefined, + throttle: undefined, + schedule: { interval: '3h' }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + frequency: { + summary: false, + notifyWhen: 'onThrottleInterval', + throttle: '1h', + }, + }, + { + group: 'group2', + id: '2', + params: { + foo: true, + }, + frequency: { + summary: false, + notifyWhen: 'onThrottleInterval', + throttle: '3m', + }, + }, + { + group: 'group3', + id: '3', + params: { + foo: true, + }, + }, + ], + }); + await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(` + "Failed to validate actions due to the following 2 errors: + - Actions missing frequency parameters: group3 + - Action throttle cannot be shorter than the schedule interval of 3h: default (1h), group2 (3m)" + `); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 0de571c72916bc..44c4eeb50fe276 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -1767,7 +1767,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default, group2"` + `"Failed to validate actions due to the following error: Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default, group2"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -1808,7 +1808,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default"` + `"Failed to validate actions due to the following error: Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -1844,7 +1844,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Actions missing frequency parameters: default"` + `"Failed to validate actions due to the following error: Actions missing frequency parameters: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -1892,7 +1892,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Actions missing frequency parameters: default"` + `"Failed to validate actions due to the following error: Actions missing frequency parameters: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2016,7 +2016,9 @@ describe('update()', () => { ], }, }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid connectors: another connector"`); + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to validate actions due to the following error: Invalid connectors: another connector"` + ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); From 80062f19b91a5089f9d101e09770bcea7e449390 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 31 Jan 2023 19:07:42 +0100 Subject: [PATCH 07/59] [Synthetics] Error details comparison of metrics (#149817) Fixes https://github.com/elastic/kibana/issues/148588 --- .../synthetics/test_now_mode.journey.ts | 2 +- .../common/components/thershold_indicator.tsx | 78 +++++-- .../browser_steps_list.tsx | 56 +++-- .../monitor_test_result/result_details.tsx | 81 ++++++- .../result_details_successful.tsx | 70 ++++++ .../hooks/use_journey_steps.tsx | 11 +- .../monitor_summary/last_test_run.tsx | 6 +- .../common/network_data/data_formatting.ts | 4 + .../hooks/use_network_timings.ts | 84 +++---- .../hooks/use_network_timings_prev.ts | 63 +----- .../hooks/use_step_metrics.ts | 118 ++++++++-- .../hooks/use_step_prev_metrics.ts | 208 ++++++++++++++++-- .../step_metrics/step_metrics.tsx | 139 +++--------- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 16 files changed, 621 insertions(+), 302 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details_successful.tsx diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_now_mode.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_now_mode.journey.ts index 67ee9f278b9bf2..6c61272b24b91e 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_now_mode.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_now_mode.journey.ts @@ -137,7 +137,7 @@ journey(`TestNowMode`, async ({ page, params }) => { await page.waitForSelector('text=1 step completed'); await page.waitForSelector('text=Go to https://www.google.com'); - await page.waitForSelector('text=1.4 s'); + await page.waitForSelector('text=1.42 s'); await page.waitForSelector('text=Complete'); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx index 26ebe47e045fad..b1d82f16f3c358 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx @@ -11,29 +11,37 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiIconTip, EuiLoadingContent, + EuiStat, EuiText, EuiToolTip, } from '@elastic/eui'; -export const getDeltaPercent = (current: number, previous: number | null) => { - if (previous === 0 || previous === null) { +export const getDeltaPercent = (current: number, previous?: number | null) => { + if (previous === 0 || previous === null || previous === undefined) { return 0; } - return Number((((current - previous) / previous) * 100).toFixed(0)); }; export const ThresholdIndicator = ({ + description, + helpText, loading, current, previous, previousFormatted, currentFormatted, + asStat = false, }: { + description?: string; + helpText?: string; loading: boolean; current: number; - previous: number | null; + previous?: number | null; previousFormatted: string; currentFormatted: string; + asStat?: boolean; + setHasAnyDelta?: (hasDelta: boolean) => void; }) => { if (loading) { return ; @@ -71,6 +79,50 @@ export const ThresholdIndicator = ({ const hasDelta = Math.abs(delta) > 0; + const content = + previous === null ? ( + + ) : ( + + {hasDelta ? ( + 0 ? 'sortUp' : 'sortDown'} + size={asStat ? 'l' : 'm'} + color={getColor()} + /> + ) : ( + + )} + + ); + + if (asStat) { + return ( + + {description} {helpText && } + + } + title={ + <> + {currentFormatted} + {content} + + } + reverse={true} + /> + ); + } + return ( @@ -78,23 +130,7 @@ export const ThresholdIndicator = ({ {currentFormatted} - {previous !== null && ( - - - {hasDelta ? ( - 0 ? 'sortUp' : 'sortDown'} size="m" color={getColor()} /> - ) : ( - - )} - - - )} + {content} ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx index ec9a1217f8ea42..64ed7b366c2457 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx @@ -24,6 +24,7 @@ import { StepDetailsLinkIcon } from '../links/step_details_link'; import { parseBadgeStatus, getTextColorForMonitorStatus } from './status_badge'; import { StepDurationText } from './step_duration_text'; +import { ResultDetailsSuccessful } from './result_details_successful'; interface Props { steps: JourneyStep[]; @@ -64,6 +65,8 @@ export const BrowserStepsList = ({ setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); }; + const showLastSuccessful = true; + const columns: Array> = [ ...(showExpand ? [ @@ -146,17 +149,32 @@ export const BrowserStepsList = ({ /> ), }, - { - align: 'left', - name: STEP_DURATION, - render: (item: JourneyStep) => { - return ; - }, - mobileOptions: { - header: STEP_DURATION, - show: true, - }, - }, + ...(showLastSuccessful + ? [ + { + field: 'synthetics.step.status', + name: LAST_SUCCESSFUL, + render: (pingStatus: string, item: JourneyStep) => ( + + ), + }, + ] + : [ + { + align: 'left' as const, + name: STEP_DURATION, + render: (item: JourneyStep) => { + return ; + }, + mobileOptions: { + header: STEP_DURATION, + show: true, + }, + }, + ]), { align: 'right', field: 'timestamp', @@ -175,12 +193,13 @@ export const BrowserStepsList = ({ return ( <> ({ - style: { verticalAlign: 'initial' }, - })} - cellProps={() => ({ - style: { verticalAlign: 'initial' }, - })} + cellProps={(row) => { + if (itemIdToExpandedRowMap[row._id]) { + return { + style: { verticalAlign: 'top' }, + }; + } + }} compressed={compressed} loading={loading} columns={columns} @@ -232,6 +251,9 @@ const RESULT_LABEL = i18n.translate('xpack.synthetics.monitor.result.label', { defaultMessage: 'Result', }); +const LAST_SUCCESSFUL = i18n.translate('xpack.synthetics.monitor.result.lastSuccessful', { + defaultMessage: 'Last successful', +}); const SCREENSHOT_LABEL = i18n.translate('xpack.synthetics.monitor.screenshot.label', { defaultMessage: 'Screenshot', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx index 6f0c5453f920a1..2021c0da1423e6 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx @@ -6,7 +6,10 @@ */ import React from 'react'; -import { EuiDescriptionList, EuiSpacer } from '@elastic/eui'; +import { EuiDescriptionList, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useStepMetrics } from '../../step_details_page/hooks/use_step_metrics'; +import { JourneyStepScreenshotContainer } from '../screenshot/journey_step_screenshot_container'; import { formatBytes } from '../../step_details_page/hooks/use_object_metrics'; import { ThresholdIndicator } from '../components/thershold_indicator'; import { useNetworkTimings } from '../../step_details_page/hooks/use_network_timings'; @@ -14,6 +17,7 @@ import { useNetworkTimingsPrevious24Hours } from '../../step_details_page/hooks/ import { formatMillisecond } from '../../step_details_page/common/network_data/data_formatting'; import { JourneyStep } from '../../../../../../common/runtime_types'; import { parseBadgeStatus, StatusBadge } from './status_badge'; +import { useStepPrevMetrics } from '../../step_details_page/hooks/use_step_prev_metrics'; export const ResultDetails = ({ pingStatus, @@ -26,16 +30,37 @@ export const ResultDetails = ({ }) => { return (
- + + {' '} + {i18n.translate('xpack.synthetics.step.duration.label', { + defaultMessage: 'after {value}', + values: { + value: formatMillisecond((step.synthetics?.step?.duration.us ?? 0) / 1000, {}), + }, + })} + + {isExpanded && ( <> + + + + )}
); }; + export const TimingDetails = ({ step }: { step: JourneyStep }) => { const { timingsWithLabels, transferSize } = useNetworkTimings( step.monitor.check_group, @@ -46,20 +71,24 @@ export const TimingDetails = ({ step }: { step: JourneyStep }) => { timingsWithLabels: prevTimingsWithLabels, loading, transferSizePrev, - } = useNetworkTimingsPrevious24Hours(step.synthetics.step?.index, step['@timestamp']); + } = useNetworkTimingsPrevious24Hours( + step.synthetics.step?.index, + step['@timestamp'], + step.monitor.check_group + ); const items = timingsWithLabels?.map((item) => { const prevValueItem = prevTimingsWithLabels?.find((prev) => prev.label === item.label); - const prevValue = prevValueItem?.value ?? 0; + const prevValue = prevValueItem?.value; return { title: item.label, description: ( ), }; @@ -84,7 +113,41 @@ export const TimingDetails = ({ step }: { step: JourneyStep }) => { gutterSize="s" type="column" listItems={items} - style={{ maxWidth: 250 }} + style={{ maxWidth: 265 }} + textStyle="reverse" + descriptionProps={{ style: { textAlign: 'right' } }} + /> + ); +}; + +export const StepMetrics = ({ step }: { step: JourneyStep }) => { + const { metrics: stepMetrics } = useStepMetrics(step); + const { metrics: prevMetrics, loading } = useStepPrevMetrics(step); + + const items = stepMetrics?.map((item) => { + const prevValueItem = prevMetrics?.find((prev) => prev.label === item.label); + const prevValue = prevValueItem?.value; + return { + title: item.label, + description: ( + + ), + }; + }); + + return ( + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details_successful.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details_successful.tsx new file mode 100644 index 00000000000000..05e6da0b2f8b17 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details_successful.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui'; +import { useFetcher } from '@kbn/observability-plugin/public'; +import { StepMetrics, TimingDetails } from './result_details'; +import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; +import { JourneyStepScreenshotContainer } from '../screenshot/journey_step_screenshot_container'; +import { formatMillisecond } from '../../step_details_page/common/network_data/data_formatting'; +import { JourneyStep } from '../../../../../../common/runtime_types'; +import { IMAGE_UN_AVAILABLE } from '../../step_details_page/step_screenshot/last_successful_screenshot'; +import { fetchLastSuccessfulCheck } from '../../../state'; + +export const ResultDetailsSuccessful = ({ + isExpanded, + step, +}: { + isExpanded: boolean; + step: JourneyStep; +}) => { + const { euiTheme } = useEuiTheme(); + + const { data, loading } = useFetcher(() => { + return fetchLastSuccessfulCheck({ + timestamp: step['@timestamp'], + monitorId: step.monitor.id, + stepIndex: Number(step.synthetics.step?.index), + location: step.observer?.geo?.name, + }); + }, [step._id, step['@timestamp']]); + + const { currentStep } = useJourneySteps( + data?.monitor.check_group, + 0, + Number(step.synthetics.step?.index) + ); + + return ( +
+ + {formatMillisecond((currentStep?.synthetics?.step?.duration.us ?? 0) / 1000, {})} + + + {isExpanded && ( + <> + + + + {currentStep && } + + {currentStep && } + + )} +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx index bd3d45a0a59f08..59017119864195 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx @@ -16,11 +16,18 @@ import { selectBrowserJourneyLoading, } from '../../../state'; -export const useJourneySteps = (checkGroup?: string, lastRefresh?: number) => { - const { stepIndex, checkGroupId: urlCheckGroup } = useParams<{ +export const useJourneySteps = ( + checkGroup?: string, + lastRefresh?: number, + stepIndexArg?: number +) => { + const { stepIndex: stepIndexUrl, checkGroupId: urlCheckGroup } = useParams<{ stepIndex: string; checkGroupId: string; }>(); + + const stepIndex = stepIndexArg ?? stepIndexUrl; + const checkGroupId = checkGroup ?? urlCheckGroup; const journeyData = useSelector(selectBrowserJourney(checkGroupId)); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx index 9c4249517b64f5..48a8ec59620426 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx @@ -86,7 +86,7 @@ export const LastTestRunComponent = ({ return ( - {!loading && latestPing?.error ? ( + {!(loading && !latestPing) && latestPing?.error ? ( ) : ( @@ -163,7 +163,7 @@ const PanelHeader = ({ ); - if (loading) { + if (loading && !latestPing) { return ( <> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts index 4ca92c3390f1d4..11f8a6ddbd43bd 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts @@ -495,6 +495,10 @@ export const formatMillisecond = ( ms: number, { maxMillis = 1000, digits }: { digits?: number; maxMillis?: number } ) => { + if (ms < 0) { + return '--'; + } + if (ms < maxMillis) { return `${ms.toFixed(digits ?? 0)} ms`; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts index 0962bf7aeebf3f..79c852dddda7dd 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings.ts @@ -82,7 +82,7 @@ export const useNetworkTimings = (checkGroupIdArg?: string, stepIndexArg?: numbe field: SYNTHETICS_DNS_TIMINGS, }, }, - ssl: { + tls: { sum: { field: SYNTHETICS_SSL_TIMINGS, }, @@ -138,7 +138,7 @@ export const useNetworkTimings = (checkGroupIdArg?: string, stepIndexArg?: numbe send: aggs?.send.value ?? 0, wait: aggs?.wait.value ?? 0, blocked: aggs?.blocked.value ?? 0, - ssl: aggs?.ssl.value ?? 0, + tls: aggs?.tls.value ?? 0, transferSize: aggs?.transferSize.value ?? 0, }; @@ -148,58 +148,62 @@ export const useNetworkTimings = (checkGroupIdArg?: string, stepIndexArg?: numbe value: timings.transferSize, label: CONTENT_SIZE_LABEL, }, - timingsWithLabels: [ - { - value: timings.dns, - label: SYNTHETICS_DNS_TIMINGS_LABEL, - }, - { - value: timings.ssl, - label: SYNTHETICS_SSL_TIMINGS_LABEL, - }, - { - value: timings.blocked, - label: SYNTHETICS_BLOCKED_TIMINGS_LABEL, - }, - { - value: timings.connect, - label: SYNTHETICS_CONNECT_TIMINGS_LABEL, - }, - { - value: timings.receive, - label: SYNTHETICS_RECEIVE_TIMINGS_LABEL, - }, - { - value: timings.send, - label: SYNTHETICS_SEND_TIMINGS_LABEL, - }, - { - value: timings.wait, - label: SYNTHETICS_WAIT_TIMINGS_LABEL, - }, - ].sort((a, b) => b.value - a.value), + timingsWithLabels: getTimingWithLabels(timings), }; }; -const SYNTHETICS_CONNECT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.connect.label', { +export const getTimingWithLabels = (timings: Record) => { + return [ + { + value: timings.blocked, + label: SYNTHETICS_BLOCKED_TIMINGS_LABEL, + }, + { + value: timings.dns, + label: SYNTHETICS_DNS_TIMINGS_LABEL, + }, + { + value: timings.connect, + label: SYNTHETICS_CONNECT_TIMINGS_LABEL, + }, + { + value: timings.tls, + label: SYNTHETICS_TLS_TIMINGS_LABEL, + }, + { + value: timings.wait, + label: SYNTHETICS_WAIT_TIMINGS_LABEL, + }, + { + value: timings.receive, + label: SYNTHETICS_RECEIVE_TIMINGS_LABEL, + }, + { + value: timings.send, + label: SYNTHETICS_SEND_TIMINGS_LABEL, + }, + ]; +}; + +export const SYNTHETICS_CONNECT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.connect.label', { defaultMessage: 'Connect', }); -const SYNTHETICS_DNS_TIMINGS_LABEL = i18n.translate('xpack.synthetics.dns', { +export const SYNTHETICS_DNS_TIMINGS_LABEL = i18n.translate('xpack.synthetics.dns', { defaultMessage: 'DNS', }); -const SYNTHETICS_WAIT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.wait', { +export const SYNTHETICS_WAIT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.wait', { defaultMessage: 'Wait', }); -const SYNTHETICS_SSL_TIMINGS_LABEL = i18n.translate('xpack.synthetics.ssl', { - defaultMessage: 'SSL', +export const SYNTHETICS_TLS_TIMINGS_LABEL = i18n.translate('xpack.synthetics.tls', { + defaultMessage: 'TLS', }); -const SYNTHETICS_BLOCKED_TIMINGS_LABEL = i18n.translate('xpack.synthetics.blocked', { +export const SYNTHETICS_BLOCKED_TIMINGS_LABEL = i18n.translate('xpack.synthetics.blocked', { defaultMessage: 'Blocked', }); -const SYNTHETICS_SEND_TIMINGS_LABEL = i18n.translate('xpack.synthetics.send', { +export const SYNTHETICS_SEND_TIMINGS_LABEL = i18n.translate('xpack.synthetics.send', { defaultMessage: 'Send', }); -const SYNTHETICS_RECEIVE_TIMINGS_LABEL = i18n.translate('xpack.synthetics.receive', { +export const SYNTHETICS_RECEIVE_TIMINGS_LABEL = i18n.translate('xpack.synthetics.receive', { defaultMessage: 'Receive', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts index d59a551e58767f..b7e392759231fe 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_network_timings_prev.ts @@ -22,6 +22,7 @@ import moment from 'moment'; import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; +import { getTimingWithLabels } from './use_network_timings'; export const useStepFilters = (checkGroupId: string, stepIndex: number) => { return [ @@ -38,11 +39,15 @@ export const useStepFilters = (checkGroupId: string, stepIndex: number) => { ]; }; -export const useNetworkTimingsPrevious24Hours = (stepIndexArg?: number, timestampArg?: string) => { +export const useNetworkTimingsPrevious24Hours = ( + stepIndexArg?: number, + timestampArg?: string, + checkGroupIdArg?: string +) => { const params = useParams<{ checkGroupId: string; stepIndex: string; monitorId: string }>(); const configId = params.monitorId; - const checkGroupId = params.checkGroupId; + const checkGroupId = checkGroupIdArg ?? params.checkGroupId; const stepIndex = stepIndexArg ?? Number(params.stepIndex); const { currentStep } = useJourneySteps(); @@ -210,36 +215,7 @@ export const useNetworkTimingsPrevious24Hours = (stepIndexArg?: number, timestam value: timings.transferSize, label: CONTENT_SIZE_LABEL, }, - timingsWithLabels: [ - { - value: timings.dns, - label: SYNTHETICS_DNS_TIMINGS_LABEL, - }, - { - value: timings.ssl, - label: SYNTHETICS_SSL_TIMINGS_LABEL, - }, - { - value: timings.blocked, - label: SYNTHETICS_BLOCKED_TIMINGS_LABEL, - }, - { - value: timings.connect, - label: SYNTHETICS_CONNECT_TIMINGS_LABEL, - }, - { - value: timings.receive, - label: SYNTHETICS_RECEIVE_TIMINGS_LABEL, - }, - { - value: timings.send, - label: SYNTHETICS_SEND_TIMINGS_LABEL, - }, - { - value: timings.wait, - label: SYNTHETICS_WAIT_TIMINGS_LABEL, - }, - ].sort((a, b) => b.value - a.value), + timingsWithLabels: getTimingWithLabels(timings), }; }; @@ -250,29 +226,6 @@ const median = (arr: number[]): number => { return s.length % 2 === 0 ? (s[mid - 1] + s[mid]) / 2 : s[mid]; }; -const SYNTHETICS_CONNECT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.connect.label', { - defaultMessage: 'Connect', -}); -const SYNTHETICS_DNS_TIMINGS_LABEL = i18n.translate('xpack.synthetics.dns', { - defaultMessage: 'DNS', -}); -const SYNTHETICS_WAIT_TIMINGS_LABEL = i18n.translate('xpack.synthetics.wait', { - defaultMessage: 'Wait', -}); - -const SYNTHETICS_SSL_TIMINGS_LABEL = i18n.translate('xpack.synthetics.ssl', { - defaultMessage: 'SSL', -}); -const SYNTHETICS_BLOCKED_TIMINGS_LABEL = i18n.translate('xpack.synthetics.blocked', { - defaultMessage: 'Blocked', -}); -const SYNTHETICS_SEND_TIMINGS_LABEL = i18n.translate('xpack.synthetics.send', { - defaultMessage: 'Send', -}); -const SYNTHETICS_RECEIVE_TIMINGS_LABEL = i18n.translate('xpack.synthetics.receive', { - defaultMessage: 'Receive', -}); - export const CONTENT_SIZE_LABEL = i18n.translate('xpack.synthetics.contentSize', { defaultMessage: 'Content Size', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts index 3db64c5113305e..0f2342e34496b6 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts @@ -6,8 +6,13 @@ */ import { useEsSearch } from '@kbn/observability-plugin/public'; -import { useStepFilters } from './use_step_filters'; +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { formatBytes } from './use_object_metrics'; +import { formatMillisecond } from '../step_metrics/step_metrics'; +import { CLS_HELP_LABEL, DCL_TOOLTIP, FCP_TOOLTIP, LCP_HELP_LABEL } from '../step_metrics/labels'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { JourneyStep } from '../../../../../../common/runtime_types'; export const MONITOR_DURATION_US = 'monitor.duration.us'; export const SYNTHETICS_CLS = 'browser.experience.cls'; @@ -20,12 +25,14 @@ export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; export type StepMetrics = ReturnType; -export const useStepMetrics = (loadData = true, prevCheckGroupId?: string) => { - const esIndex = loadData ? SYNTHETICS_INDEX_PATTERN : ''; +export const useStepMetrics = (step?: JourneyStep) => { + const urlParams = useParams<{ checkGroupId: string; stepIndex: string }>(); + const checkGroupId = step?.monitor.check_group ?? urlParams.checkGroupId; + const stepIndex = step?.synthetics.step?.index ?? urlParams.stepIndex; const { data } = useEsSearch( { - index: esIndex, + index: SYNTHETICS_INDEX_PATTERN, body: { size: 0, query: { @@ -36,7 +43,16 @@ export const useStepMetrics = (loadData = true, prevCheckGroupId?: string) => { 'synthetics.type': ['step/metrics', 'step/end'], }, }, - ...useStepFilters(prevCheckGroupId), + { + term: { + 'monitor.check_group': checkGroupId, + }, + }, + { + term: { + 'synthetics.step.index': Number(stepIndex), + }, + }, ], }, }, @@ -69,13 +85,13 @@ export const useStepMetrics = (loadData = true, prevCheckGroupId?: string) => { }, }, }, - [esIndex], - { name: prevCheckGroupId ? 'previousStepMetrics' : 'stepMetrics' } + [stepIndex, checkGroupId], + { name: 'stepMetrics' } ); const { data: transferData } = useEsSearch( { - index: esIndex, + index: SYNTHETICS_INDEX_PATTERN, body: { size: 0, runtime_mappings: { @@ -94,7 +110,16 @@ export const useStepMetrics = (loadData = true, prevCheckGroupId?: string) => { 'synthetics.type': 'journey/network_info', }, }, - ...useStepFilters(prevCheckGroupId), + { + term: { + 'monitor.check_group': checkGroupId, + }, + }, + { + term: { + 'synthetics.step.index': Number(stepIndex), + }, + }, ], }, }, @@ -112,17 +137,80 @@ export const useStepMetrics = (loadData = true, prevCheckGroupId?: string) => { }, }, }, - [esIndex], + [stepIndex, checkGroupId], { - name: prevCheckGroupId - ? 'previousStepMetricsFromNetworkInfos' - : 'stepMetricsFromNetworkInfos', + name: 'stepMetricsFromNetworkInfos', } ); + const metrics = data?.aggregations; + const transferDataVal = transferData?.aggregations?.transferSize?.value ?? 0; + return { ...(data?.aggregations ?? {}), - transferData: ((transferData?.aggregations?.transferSize?.value ?? 0) / 1e6).toFixed(0), - resourceSize: ((transferData?.aggregations?.resourceSize?.value ?? 0) / 1e6).toFixed(0), + transferData: transferData?.aggregations?.transferSize?.value ?? 0, + resourceSize: transferData?.aggregations?.resourceSize?.value ?? 0, + + metrics: [ + { + label: STEP_DURATION_LABEL, + value: metrics?.totalDuration.value, + formatted: formatMillisecond((metrics?.totalDuration.value ?? 0) / 1000), + }, + { + value: metrics?.lcp.value, + label: LCP_LABEL, + helpText: LCP_HELP_LABEL, + formatted: formatMillisecond((metrics?.lcp.value ?? 0) / 1000), + }, + { + value: metrics?.fcp.value, + label: FCP_LABEL, + helpText: FCP_TOOLTIP, + formatted: formatMillisecond((metrics?.fcp.value ?? 0) / 1000), + }, + { + value: metrics?.cls.value, + label: CLS_LABEL, + helpText: CLS_HELP_LABEL, + formatted: formatMillisecond((metrics?.cls.value ?? 0) / 1000), + }, + { + value: metrics?.dcl.value, + label: DCL_LABEL, + helpText: DCL_TOOLTIP, + formatted: formatMillisecond((metrics?.dcl.value ?? 0) / 1000), + }, + { + value: transferDataVal, + label: TRANSFER_SIZE, + helpText: '', + formatted: formatBytes(transferDataVal ?? 0), + }, + ], }; }; + +export const LCP_LABEL = i18n.translate('xpack.synthetics.lcp.label', { + defaultMessage: 'LCP', +}); + +export const FCP_LABEL = i18n.translate('xpack.synthetics.fcp.label', { + defaultMessage: 'FCP', +}); + +export const CLS_LABEL = i18n.translate('xpack.synthetics.cls.label', { + defaultMessage: 'CLS', +}); + +export const DCL_LABEL = i18n.translate('xpack.synthetics.dcl.label', { + defaultMessage: 'DCL', +}); + +export const STEP_DURATION_LABEL = i18n.translate('xpack.synthetics.totalDuration.metrics', { + defaultMessage: 'Step duration', +}); + +export const TRANSFER_SIZE = i18n.translate('xpack.synthetics.totalDuration.transferSize', { + defaultMessage: 'Transfer size', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts index bf8f1e14259de0..db5475380f5ebf 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts @@ -6,8 +6,19 @@ */ import { useParams } from 'react-router-dom'; -import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; -import { StepMetrics, useStepMetrics } from './use_step_metrics'; +import { useEsSearch } from '@kbn/observability-plugin/public'; +import { formatBytes } from './use_object_metrics'; +import { formatMillisecond } from '../step_metrics/step_metrics'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { + CLS_LABEL, + DCL_LABEL, + FCP_LABEL, + LCP_LABEL, + STEP_DURATION_LABEL, + TRANSFER_SIZE, +} from './use_step_metrics'; +import { JourneyStep } from '../../../../../../common/runtime_types'; export const MONITOR_DURATION_US = 'monitor.duration.us'; export const SYNTHETICS_CLS = 'browser.experience.cls'; @@ -18,33 +29,182 @@ 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 useStepPrevMetrics = (stepMetrics: StepMetrics) => { - const { checkGroupId } = useParams<{ checkGroupId: string; stepIndex: string }>(); +export const useStepPrevMetrics = (step?: JourneyStep) => { + const urlParams = useParams<{ + checkGroupId: string; + stepIndex: string; + monitorId: string; + }>(); - const { data } = useJourneySteps(checkGroupId); + const monitorId = urlParams.monitorId; + const checkGroupId = step?.monitor.check_group ?? urlParams.checkGroupId; + const stepIndex = step?.synthetics.step?.index ?? urlParams.stepIndex; - const prevCheckGroupId = data?.details?.previous?.checkGroup; - - const prevMetrics = useStepMetrics(Boolean(prevCheckGroupId), prevCheckGroupId); + const { data, loading } = useEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + 'synthetics.type': ['step/metrics', 'step/end'], + }, + }, + { + term: { + 'synthetics.step.index': Number(stepIndex), + }, + }, + { + term: { + config_id: monitorId, + }, + }, + { + range: { + '@timestamp': { + lte: 'now', + gte: 'now-24h/h', + }, + }, + }, + ], + }, + }, + aggs: { + fcp: { + avg: { + field: SYNTHETICS_FCP, + }, + }, + lcp: { + avg: { + field: SYNTHETICS_LCP, + }, + }, + cls: { + avg: { + field: SYNTHETICS_CLS, + }, + }, + dcl: { + avg: { + field: SYNTHETICS_DCL, + }, + }, + totalDuration: { + avg: { + field: SYNTHETICS_STEP_DURATION, + }, + }, + }, + }, + }, + [monitorId, checkGroupId, stepIndex], + { name: 'previousStepMetrics' } + ); - const fcpThreshold = findThreshold(stepMetrics?.fcp?.value, prevMetrics?.fcp?.value); - const lcpThreshold = findThreshold(stepMetrics?.lcp?.value, prevMetrics?.lcp?.value); - const clsThreshold = findThreshold(stepMetrics?.cls?.value, prevMetrics?.cls?.value); - const dclThreshold = findThreshold(stepMetrics?.dcl?.value, prevMetrics?.dcl?.value); - const totalThreshold = findThreshold( - stepMetrics?.totalDuration?.value, - prevMetrics?.totalDuration?.value + const { data: transferData } = useEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 0, + runtime_mappings: { + 'synthetics.payload.transfer_size': { + type: 'double', + }, + 'synthetics.payload.resource_size': { + type: 'double', + }, + }, + query: { + bool: { + filter: [ + { + term: { + 'synthetics.type': 'journey/network_info', + }, + }, + { + term: { + 'synthetics.step.index': Number(stepIndex), + }, + }, + { + term: { + config_id: monitorId, + }, + }, + { + range: { + '@timestamp': { + lte: 'now', + gte: 'now-24h/h', + }, + }, + }, + ], + }, + }, + aggs: { + transferSize: { + avg: { + field: 'synthetics.payload.transfer_size', + }, + }, + resourceSize: { + avg: { + field: 'synthetics.payload.resource_size', + }, + }, + }, + }, + }, + [monitorId, checkGroupId, stepIndex], + { + name: 'previousStepMetricsFromNetworkInfos', + } ); + const metrics = data?.aggregations; + const transferDataVal = transferData?.aggregations?.transferSize?.value ?? 0; + return { - fcpThreshold, - lcpThreshold, - clsThreshold, - dclThreshold, - totalThreshold, + loading, + metrics: [ + { + label: STEP_DURATION_LABEL, + value: metrics?.totalDuration.value, + formatted: formatMillisecond((metrics?.totalDuration.value ?? 0) / 1000), + }, + { + value: metrics?.lcp.value, + label: LCP_LABEL, + formatted: formatMillisecond((metrics?.lcp.value ?? 0) / 1000), + }, + { + value: metrics?.fcp.value, + label: FCP_LABEL, + formatted: formatMillisecond((metrics?.fcp.value ?? 0) / 1000), + }, + { + value: metrics?.cls.value, + label: CLS_LABEL, + formatted: formatMillisecond((metrics?.cls.value ?? 0) / 1000), + }, + { + value: metrics?.dcl.value, + label: DCL_LABEL, + formatted: formatMillisecond((metrics?.dcl.value ?? 0) / 1000), + }, + { + value: transferDataVal, + label: TRANSFER_SIZE, + formatted: formatBytes(transferDataVal ?? 0), + }, + ], }; }; - -const findThreshold = (current?: number | null, prev?: number | null) => { - return -1 * (100 - ((current ?? 0) / (prev ?? 0)) * 100); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx index 1539c98111c5fb..bb508e9bea2f0c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx @@ -6,23 +6,18 @@ */ import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiStat, - EuiTitle, - EuiIcon, - EuiIconTip, - EuiFlexGrid, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiFlexGrid } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CLS_HELP_LABEL, DCL_TOOLTIP, FCP_TOOLTIP, LCP_HELP_LABEL } from './labels'; +import { ThresholdIndicator } from '../../common/components/thershold_indicator'; import { DefinitionsPopover } from './definitions_popover'; import { useStepMetrics } from '../hooks/use_step_metrics'; import { useStepPrevMetrics } from '../hooks/use_step_prev_metrics'; export const formatMillisecond = (ms: number) => { + if (ms < 0) { + return '- ms'; + } + if (ms < 1000) { return `${ms.toFixed(0)} ms`; } @@ -30,10 +25,8 @@ export const formatMillisecond = (ms: number) => { }; export const StepMetrics = () => { - const stepMetrics = useStepMetrics(); - - const { fcpThreshold, lcpThreshold, clsThreshold, dclThreshold, totalThreshold } = - useStepPrevMetrics(stepMetrics); + const { metrics: stepMetrics } = useStepMetrics(); + const { metrics: prevMetrics, loading } = useStepPrevMetrics(); return ( <> @@ -50,108 +43,30 @@ export const StepMetrics = () => { - - - - - - - - - - - - - - - - - - - + {stepMetrics.map(({ label, value, helpText, formatted }) => { + const prevVal = prevMetrics.find((prev) => prev.label === label); + + if (label) + return ( + + + + ); + })} ); }; -const StatThreshold = ({ - title, - threshold, - description, - helpText, -}: { - threshold: number; - title: number | string; - description: string; - helpText?: string; -}) => { - const isUp = threshold >= 5; - const isDown = threshold < 5; - - const isSame = (!isUp && !isDown) || !isFinite(threshold); - return ( - - - - {description} {helpText && } - - } - title={ - <> - {title} - - {isSame ? ( - - ) : ( - - )} - - - } - reverse={true} - /> - - - ); -}; - const METRICS_LABEL = i18n.translate('xpack.synthetics.stepDetailsRoute.metrics', { defaultMessage: 'Metrics', }); - -const TOTAL_DURATION_LABEL = i18n.translate('xpack.synthetics.totalDuration.metrics', { - defaultMessage: 'Step duration', -}); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e7f636b6915684..83edd358eaa8a6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34060,7 +34060,6 @@ "xpack.synthetics.sourceConfiguration.heartbeatIndicesTitle": "Index Uptime", "xpack.synthetics.sourceConfiguration.indicesSectionTitle": "Index", "xpack.synthetics.sourceConfiguration.warningStateLabel": "Limite d'âge", - "xpack.synthetics.ssl": "SSL", "xpack.synthetics.stackManagement": "Gestion de la Suite", "xpack.synthetics.stepDetails.expected": "Attendus", "xpack.synthetics.stepDetails.objectCount": "Décompte de l'objet", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 249b8cc04d8188..60f3f7c9ae88de 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34031,7 +34031,6 @@ "xpack.synthetics.sourceConfiguration.heartbeatIndicesTitle": "アップタイムインデックス", "xpack.synthetics.sourceConfiguration.indicesSectionTitle": "インデックス", "xpack.synthetics.sourceConfiguration.warningStateLabel": "使用期間上限", - "xpack.synthetics.ssl": "SSL", "xpack.synthetics.stackManagement": "スタック管理", "xpack.synthetics.stepDetails.expected": "期待値", "xpack.synthetics.stepDetails.objectCount": "オブジェクト数", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ccd28eb8c63d51..b3f6dcf5ef9a4c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34066,7 +34066,6 @@ "xpack.synthetics.sourceConfiguration.heartbeatIndicesTitle": "Uptime 索引", "xpack.synthetics.sourceConfiguration.indicesSectionTitle": "索引", "xpack.synthetics.sourceConfiguration.warningStateLabel": "使用时间限制", - "xpack.synthetics.ssl": "SSL", "xpack.synthetics.stackManagement": "Stack Management", "xpack.synthetics.stepDetails.expected": "预期", "xpack.synthetics.stepDetails.objectCount": "对象计数", From c5f873900187179ab32f579cb5eeb4b2bc29ffb3 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Tue, 31 Jan 2023 19:14:34 +0100 Subject: [PATCH 08/59] event log failure message (#149355) fixes: #147512 Event log message gets overwritten when the existing message is not a failure message and the last status is a failure. Flaky test runner results: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds --- .../alerting_event_logger.test.ts | 39 +++++++++++++++++++ .../alerting_event_logger.ts | 2 +- .../group2/tests/alerting/event_log.ts | 9 ++--- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 3d7f3b10390fe6..11f57e8145e95c 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -860,6 +860,45 @@ describe('AlertingEventLogger', () => { expect(alertingEventLogger.getEvent()).toEqual(loggedEvent); expect(eventLogger.logEvent).toHaveBeenCalledWith(loggedEvent); }); + + test('overwrites the message when the final status is error', () => { + alertingEventLogger.initialize(context); + alertingEventLogger.start(); + alertingEventLogger.setExecutionSucceeded('success message'); + + expect(alertingEventLogger.getEvent()!.message).toBe('success message'); + + alertingEventLogger.done({ + status: { + status: 'error', + lastExecutionDate: new Date(), + error: { reason: RuleExecutionStatusErrorReasons.Execute, message: 'failed execution' }, + }, + }); + + expect(alertingEventLogger.getEvent()!.message).toBe('test:123: execution failed'); + }); + + test('does not overwrites the message when there is already a failure message', () => { + alertingEventLogger.initialize(context); + alertingEventLogger.start(); + alertingEventLogger.setExecutionFailed('first failure message', 'failure error message'); + + expect(alertingEventLogger.getEvent()!.message).toBe('first failure message'); + + alertingEventLogger.done({ + status: { + status: 'error', + lastExecutionDate: new Date(), + error: { + reason: RuleExecutionStatusErrorReasons.Execute, + message: 'second failure execution', + }, + }, + }); + + expect(alertingEventLogger.getEvent()!.message).toBe('first failure message'); + }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index 3422fb21bb1f9e..fff026a358bc82 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -184,7 +184,7 @@ export class AlertingEventLogger { alertingOutcome: 'failure', reason: status.error?.reason || 'unknown', error: this.event?.error?.message || status.error.message, - ...(this.event.message + ...(this.event.message && this.event.event?.outcome === 'failure' ? {} : { message: `${this.ruleContext.ruleType.id}:${this.ruleContext.ruleId}: execution failed`, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts index f16c04565872b2..dd5f85dac298d6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts @@ -16,8 +16,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/147512 - describe.skip('eventLog', () => { + describe('eventLog', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); @@ -65,13 +64,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const errorEvents = someEvents.filter( (event) => event?.kibana?.alerting?.status === 'error' ); - if (errorEvents.length === 0) { - throw new Error('no execute/error events yet'); + if (errorEvents.length < 2) { + throw new Error('not enough execute/error events yet'); } return errorEvents; }); - const event = events[0]; + const event = events[1]; expect(event).to.be.ok(); validateEvent(event, { From 7ac74799c4c4f2f5252cf699560f8e8192210b5a Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 31 Jan 2023 19:39:01 +0100 Subject: [PATCH 09/59] [Lens] Fix formula validation issue with non-default locale (#149806) ## Summary Fix #149803 This PR addresses the problem with i18n validation on formula's content, centralising the default node type into a locale-based type. I've also added some i18n functional tests as suggested by @stratoula : one for this specific bug and some smokescreen ones for Lens. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Stratoula Kalafateli Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../operations/definitions/formula/util.ts | 2 +- .../definitions/formula/validation.ts | 10 +- x-pack/test/localization/tests/index.ts | 1 + .../test/localization/tests/lens/formula.ts | 33 + x-pack/test/localization/tests/lens/index.ts | 77 ++ .../localization/tests/lens/smokescreen.ts | 881 ++++++++++++++++++ x-pack/test/localization/tests/login_page.ts | 14 +- x-pack/test/localization/tests/utils.ts | 19 + 8 files changed, 1019 insertions(+), 18 deletions(-) create mode 100644 x-pack/test/localization/tests/lens/formula.ts create mode 100644 x-pack/test/localization/tests/lens/index.ts create mode 100644 x-pack/test/localization/tests/lens/smokescreen.ts create mode 100644 x-pack/test/localization/tests/utils.ts diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts index 4c310b5efa5d3b..1d3301bd050e65 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/util.ts @@ -107,7 +107,7 @@ export function getOperationParams( }, {}); } -function getTypeI18n(type: string) { +export function getTypeI18n(type: string) { if (type === 'number') { return i18n.translate('xpack.lens.formula.number', { defaultMessage: 'number' }); } diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts index 0951f950310cb2..364103ed3e3659 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/validation.ts @@ -23,6 +23,7 @@ import { findMathNodes, findVariables, getOperationParams, + getTypeI18n, getValueOrName, groupArgsByType, isMathNode, @@ -121,6 +122,8 @@ export interface ErrorWrapper { severity?: 'error' | 'warning'; } +const DEFAULT_RETURN_TYPE = getTypeI18n('number'); + function getNodeLocation(node: TinymathFunction): TinymathLocation[] { return [node.location].filter(nonNullable); } @@ -131,11 +134,11 @@ function getArgumentType(arg: TinymathAST, operations: Record { loadTestFile(require.resolve('./login_page')); + loadTestFile(require.resolve('./lens')); }); } diff --git a/x-pack/test/localization/tests/lens/formula.ts b/x-pack/test/localization/tests/lens/formula.ts new file mode 100644 index 00000000000000..3a260e1becbe1e --- /dev/null +++ b/x-pack/test/localization/tests/lens/formula.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens']); + const elasticChart = getService('elasticChart'); + + describe('lens formula tests', () => { + it('should allow creation of a lens chart via formula', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await elasticChart.setNewChartUiDebugFlag(true); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'formula', + formula: `count() + average(bytes)`, + }); + + expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(0); + const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data).to.be.ok(); + }); + }); +} diff --git a/x-pack/test/localization/tests/lens/index.ts b/x-pack/test/localization/tests/lens/index.ts new file mode 100644 index 00000000000000..0f4c2103578339 --- /dev/null +++ b/x-pack/test/localization/tests/lens/index.ts @@ -0,0 +1,77 @@ +/* + * 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 { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const PageObjects = getPageObjects(['timePicker']); + const browser = getService('browser'); + const config = getService('config'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + let remoteEsArchiver; + + describe('lens app', () => { + const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; + const localIndexPatternString = 'logstash-*'; + const remoteIndexPatternString = 'ftr-remote:logstash-*'; + const localFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/default', + }; + + const remoteFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/default', + }; + let esNode: EsArchiver; + let fixtureDirs: { + lensBasic: string; + lensDefault: string; + }; + let indexPatternString: string; + before(async () => { + log.debug('Starting lens before method'); + await browser.setWindowSize(1280, 1200); + await kibanaServer.savedObjects.cleanStandardList(); + try { + config.get('esTestCluster.ccs'); + remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + esNode = remoteEsArchiver; + fixtureDirs = remoteFixtures; + indexPatternString = remoteIndexPatternString; + } catch (error) { + esNode = esArchiver; + fixtureDirs = localFixtures; + indexPatternString = localIndexPatternString; + } + + await esNode.load(esArchive); + // changing the timepicker default here saves us from having to set it in Discover (~8s) + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + defaultIndex: indexPatternString, + 'dateFormat:tz': 'UTC', + }); + await kibanaServer.importExport.load(fixtureDirs.lensBasic); + await kibanaServer.importExport.load(fixtureDirs.lensDefault); + }); + + after(async () => { + await esArchiver.unload(esArchive); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.importExport.unload(fixtureDirs.lensBasic); + await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./formula')); + }); +} diff --git a/x-pack/test/localization/tests/lens/smokescreen.ts b/x-pack/test/localization/tests/lens/smokescreen.ts new file mode 100644 index 00000000000000..3414cc89bba654 --- /dev/null +++ b/x-pack/test/localization/tests/lens/smokescreen.ts @@ -0,0 +1,881 @@ +/* + * 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 { range } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { getI18nLocaleFromServerArgs } from '../utils'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const find = getService('find'); + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const elasticChart = getService('elasticChart'); + const filterBar = getService('filterBar'); + const retry = getService('retry'); + const config = getService('config'); + + function getTranslationFr(term: string) { + switch (term) { + case 'legacyMetric': + return 'Ancien indicateur'; + case 'datatable': + return 'Tableau'; + case 'bar': + return 'Vertical à barres'; + case 'bar_stacked': + return 'Vertical à barres empilées'; + case 'line': + return 'Ligne'; + case 'donut': + return 'Graphique en anneau'; + case 'pie': + return 'Camembert'; + case 'treemap': + return 'Compartimentage'; + case 'heatmap': + return 'Carte thermique'; + case 'Percent': + return 'Pourcent'; + case 'Number': + return 'Nombre'; + case 'Linear': + return 'Linéaire'; + case 'Records': + return 'Enregistrements'; + case 'records': + return 'enregistrements'; + case 'moving_average': + return 'Moyenne mobile de'; + case 'sum': + return 'somme'; + default: + return term; + } + } + + function getTranslationJa(term: string) { + switch (term) { + case 'legacyMetric': + return 'レガシーメトリック'; + case 'datatable': + return '表'; + case 'bar': + return '縦棒'; + case 'bar_stacked': + return '積み上げ縦棒'; + case 'line': + return '折れ線'; + case 'donut': + return 'ドーナッツ'; + case 'pie': + return '円'; + case 'treemap': + return 'ツリーマップ'; + case 'heatmap': + return 'ヒートマップ'; + case 'Number': + return '数字'; + case 'Percent': + return '割合(%)'; + case 'Linear': + return '線形'; + case 'Records': + case 'records': + return '記録'; + case 'moving_average': + return 'の移動平均'; + case 'sum': + return '合計'; + default: + return term; + } + } + + function getTranslationZh(term: string) { + switch (term) { + case 'legacyMetric': + return '旧版指标'; + case 'datatable': + return '表'; + case 'bar': + return '垂直条形图'; + case 'bar_stacked': + return '垂直堆积条形图'; + case 'line': + return '折线图'; + case 'donut': + return '圆环图'; + case 'pie': + return '饼图'; + case 'treemap': + return '树状图'; + case 'heatmap': + return '热图'; + case 'Number': + return '数字'; + case 'Percent': + return '百分比'; + case 'Linear': + return '线性'; + case 'Records': + case 'records': + return '记录'; + case 'moving_average': + return '的移动平均值'; + case 'sum': + return '求和'; + default: + return term; + } + } + + function getExpectedI18nTranslator(locale: string): (chartType: string) => string { + switch (locale) { + case 'ja-JP': + return getTranslationJa; + case 'zh-CN': + return getTranslationZh; + case 'fr-FR': + return getTranslationFr; + default: + return (v: string) => v; + } + } + + describe('lens smokescreen tests', () => { + let termTranslator: (chartType: string) => string; + + before(async () => { + const serverArgs: string[] = config.get('kbnTestServer.serverArgs'); + const kbnServerLocale = getI18nLocaleFromServerArgs(serverArgs); + termTranslator = getExpectedI18nTranslator(kbnServerLocale); + }); + + it('should allow creation of lens xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: '@message.raw', + }); + + await PageObjects.lens.switchToVisualization('lnsDatatable', termTranslator('datatable')); + await PageObjects.lens.removeDimension('lnsDatatable_rows'); + await PageObjects.lens.switchToVisualization('bar_stacked', termTranslator('bar_stacked')); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'ip', + }); + + await PageObjects.lens.save('Afancilenstest'); + + // Ensure the visualization shows up in the visualize list, and takes + // us back to the visualization as we configured it. + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Afancilenstest'); + await PageObjects.lens.clickVisualizeListItemTitle('Afancilenstest'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.waitForVisualization('xyVisChart'); + + expect(await PageObjects.lens.getTitle()).to.eql('Afancilenstest'); + + // .echLegendItem__title is the only viable way of getting the xy chart's + // legend item(s), so we're using a class selector here. + // 4th item is the other bucket + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(4); + }); + + it('should create an xy visualization with filters aggregation', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + // Change the IP field to filters + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', + operation: 'filters', + keepOpen: true, + }); + await PageObjects.lens.addFilterToAgg(`geo.src : CN`); + await PageObjects.lens.waitForVisualization('xyVisChart'); + + // Verify that the field was persisted from the transition + expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + }); + + it('should transition from metric to table to metric', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertLegacyMetric('Maximum of bytes', '19,986'); + await PageObjects.lens.switchToVisualization('lnsDatatable', termTranslator('datatable')); + expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('Maximum of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('19,986'); + await PageObjects.lens.switchToVisualization( + 'lnsLegacyMetric', + termTranslator('legacyMetric') + ); + await PageObjects.lens.assertLegacyMetric('Maximum of bytes', '19,986'); + }); + + it('should transition from a multi-layer stacked bar to a multi-layer line chart and correctly remove all layers', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.createLayer(); + + expect(await PageObjects.lens.hasChartSwitchWarning('line', termTranslator('line'))).to.eql( + false + ); + + await PageObjects.lens.switchToVisualization('line', termTranslator('line')); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'median', + field: 'bytes', + }); + + expect(await PageObjects.lens.getLayerCount()).to.eql(2); + await PageObjects.lens.removeLayer(); + await PageObjects.lens.removeLayer(); + await testSubjects.existOrFail('workspace-drag-drop-prompt'); + }); + + it('should edit settings of xy line chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await testSubjects.click('lnsXY_splitDimensionPanel > indexPattern-dimension-remove'); + await PageObjects.lens.switchToVisualization('line', termTranslator('line')); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'max', + field: 'memory', + keepOpen: true, + }); + await PageObjects.lens.editDimensionLabel('Test of label'); + await PageObjects.lens.editDimensionFormat(termTranslator('Percent')); + await PageObjects.lens.editDimensionColor('#ff0000'); + await PageObjects.lens.openVisualOptions(); + + await PageObjects.lens.useCurvedLines(); + await PageObjects.lens.editMissingValues('Linear'); + + await PageObjects.lens.assertMissingValues(termTranslator('Linear')); + + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); + await PageObjects.lens.assertColor('#ff0000'); + + await testSubjects.existOrFail('indexPattern-dimension-formatDecimals'); + + await PageObjects.lens.closeDimensionEditor(); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Test of label' + ); + }); + + it('should not show static value tab for data layers', async () => { + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); + // Quick functions and Formula tabs should be visible + expect(await testSubjects.exists('lens-dimensionTabs-quickFunctions')).to.eql(true); + expect(await testSubjects.exists('lens-dimensionTabs-formula')).to.eql(true); + // Static value tab should not be visible + expect(await testSubjects.exists('lens-dimensionTabs-static_value')).to.eql(false); + + await PageObjects.lens.closeDimensionEditor(); + }); + + it('should be able to add very long labels and still be able to remove a dimension', async () => { + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); + const longLabel = + 'Veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryvery long label wrapping multiple lines'; + await PageObjects.lens.editDimensionLabel(longLabel); + await PageObjects.lens.waitForVisualization('xyVisChart'); + await PageObjects.lens.closeDimensionEditor(); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + longLabel + ); + expect( + await testSubjects.isDisplayed('lnsXY_yDimensionPanel > indexPattern-dimension-remove') + ).to.equal(true); + await PageObjects.lens.removeDimension('lnsXY_yDimensionPanel'); + await testSubjects.missingOrFail('lnsXY_yDimensionPanel > lns-dimensionTrigger'); + }); + + it('should allow creation of a multi-axis chart and switching multiple times', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await elasticChart.setNewChartUiDebugFlag(true); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('bar', termTranslator('bar')); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.dest', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'unique_count', + field: 'bytes', + keepOpen: true, + }); + + await PageObjects.lens.changeAxisSide('right'); + let data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data?.axes?.y.length).to.eql(2); + expect(data?.axes?.y.some(({ position }) => position === 'right')).to.eql(true); + + await PageObjects.lens.changeAxisSide('left'); + data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data?.axes?.y.length).to.eql(1); + expect(data?.axes?.y.some(({ position }) => position === 'right')).to.eql(false); + + await PageObjects.lens.changeAxisSide('right'); + await PageObjects.lens.waitForVisualization('xyVisChart'); + + await PageObjects.lens.closeDimensionEditor(); + }); + + it('should show value labels on bar charts when enabled', async () => { + // enable value labels + await PageObjects.lens.openVisualOptions(); + await testSubjects.click('lns_valueLabels_inside'); + + // check for value labels + let data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data?.bars?.[0].labels).not.to.eql(0); + + // switch to stacked bar chart + await PageObjects.lens.switchToVisualization('bar_stacked', termTranslator('bar_stacked')); + + // check for value labels + data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data?.bars?.[0].labels).not.to.eql(0); + }); + + it('should override axis title', async () => { + const axisTitle = 'overridden axis'; + await PageObjects.lens.toggleToolbarPopover('lnsLeftAxisButton'); + await testSubjects.setValue('lnsyLeftAxisTitle', axisTitle, { + clearWithKeyboard: true, + }); + + let data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data?.axes?.y?.[1].title).to.eql(axisTitle); + + // hide the gridlines + await testSubjects.click('lnsshowyLeftAxisGridlines'); + + data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + expect(data?.axes?.y?.[1].gridlines.length).to.eql(0); + }); + + it('should transition from line chart to donut chart and to bar chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + expect(await PageObjects.lens.hasChartSwitchWarning('donut', termTranslator('donut'))).to.eql( + true + ); + await PageObjects.lens.switchToVisualization('donut', termTranslator('donut')); + + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql( + 'Top values of ip' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + + expect(await PageObjects.lens.hasChartSwitchWarning('bar', termTranslator('bar'))).to.eql( + false + ); + await PageObjects.lens.switchToVisualization('bar', termTranslator('bar')); + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + 'Top values of ip' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + + it('should transition from bar chart to line chart using layer chart switch', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchLayerSeriesType('line'); + expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Average of bytes' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_splitDimensionPanel')).to.eql( + 'Top values of ip' + ); + }); + + it('should transition from pie chart to treemap chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsPieVis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsPieVis'); + await PageObjects.lens.goToTimeRange(); + expect( + await PageObjects.lens.hasChartSwitchWarning('treemap', termTranslator('treemap')) + ).to.eql(false); + await PageObjects.lens.switchToVisualization('treemap', termTranslator('treemap')); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsPie_groupByDimensionPanel') + ).to.eql(['Top values of geo.dest', 'Top values of geo.src']); + expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( + 'Average of bytes' + ); + }); + + it('should create a pie chart and switch to datatable', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('pie', termTranslator('pie')); + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + disableEmptyRows: true, + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + expect( + await PageObjects.lens.hasChartSwitchWarning('lnsDatatable', termTranslator('datatable')) + ).to.eql(false); + await PageObjects.lens.switchToVisualization('lnsDatatable', termTranslator('datatable')); + + // Need to provide a fn for these + // expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('@timestamp per 3 hours'); + // expect(await PageObjects.lens.getDatatableHeaderText(1)).to.eql('Average of bytes'); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2015-09-20 00:00'); + expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('6,011.351'); + }); + + it('should create a heatmap chart and transition to barchart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('heatmap', termTranslator('heatmap')); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsHeatmap_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsHeatmap_yDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.dest', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsHeatmap_cellPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + expect(await PageObjects.lens.hasChartSwitchWarning('bar', termTranslator('bar'))).to.eql( + false + ); + await PageObjects.lens.switchToVisualization('bar', termTranslator('bar')); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.contain( + 'bytes' + ); + }); + + it('should create a valid XY chart with references', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'moving_average', + keepOpen: true, + }); + await PageObjects.lens.configureReference({ + operation: termTranslator('sum'), + field: 'bytes', + }); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'cumulative_sum', + keepOpen: true, + }); + await PageObjects.lens.configureReference({ + field: termTranslator('Records'), + }); + await PageObjects.lens.closeDimensionEditor(); + + // Two Y axes that are both valid + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + }); + + it('should allow formatting on references', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable', termTranslator('datatable')); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_rows > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + disableEmptyRows: true, + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'moving_average', + keepOpen: true, + }); + await PageObjects.lens.configureReference({ + operation: termTranslator('sum'), + field: 'bytes', + }); + await PageObjects.lens.editDimensionFormat(termTranslator('Number')); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.waitForVisualization(); + + const values = await Promise.all( + range(0, 6).map((index) => PageObjects.lens.getDatatableCellText(index, 1)) + ); + expect(values).to.eql([ + '-', + '222,420.00', + '702,050.00', + '1,879,613.33', + '3,482,256.25', + '4,359,953.00', + ]); + }); + + /** + * The edge cases are: + * + * 1. Showing errors when creating a partial configuration + * 2. Being able to drag in a new field while in partial config + * 3. Being able to switch charts while in partial config + */ + it('should handle edge cases in reference-based operations', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'cumulative_sum', + }); + expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(1); + + await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel'); + expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(2); + + await PageObjects.lens.dragFieldToDimensionTrigger( + '@timestamp', + 'lnsXY_xDimensionPanel > lns-empty-dimension' + ); + expect(await PageObjects.lens.getWorkspaceErrorCount()).to.eql(1); + + expect( + await PageObjects.lens.hasChartSwitchWarning('lnsDatatable', termTranslator('datatable')) + ).to.eql(false); + await PageObjects.lens.switchToVisualization('lnsDatatable', termTranslator('datatable')); + + // TODO: fix this later on + // expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_metrics')).to.eql( + // 'Cumulative sum of (incomplete)' + // ); + }); + + it('should keep the field selection while transitioning to every reference-based operation', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'counter_rate', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'cumulative_sum', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'differences', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'moving_average', + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.contain( + 'bytes' + ); + }); + + it('should not leave an incomplete column in the visualization config with field-based operation', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'min', + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + undefined + ); + }); + + it('should revert to previous configuration and not leave an incomplete column in the visualization config with reference-based operations', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'moving_average', + field: termTranslator('Records'), + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.contain( + termTranslator('moving_average') + ); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'median', + isPreviousIncompatible: true, + keepOpen: true, + }); + + expect(await PageObjects.lens.isDimensionEditorOpen()).to.eql(true); + + await PageObjects.lens.closeDimensionEditor(); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.contain( + termTranslator('moving_average') + ); + }); + + it('should transition from unique count to last value', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'unique_count', + field: 'ip', + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'last_value', + field: 'bytes', + isPreviousIncompatible: true, + }); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.contain( + 'bytes' + ); + }); + + it('should allow to change index pattern', async () => { + let indexPatternString; + if (config.get('esTestCluster.ccs')) { + indexPatternString = 'ftr-remote:log*'; + } else { + indexPatternString = 'log*'; + } + await PageObjects.lens.switchFirstLayerIndexPattern(indexPatternString); + expect(await PageObjects.lens.getFirstLayerIndexPattern()).to.equal(indexPatternString); + }); + + it('should allow filtering by legend on an xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'extension.raw', + }); + + await PageObjects.lens.filterLegend('jpg'); + const hasExtensionFilter = await filterBar.hasFilter('extension.raw', 'jpg'); + expect(hasExtensionFilter).to.be(true); + + await filterBar.removeFilter('extension.raw'); + }); + + it('should allow filtering by legend on a pie chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('pie', termTranslator('pie')); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'extension.raw', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'agent.raw', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.filterLegend('jpg'); + const hasExtensionFilter = await filterBar.hasFilter('extension.raw', 'jpg'); + expect(hasExtensionFilter).to.be(true); + + await filterBar.removeFilter('extension.raw'); + }); + + it('should show visual options button group for a donut chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.switchToVisualization('donut', termTranslator('donut')); + + const hasVisualOptionsButton = await PageObjects.lens.hasVisualOptionsButton(); + expect(hasVisualOptionsButton).to.be(true); + + await PageObjects.lens.openVisualOptions(); + await retry.try(async () => { + expect(await PageObjects.lens.hasEmptySizeRatioButtonGroup()).to.be(true); + }); + }); + + it('should not show visual options button group for a pie chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.switchToVisualization('pie', termTranslator('pie')); + + const hasVisualOptionsButton = await PageObjects.lens.hasVisualOptionsButton(); + expect(hasVisualOptionsButton).to.be(false); + }); + }); +} diff --git a/x-pack/test/localization/tests/login_page.ts b/x-pack/test/localization/tests/login_page.ts index 6ff2b3a9b47575..613720601d2434 100644 --- a/x-pack/test/localization/tests/login_page.ts +++ b/x-pack/test/localization/tests/login_page.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../ftr_provider_context'; +import { getI18nLocaleFromServerArgs } from './utils'; /** * Strings Needs to be hardcoded since getting it from the i18n.translate @@ -28,19 +29,6 @@ function getExpectedI18nTranslation(locale: string): string | undefined { } } -function getI18nLocaleFromServerArgs(kbnServerArgs: string[]): string { - const re = /--i18n\.locale=(?.*)/; - for (const serverArg of kbnServerArgs) { - const match = re.exec(serverArg); - const locale = match?.groups?.locale; - if (locale) { - return locale; - } - } - - throw Error('i18n.locale is not set in the server arguments'); -} - export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const config = getService('config'); diff --git a/x-pack/test/localization/tests/utils.ts b/x-pack/test/localization/tests/utils.ts new file mode 100644 index 00000000000000..4ac2a970ef02c0 --- /dev/null +++ b/x-pack/test/localization/tests/utils.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. + */ + +export function getI18nLocaleFromServerArgs(kbnServerArgs: string[]): string { + const re = /--i18n\.locale=(?.*)/; + for (const serverArg of kbnServerArgs) { + const match = re.exec(serverArg); + const locale = match?.groups?.locale; + if (locale) { + return locale; + } + } + + throw Error('i18n.locale is not set in the server arguments'); +} From a32b5a450adc704ddfd072a23cc9e2204dd07279 Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Tue, 31 Jan 2023 11:54:42 -0700 Subject: [PATCH 10/59] [Custom Branding] Replace EuiLoadingElastic with EuiLoadingSpinner (#149253) --- .../src/ui/app_container.tsx | 10 ++++++- .../analytics_no_data_page.component.test.tsx | 2 ++ .../src/analytics_no_data_page.component.tsx | 10 ++++++- .../impl/src/analytics_no_data_page.test.tsx | 17 +++++++++++- .../impl/src/analytics_no_data_page.tsx | 6 ++++- .../analytics_no_data/impl/src/services.tsx | 8 +++--- .../page/analytics_no_data/mocks/index.ts | 1 + .../page/analytics_no_data/mocks/src/jest.ts | 13 +++++++++ .../analytics_no_data/mocks/src/storybook.ts | 12 ++++++++- .../page/analytics_no_data/types/index.d.ts | 5 ++++ .../impl/src/kibana_no_data_page.test.tsx | 27 ++++++++++++++++--- .../impl/src/kibana_no_data_page.tsx | 9 +++++-- .../page/kibana_no_data/mocks/src/jest.ts | 2 ++ .../kibana_no_data/mocks/src/storybook.ts | 6 ++++- .../page/kibana_no_data/types/index.d.ts | 2 ++ .../no_data/dashboard_app_no_data.tsx | 4 +++ src/plugins/dashboard/public/plugin.tsx | 2 ++ .../custom_branding/custom_branding.stub.ts | 20 ++++++++++++++ .../custom_branding_service.ts | 23 ++++++++++++++++ .../public/services/custom_branding/types.ts | 13 +++++++++ .../public/services/plugin_services.stub.ts | 2 ++ .../public/services/plugin_services.ts | 2 ++ .../dashboard/public/services/types.ts | 2 ++ src/plugins/dashboard/tsconfig.json | 1 + .../__snapshots__/overview.test.tsx.snap | 1 + .../public/components/overview/overview.tsx | 14 ++++++++-- 26 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 src/plugins/dashboard/public/services/custom_branding/custom_branding.stub.ts create mode 100644 src/plugins/dashboard/public/services/custom_branding/custom_branding_service.ts create mode 100644 src/plugins/dashboard/public/services/custom_branding/types.ts diff --git a/packages/core/application/core-application-browser-internal/src/ui/app_container.tsx b/packages/core/application/core-application-browser-internal/src/ui/app_container.tsx index c7fafa498bef49..11899a938da9f1 100644 --- a/packages/core/application/core-application-browser-internal/src/ui/app_container.tsx +++ b/packages/core/application/core-application-browser-internal/src/ui/app_container.tsx @@ -126,7 +126,15 @@ export const AppContainer: FC = ({ const AppLoadingPlaceholder: FC<{ showPlainSpinner: boolean }> = ({ showPlainSpinner }) => { if (showPlainSpinner) { - return ; + return ( + + ); } return ( { ); @@ -50,6 +51,7 @@ describe('AnalyticsNoDataPageComponent', () => { onDataViewCreated={onDataViewCreated} kibanaGuideDocLink={'http://www.test.com'} allowAdHocDataView={true} + showPlainSpinner={false} /> ); diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx index fe607b70120df8..d67cb082f5539c 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx @@ -19,6 +19,8 @@ export interface Props { onDataViewCreated: (dataView: unknown) => void; /** if set to true allows creation of an ad-hoc dataview from data view editor */ allowAdHocDataView?: boolean; + /** if the kibana instance is customly branded */ + showPlainSpinner: boolean; } const solution = i18n.translate('sharedUXPackages.noDataConfig.analytics', { @@ -47,6 +49,7 @@ export const AnalyticsNoDataPage = ({ kibanaGuideDocLink, onDataViewCreated, allowAdHocDataView, + showPlainSpinner, }: Props) => { const noDataConfig = { solution, @@ -61,5 +64,10 @@ export const AnalyticsNoDataPage = ({ }, docsLink: kibanaGuideDocLink, }; - return ; + + return ( + + ); }; diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx index 996b9d062becf4..c73f61e6c0e827 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx @@ -9,7 +9,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { getAnalyticsNoDataPageServicesMock } from '@kbn/shared-ux-page-analytics-no-data-mocks'; +import { + getAnalyticsNoDataPageServicesMock, + getAnalyticsNoDataPageServicesMockWithCustomBranding, +} from '@kbn/shared-ux-page-analytics-no-data-mocks'; import { AnalyticsNoDataPageProvider } from './services'; import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component'; @@ -19,6 +22,7 @@ describe('AnalyticsNoDataPage', () => { const onDataViewCreated = jest.fn(); const services = getAnalyticsNoDataPageServicesMock(); + const servicesWithCustomBranding = getAnalyticsNoDataPageServicesMockWithCustomBranding(); afterAll(() => { jest.resetAllMocks(); @@ -38,4 +42,15 @@ describe('AnalyticsNoDataPage', () => { expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated); expect(component.find(Component).props().allowAdHocDataView).toBe(true); }); + + it('passes correct boolean value to showPlainSpinner', () => { + const component = mountWithIntl( + + + + ); + + expect(component.find(Component).length).toBe(1); + expect(component.find(Component).props().showPlainSpinner).toBe(true); + }); }); diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx index df1fc2486c1b37..9b600c374dd02c 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { AnalyticsNoDataPageProps } from '@kbn/shared-ux-page-analytics-no-data-types'; +import useObservable from 'react-use/lib/useObservable'; import { useServices } from './services'; import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component'; @@ -20,7 +21,9 @@ export const AnalyticsNoDataPage = ({ allowAdHocDataView, }: AnalyticsNoDataPageProps) => { const services = useServices(); - const { kibanaGuideDocLink } = services; + const { kibanaGuideDocLink, customBranding } = services; + const { hasCustomBranding$ } = customBranding; + const showPlainSpinner = useObservable(hasCustomBranding$) ?? false; return ( ); diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx index bd486be8f8976f..991893aeca501a 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/services.tsx @@ -27,10 +27,10 @@ export const AnalyticsNoDataPageProvider: FC = ({ children, ...services }) => { - const { kibanaGuideDocLink } = services; + const { kibanaGuideDocLink, customBranding } = services; return ( - + {children} ); @@ -45,8 +45,10 @@ export const AnalyticsNoDataPageKibanaProvider: FC { const value: Services = { kibanaGuideDocLink: dependencies.coreStart.docLinks.links.kibana.guide, + customBranding: { + hasCustomBranding$: dependencies.coreStart.customBranding.hasCustomBranding$, + }, }; - return ( {children} diff --git a/packages/shared-ux/page/analytics_no_data/mocks/index.ts b/packages/shared-ux/page/analytics_no_data/mocks/index.ts index cc73dc378452bd..1f1ac86e9d2476 100644 --- a/packages/shared-ux/page/analytics_no_data/mocks/index.ts +++ b/packages/shared-ux/page/analytics_no_data/mocks/index.ts @@ -7,6 +7,7 @@ */ export { getServicesMock as getAnalyticsNoDataPageServicesMock } from './src/jest'; +export { getServicesMockCustomBranding as getAnalyticsNoDataPageServicesMockWithCustomBranding } from './src/jest'; export { StorybookMock as AnalyticsNoDataPageStorybookMock } from './src/storybook'; export type { Params as AnalyticsNoDataPageStorybookParams } from './src/storybook'; diff --git a/packages/shared-ux/page/analytics_no_data/mocks/src/jest.ts b/packages/shared-ux/page/analytics_no_data/mocks/src/jest.ts index 29a79c40054aa3..98885d55ba47d9 100644 --- a/packages/shared-ux/page/analytics_no_data/mocks/src/jest.ts +++ b/packages/shared-ux/page/analytics_no_data/mocks/src/jest.ts @@ -8,11 +8,24 @@ import type { AnalyticsNoDataPageServices } from '@kbn/shared-ux-page-analytics-no-data-types'; import { getKibanaNoDataPageServicesMock } from '@kbn/shared-ux-page-kibana-no-data-mocks'; +import { of } from 'rxjs'; export const getServicesMock = () => { const services: AnalyticsNoDataPageServices = { ...getKibanaNoDataPageServicesMock(), kibanaGuideDocLink: 'Kibana guide', + customBranding: { hasCustomBranding$: of(false) }, + }; + + return services; +}; + +export const getServicesMockCustomBranding = () => { + const services: AnalyticsNoDataPageServices = { + ...getKibanaNoDataPageServicesMock(), + // this mock will have custom branding set to true + customBranding: { hasCustomBranding$: of(true) }, + kibanaGuideDocLink: 'Kibana guide', }; return services; diff --git a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts index c909795647c4e6..86bf25dbde9e92 100644 --- a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts +++ b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts @@ -15,8 +15,9 @@ import type { AnalyticsNoDataPageServices, AnalyticsNoDataPageProps, } from '@kbn/shared-ux-page-analytics-no-data-types'; +import { of } from 'rxjs'; -type ServiceArguments = Pick; +type ServiceArguments = Pick; export type Params = ArgumentParams<{}, ServiceArguments> & KibanaNoDataPageStorybookParams; @@ -34,6 +35,12 @@ export class StorybookMock extends AbstractStorybookMock< control: 'text', defaultValue: 'Kibana guide', }, + customBranding: { + hasCustomBranding$: { + control: 'boolean', + defaultValue: false, + }, + }, }; dependencies = [kibanaNoDataMock]; @@ -41,6 +48,9 @@ export class StorybookMock extends AbstractStorybookMock< getServices(params: Params): AnalyticsNoDataPageServices { return { kibanaGuideDocLink: 'Kibana guide', + customBranding: { + hasCustomBranding$: of(false), + }, ...kibanaNoDataMock.getServices(params), }; } diff --git a/packages/shared-ux/page/analytics_no_data/types/index.d.ts b/packages/shared-ux/page/analytics_no_data/types/index.d.ts index d4021360bea236..4e54315f071dd1 100644 --- a/packages/shared-ux/page/analytics_no_data/types/index.d.ts +++ b/packages/shared-ux/page/analytics_no_data/types/index.d.ts @@ -9,12 +9,14 @@ import { KibanaNoDataPageServices, KibanaNoDataPageKibanaDependencies, } from '@kbn/shared-ux-page-kibana-no-data-types'; +import { Observable } from 'rxjs'; /** * A list of services that are consumed by this component. */ export interface Services { kibanaGuideDocLink: string; + customBranding: { hasCustomBranding$: Observable }; } /** @@ -31,6 +33,9 @@ export interface KibanaDependencies { }; }; }; + customBranding: { + hasCustomBranding$: Observable; + }; }; } diff --git a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.test.tsx b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.test.tsx index c15a5c061dd1ba..a3484719a49ede 100644 --- a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.test.tsx +++ b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { EuiLoadingElastic } from '@elastic/eui'; +import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views'; import { NoDataConfigPage } from '@kbn/shared-ux-page-no-data-config'; @@ -46,7 +46,7 @@ describe('Kibana No Data Page', () => { const services = getKibanaNoDataPageServicesMock(config); const component = mountWithIntl( - + ); @@ -61,7 +61,11 @@ describe('Kibana No Data Page', () => { const services = getKibanaNoDataPageServicesMock({ ...config, hasESData: true }); const component = mountWithIntl( - + ); @@ -86,7 +90,11 @@ describe('Kibana No Data Page', () => { const component = mountWithIntl( - + ); @@ -96,4 +104,15 @@ describe('Kibana No Data Page', () => { expect(component.find(NoDataViewsPrompt).length).toBe(0); expect(component.find(NoDataConfigPage).length).toBe(0); }); + + test('shows EuiLoadingSpinner vs EuiLoadingElastic for custom branding', () => { + const services = getKibanaNoDataPageServicesMock(config); + const component = mountWithIntl( + + + + ); + expect(component.find(EuiLoadingSpinner).length).toBe(1); + expect(component.find(EuiLoadingElastic).length).toBe(0); + }); }); diff --git a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx index c3fbccd3a60fb8..2773184b087bb2 100644 --- a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx +++ b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import React, { useEffect, useState } from 'react'; -import { EuiLoadingElastic } from '@elastic/eui'; +import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui'; import { NoDataConfigPage } from '@kbn/shared-ux-page-no-data-config'; import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views'; import { KibanaNoDataPageProps } from '@kbn/shared-ux-page-kibana-no-data-types'; @@ -20,6 +20,7 @@ export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig, allowAdHocDataView, + showPlainSpinner, }: KibanaNoDataPageProps) => { // These hooks are temporary, until this component is moved to a package. const services = useServices(); @@ -43,7 +44,11 @@ export const KibanaNoDataPage = ({ }, [hasESData, hasUserDataView]); if (isLoading) { - return ; + return showPlainSpinner ? ( + + ) : ( + + ); } if (!hasUserDataViews && dataExists) { diff --git a/packages/shared-ux/page/kibana_no_data/mocks/src/jest.ts b/packages/shared-ux/page/kibana_no_data/mocks/src/jest.ts index 5f2f6b309e56c2..dc46f222866467 100644 --- a/packages/shared-ux/page/kibana_no_data/mocks/src/jest.ts +++ b/packages/shared-ux/page/kibana_no_data/mocks/src/jest.ts @@ -13,11 +13,13 @@ import { getNoDataViewsPromptServicesMock } from '@kbn/shared-ux-prompt-no-data- interface Params { hasESData: boolean; hasUserDataView: boolean; + showPlainSpinner: boolean; } const defaultParams = { hasESData: true, hasUserDataView: true, + showPlainSpinner: false, }; /** diff --git a/packages/shared-ux/page/kibana_no_data/mocks/src/storybook.ts b/packages/shared-ux/page/kibana_no_data/mocks/src/storybook.ts index 1f4a7453e59b6b..10cc9a0f40961c 100644 --- a/packages/shared-ux/page/kibana_no_data/mocks/src/storybook.ts +++ b/packages/shared-ux/page/kibana_no_data/mocks/src/storybook.ts @@ -79,7 +79,11 @@ export class StorybookMock extends AbstractStorybookMock< docsLink: 'http://docs.elastic.dev', }; - return { noDataConfig, onDataViewCreated: action('onDataViewCreated') }; + return { + showPlainSpinner: false, + noDataConfig, + onDataViewCreated: action('onDataViewCreated'), + }; } getServices(params: Params): KibanaNoDataPageServices { diff --git a/packages/shared-ux/page/kibana_no_data/types/index.d.ts b/packages/shared-ux/page/kibana_no_data/types/index.d.ts index 1cce51f372021a..ff9b4d845f597a 100644 --- a/packages/shared-ux/page/kibana_no_data/types/index.d.ts +++ b/packages/shared-ux/page/kibana_no_data/types/index.d.ts @@ -57,4 +57,6 @@ export interface KibanaNoDataPageProps { noDataConfig: NoDataPageProps; /** if set to true allows creation of an ad-hoc dataview from data view editor */ allowAdHocDataView?: boolean; + /** Set to true if the kibana is customly branded */ + showPlainSpinner: boolean; } diff --git a/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx b/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx index 603a36bb3d59bf..fb04a3187c72c2 100644 --- a/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx +++ b/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx @@ -25,6 +25,7 @@ export const DashboardAppNoDataPage = ({ dataViewEditor, http: { basePath }, documentationLinks: { indexPatternsDocLink, kibanaGuideDocLink }, + customBranding, } = pluginServices.getServices(); const analyticsServices = { @@ -37,6 +38,9 @@ export const DashboardAppNoDataPage = ({ }, application, http: { basePath }, + customBranding: { + hasCustomBranding$: customBranding.hasCustomBranding$, + }, }, dataViews, dataViewEditor, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index fa457c425440e6..1d562877a5dea4 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -49,6 +49,7 @@ import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plu import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; import { DashboardContainerFactoryDefinition } from './dashboard_container/embeddable/dashboard_container_factory'; import { type DashboardAppLocator, @@ -97,6 +98,7 @@ export interface DashboardStartDependencies { urlForwarding: UrlForwardingStart; usageCollection?: UsageCollectionStart; visualizations: VisualizationsStart; + customBranding: CustomBrandingStart; } export interface DashboardSetup { diff --git a/src/plugins/dashboard/public/services/custom_branding/custom_branding.stub.ts b/src/plugins/dashboard/public/services/custom_branding/custom_branding.stub.ts new file mode 100644 index 00000000000000..5496c29b760f96 --- /dev/null +++ b/src/plugins/dashboard/public/services/custom_branding/custom_branding.stub.ts @@ -0,0 +1,20 @@ +/* + * 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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { DashboardCustomBrandingService } from './types'; + +type CustomBrandingServiceFactory = PluginServiceFactory; + +export const customBrandingServiceFactory: CustomBrandingServiceFactory = () => { + const pluginMock = coreMock.createStart(); + return { + hasCustomBranding$: pluginMock.customBranding.hasCustomBranding$, + }; +}; diff --git a/src/plugins/dashboard/public/services/custom_branding/custom_branding_service.ts b/src/plugins/dashboard/public/services/custom_branding/custom_branding_service.ts new file mode 100644 index 00000000000000..659a669a5bda10 --- /dev/null +++ b/src/plugins/dashboard/public/services/custom_branding/custom_branding_service.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 type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DashboardStartDependencies } from '../../plugin'; +import { DashboardCustomBrandingService } from './types'; + +export type CustomBrandingServiceFactory = KibanaPluginServiceFactory< + DashboardCustomBrandingService, + DashboardStartDependencies +>; + +export const customBrandingServiceFactory: CustomBrandingServiceFactory = ({ coreStart }) => { + const { customBranding } = coreStart; + return { + hasCustomBranding$: customBranding.hasCustomBranding$, + }; +}; diff --git a/src/plugins/dashboard/public/services/custom_branding/types.ts b/src/plugins/dashboard/public/services/custom_branding/types.ts new file mode 100644 index 00000000000000..7e7e88bb15a7ad --- /dev/null +++ b/src/plugins/dashboard/public/services/custom_branding/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 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 { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; + +export interface DashboardCustomBrandingService { + hasCustomBranding$: CustomBrandingStart['hasCustomBranding$']; +} diff --git a/src/plugins/dashboard/public/services/plugin_services.stub.ts b/src/plugins/dashboard/public/services/plugin_services.stub.ts index b8c39909dd61a0..eabe85288687a0 100644 --- a/src/plugins/dashboard/public/services/plugin_services.stub.ts +++ b/src/plugins/dashboard/public/services/plugin_services.stub.ts @@ -38,6 +38,7 @@ import { spacesServiceFactory } from './spaces/spaces.stub'; import { urlForwardingServiceFactory } from './url_forwarding/url_fowarding.stub'; import { visualizationsServiceFactory } from './visualizations/visualizations.stub'; import { dashboardSavedObjectServiceFactory } from './dashboard_saved_object/dashboard_saved_object.stub'; +import { customBrandingServiceFactory } from './custom_branding/custom_branding.stub'; export const providers: PluginServiceProviders = { dashboardSavedObject: new PluginServiceProvider(dashboardSavedObjectServiceFactory), @@ -64,6 +65,7 @@ export const providers: PluginServiceProviders = { urlForwarding: new PluginServiceProvider(urlForwardingServiceFactory), usageCollection: new PluginServiceProvider(usageCollectionServiceFactory), visualizations: new PluginServiceProvider(visualizationsServiceFactory), + customBranding: new PluginServiceProvider(customBrandingServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index b4ee1b566a8ace..4382506a37948d 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -39,6 +39,7 @@ import { visualizationsServiceFactory } from './visualizations/visualizations_se import { usageCollectionServiceFactory } from './usage_collection/usage_collection_service'; import { analyticsServiceFactory } from './analytics/analytics_service'; import { dashboardSavedObjectServiceFactory } from './dashboard_saved_object/dashboard_saved_object_service'; +import { customBrandingServiceFactory } from './custom_branding/custom_branding_service'; const providers: PluginServiceProviders = { dashboardSavedObject: new PluginServiceProvider(dashboardSavedObjectServiceFactory, [ @@ -78,6 +79,7 @@ const providers: PluginServiceProviders(); diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index 5d14b59e8a125a..fc7e0acf1b5c48 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -14,6 +14,7 @@ import { DashboardAnalyticsService } from './analytics/types'; import { DashboardApplicationService } from './application/types'; import { DashboardChromeService } from './chrome/types'; import { DashboardCoreContextService } from './core_context/types'; +import { DashboardCustomBrandingService } from './custom_branding/types'; import { DashboardCapabilitiesService } from './dashboard_capabilities/types'; import { DashboardSavedObjectService } from './dashboard_saved_object/types'; import { DashboardSessionStorageServiceType } from './dashboard_session_storage/types'; @@ -64,4 +65,5 @@ export interface DashboardServices { urlForwarding: DashboardUrlForwardingService; usageCollection: DashboardUsageCollectionService; // TODO: make this optional in follow up visualizations: DashboardVisualizationsService; + customBranding: DashboardCustomBrandingService; } diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index c79c0bc2810428..8ab580ccdfe721 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -51,6 +51,7 @@ "@kbn/core-saved-objects-common", "@kbn/task-manager-plugin", "@kbn/core-execution-context-common", + "@kbn/core-custom-branding-browser", ], "exclude": [ "target/**/*", diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 3ac183860d2152..453d20385e7062 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -457,6 +457,7 @@ exports[`Overview renders correctly when there is no user data view 1`] = ` "navigateToUrl": [MockFunction], }, "chrome": undefined, + "customBranding": undefined, "docLinks": Object { "links": Object { "kibana": Object { diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index f87c90a4591d48..f6d97d54681e81 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -62,8 +62,17 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const [hasDataView, setHasDataView] = useState(false); const [isLoading, setIsLoading] = useState(true); const { services } = useKibana(); - const { http, docLinks, dataViews, share, uiSettings, application, chrome, dataViewEditor } = - services; + const { + http, + docLinks, + dataViews, + share, + uiSettings, + application, + chrome, + dataViewEditor, + customBranding, + } = services; const addBasePath = http.basePath.prepend; const IS_DARK_THEME = uiSettings.get('theme:darkMode'); @@ -177,6 +186,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => chrome, docLinks, http, + customBranding, }, dataViews: { ...dataViews, From e534d8784364c62e2ce4c7e20903a44c7f311f42 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 31 Jan 2023 12:16:44 -0700 Subject: [PATCH 11/59] [ML] Data Frame Analytics results view: add link to custom visualizations for viewing scatterplot charts (#149647) ## Summary This PR adds a link to the custom visualization UI from scatterplot matrix charts in the data frame analytics job wizard and results views. This allows you to view the data and edit the visualization inside the custom visualization editor, and then save it to a dashboard to explore the data alongside other contextual information. image Related meta issue: https://github.com/elastic/kibana/issues/131551 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../scatterplot_matrix.test.tsx | 24 ++++ .../scatterplot_matrix/scatterplot_matrix.tsx | 113 +++++++++++++++++- .../scatterplot_matrix_vega_lite_spec.test.ts | 25 +++- .../scatterplot_matrix_vega_lite_spec.ts | 62 +++++++--- .../exploration_page_wrapper.tsx | 1 + .../outlier_exploration.tsx | 1 + 6 files changed, 203 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx index c1857979e7d533..28cf8e7c6ffd23 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -12,8 +12,12 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { euiLightVars as euiThemeLight } from '@kbn/ui-theme'; +import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; + import { ScatterplotMatrix } from './scatterplot_matrix'; +const mockFilterManager = createFilterManagerMock(); + const mockEsSearch = jest.fn((body) => ({ hits: { hits: [{ fields: { x: [1], y: [2] } }, { fields: { x: [2], y: [3] } }] }, })); @@ -21,6 +25,26 @@ jest.mock('../../contexts/kibana', () => ({ useMlApiContext: () => ({ esSearch: mockEsSearch, }), + useMlKibana: () => ({ + services: { + application: { + navigateToApp: jest.fn(), + }, + data: { + query: { + filterManager: mockFilterManager, + timefilter: { + timefilter: { + getTime: jest.fn(() => { + return { from: '', to: '' }; + }), + getRefreshInterval: jest.fn(), + }, + }, + }, + }, + }, + }), })); const mockEuiTheme = euiThemeLight; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index e822d2ebd91d76..6e718e0f0ccd88 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useMemo, useEffect, useState, FC } from 'react'; - +import React, { useMemo, useEffect, useState, FC, useCallback } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import rison from '@kbn/rison'; import { EuiCallOut, @@ -17,12 +17,14 @@ import { EuiFlexItem, EuiFormRow, EuiIconTip, + EuiLink, EuiSelect, EuiSpacer, EuiSwitch, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Query } from '@kbn/data-plugin/common/query'; import { DataView } from '@kbn/data-views-plugin/public'; import { stringHash } from '@kbn/ml-string-hash'; @@ -31,7 +33,7 @@ import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; import { RuntimeMappings } from '../../../../common/types/fields'; import { getCombinedRuntimeMappings } from '../data_grid'; -import { useMlApiContext } from '../../contexts/kibana'; +import { useMlApiContext, useMlKibana } from '../../contexts/kibana'; import { getProcessedFields } from '../data_grid'; import { useCurrentEuiTheme } from '../color_range_legend'; @@ -101,6 +103,7 @@ export interface ScatterplotMatrixProps { searchQuery?: estypes.QueryDslQueryContainer; runtimeMappings?: RuntimeMappings; indexPattern?: DataView; + query?: Query; } export const ScatterplotMatrix: FC = ({ @@ -112,9 +115,13 @@ export const ScatterplotMatrix: FC = ({ searchQuery, runtimeMappings, indexPattern, + query, }) => { const { esSearch } = useMlApiContext(); - + const kibana = useMlKibana(); + const { + services: { application, data }, + } = kibana; // dynamicSize is optionally used for outlier charts where the scatterplot marks // are sized according to outlier_score const [dynamicSize, setDynamicSize] = useState(false); @@ -142,6 +149,8 @@ export const ScatterplotMatrix: FC = ({ { items: any[]; backgroundItems: any[]; columns: string[]; messages: string[] } | undefined >(); + const { euiTheme } = useCurrentEuiTheme(); + // formats the array of field names for EuiComboBox const fieldOptions = useMemo( () => @@ -172,7 +181,77 @@ export const ScatterplotMatrix: FC = ({ setDynamicSize(!dynamicSize); }; - const { euiTheme } = useCurrentEuiTheme(); + const getCustomVisualizationLink = useCallback(() => { + const { columns } = splom!; + const outlierScoreField = + resultsField !== undefined ? `${resultsField}.${OUTLIER_SCORE_FIELD}` : undefined; + const vegaSpec = getScatterplotMatrixVegaLiteSpec( + true, + [], + [], + columns, + euiTheme, + resultsField, + color, + legendType, + dynamicSize + ); + + vegaSpec.$schema = 'https://vega.github.io/schema/vega-lite/v5.json'; + vegaSpec.title = `Scatterplot matrix for ${index}`; + + const fieldsToFetch = [ + ...columns, + // Add outlier_score field in fetch if it's available so custom visualization can use it + ...(outlierScoreField ? [outlierScoreField] : []), + // Add field to color code by in fetch so custom visualization can use it - usually for classfication jobs + ...(color ? [color] : []), + ]; + + vegaSpec.data = { + url: { + '%context%': true, + ...(indexPattern?.timeFieldName + ? { ['%timefield%']: `${indexPattern?.timeFieldName}` } + : {}), + index, + body: { + fields: fieldsToFetch, + size: fetchSize, + _source: false, + }, + }, + format: { property: 'hits.hits' }, + }; + + const globalState = encodeURIComponent( + rison.encode({ + filters: data.query.filterManager.getFilters(), + refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), + time: data.query.timefilter.timefilter.getTime(), + }) + ); + + const appState = encodeURIComponent( + rison.encode({ + filters: [], + linked: false, + query, + uiState: {}, + vis: { + aggs: [], + params: { + spec: JSON.stringify(vegaSpec, null, 2), + }, + }, + }) + ); + + const basePath = `/create?type=vega&_g=${globalState}&_a=${appState}`; + + return { path: basePath }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [splom]); useEffect(() => { if (fields.length === 0) { @@ -316,6 +395,7 @@ export const ScatterplotMatrix: FC = ({ const { items, backgroundItems, columns } = splom; return getScatterplotMatrixVegaLiteSpec( + false, items, backgroundItems, columns, @@ -442,6 +522,29 @@ export const ScatterplotMatrix: FC = ({ )} + {splom ? ( + + { + const customVisLink = getCustomVisualizationLink(); + await application.navigateToApp('visualize#', { + path: customVisLink.path, + openInNewTab: false, + }); + }} + data-test-subj="mlSplomoExploreInCustomVisualizationLink" + > + + + + ) : null} {splom.messages.length > 0 && ( diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts index 0fbe08dd24af7d..dfa2389c6e0a52 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -26,7 +26,7 @@ import { describe('getColorSpec()', () => { it('should return only user selection conditions and the default color for non-outlier specs', () => { - const colorSpec = getColorSpec(euiThemeLight); + const colorSpec = getColorSpec(false, euiThemeLight); expect(colorSpec).toEqual({ condition: [{ selection: USER_SELECTION }, { selection: SINGLE_POINT_CLICK }], @@ -35,7 +35,7 @@ describe('getColorSpec()', () => { }); it('should return user selection condition and conditional spec for outliers', () => { - const colorSpec = getColorSpec(euiThemeLight, 'outlier_score'); + const colorSpec = getColorSpec(false, euiThemeLight, 'outlier_score'); expect(colorSpec).toEqual({ condition: { @@ -53,7 +53,13 @@ describe('getColorSpec()', () => { it('should return user selection condition and a field based spec for non-outlier specs with legendType supplied', () => { const colorName = 'the-color-field'; - const colorSpec = getColorSpec(euiThemeLight, undefined, colorName, LEGEND_TYPES.NOMINAL); + const colorSpec = getColorSpec( + false, + euiThemeLight, + undefined, + colorName, + LEGEND_TYPES.NOMINAL + ); expect(colorSpec).toEqual({ condition: { @@ -70,10 +76,18 @@ describe('getColorSpec()', () => { }); describe('getScatterplotMatrixVegaLiteSpec()', () => { + const forCustomLink = false; + it('should return the default spec for non-outliers without a legend', () => { const data = [{ x: 1, y: 1 }]; - const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(data, [], ['x', 'y'], euiThemeLight); + const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec( + forCustomLink, + data, + [], + ['x', 'y'], + euiThemeLight + ); const specForegroundLayer = vegaLiteSpec.spec.layer[0]; // A valid Vega Lite spec shouldn't throw an error when compiled. @@ -103,6 +117,7 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => { const data = [{ x: 1, y: 1 }]; const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec( + forCustomLink, data, [], ['x', 'y'], @@ -151,6 +166,7 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => { const data = [{ x: 1, y: 1 }]; const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec( + forCustomLink, data, [], ['x', 'y'], @@ -196,6 +212,7 @@ describe('getScatterplotMatrixVegaLiteSpec()', () => { const data = [{ ['x.a']: 1, ['y[a]']: 1 }]; const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec( + forCustomLink, data, [], ['x.a', 'y[a]'], diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index de29332f57ef68..31ec6403b8fd01 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -29,8 +29,10 @@ export const COLOR_SELECTION = euiPaletteColorBlind()[2]; export const COLOR_RANGE_OUTLIER = [euiPaletteColorBlind()[1], euiPaletteColorBlind()[2]]; export const COLOR_RANGE_NOMINAL = euiPaletteColorBlind({ rotations: 2 }); export const COLOR_RANGE_QUANTITATIVE = euiPalettePositive(5); +const CUSTOM_VIS_FIELDS_PATH = 'fields'; export const getColorSpec = ( + forCustomVisLink: boolean, euiTheme: typeof euiThemeLight, escapedOutlierScoreField?: string, color?: string, @@ -58,7 +60,10 @@ export const getColorSpec = ( return { condition: { selection: USER_SELECTION, - field: getEscapedVegaFieldName(color ?? '00FF00'), + field: `${forCustomVisLink ? `${CUSTOM_VIS_FIELDS_PATH}.` : ''}${getEscapedVegaFieldName( + color ?? '00FF00' + // When creating the custom link - this field is returned in an array so we need to access it + )}${forCustomVisLink ? '[0]' : ''}`, type: legendType, scale: { range: @@ -76,6 +81,7 @@ export const getColorSpec = ( }; const getVegaSpecLayer = ( + forCustomVisLink: boolean, isBackground: boolean, values: VegaValue[], colorSpec: any, @@ -117,7 +123,8 @@ const getVegaSpecLayer = ( }; return { - data: { values: [...values] }, + // Don't need to add static data for custom vis links + ...(forCustomVisLink ? {} : { data: { values: [...values] } }), mark: { ...(outliers && dynamicSize ? { @@ -133,7 +140,9 @@ const getVegaSpecLayer = ( ? { transform: [ { - calculate: `datum['${escapedOutlierScoreField}'] >= mlOutlierScoreThreshold.cutoff`, + calculate: `datum${ + forCustomVisLink ? `.${CUSTOM_VIS_FIELDS_PATH}` : '' + }['${escapedOutlierScoreField}'] >= mlOutlierScoreThreshold.cutoff`, as: 'is_outlier', }, ], @@ -154,7 +163,9 @@ const getVegaSpecLayer = ( opacity: { condition: { value: 1, - test: `(datum['${escapedOutlierScoreField}'] >= mlOutlierScoreThreshold.cutoff)`, + test: `(datum${ + forCustomVisLink ? `.${CUSTOM_VIS_FIELDS_PATH}` : '' + }['${escapedOutlierScoreField}'] >= mlOutlierScoreThreshold.cutoff)`, }, value: 0.5, }, @@ -162,19 +173,27 @@ const getVegaSpecLayer = ( : {}), ...(outliers ? { - order: { field: escapedOutlierScoreField }, + order: { + field: `${ + forCustomVisLink ? `${CUSTOM_VIS_FIELDS_PATH}.` : '' + }${escapedOutlierScoreField}`, + }, size: { ...(!dynamicSize ? { condition: { value: 40, - test: `(datum['${escapedOutlierScoreField}'] >= mlOutlierScoreThreshold.cutoff)`, + test: `(datum${ + forCustomVisLink ? `.${CUSTOM_VIS_FIELDS_PATH}` : '' + }['${escapedOutlierScoreField}'] >= mlOutlierScoreThreshold.cutoff)`, }, value: 8, } : { type: LEGEND_TYPES.QUANTITATIVE, - field: escapedOutlierScoreField, + field: `${ + forCustomVisLink ? `${CUSTOM_VIS_FIELDS_PATH}.` : '' + }${escapedOutlierScoreField}`, scale: { type: 'linear', range: [8, 200], @@ -197,7 +216,14 @@ const getVegaSpecLayer = ( tooltip: [ ...(color !== undefined ? // @ts-ignore - [{ type: colorSpec.condition.type, field: getEscapedVegaFieldName(color) }] + [ + { + type: colorSpec.condition.type, + field: `${ + forCustomVisLink ? `${CUSTOM_VIS_FIELDS_PATH}.` : '' + }${getEscapedVegaFieldName(color)}`, + }, + ] : []), ...vegaColumns.map((d) => ({ type: LEGEND_TYPES.QUANTITATIVE, @@ -207,7 +233,9 @@ const getVegaSpecLayer = ( ? [ { type: LEGEND_TYPES.QUANTITATIVE, - field: escapedOutlierScoreField, + field: `${ + forCustomVisLink ? `${CUSTOM_VIS_FIELDS_PATH}.` : '' + }${escapedOutlierScoreField}`, format: '.3f', }, ] @@ -215,21 +243,22 @@ const getVegaSpecLayer = ( ], }, ...(isBackground ? {} : selection), - width: SCATTERPLOT_SIZE, - height: SCATTERPLOT_SIZE, + ...(forCustomVisLink ? {} : { width: SCATTERPLOT_SIZE }), + ...(forCustomVisLink ? {} : { height: SCATTERPLOT_SIZE }), }; }; // Escapes the characters .[] in field names with double backslashes // since VEGA treats dots/brackets in field names as nested values. // See https://vega.github.io/vega-lite/docs/field.html for details. -function getEscapedVegaFieldName(fieldName: string) { - return fieldName.replace(/([\.|\[|\]])/g, '\\$1'); +function getEscapedVegaFieldName(fieldName: string, prependString: string = '') { + return `${prependString}${fieldName.replace(/([\.|\[|\]])/g, '\\$1')}`; } type VegaValue = Record; export const getScatterplotMatrixVegaLiteSpec = ( + forCustomVisLink: boolean, values: VegaValue[], backgroundValues: VegaValue[], columns: string[], @@ -240,12 +269,15 @@ export const getScatterplotMatrixVegaLiteSpec = ( dynamicSize?: boolean ): TopLevelSpec => { const vegaValues = values; - const vegaColumns = columns.map(getEscapedVegaFieldName); + const vegaColumns = columns.map((column) => + getEscapedVegaFieldName(column, forCustomVisLink ? 'fields.' : '') + ); const outliers = resultsField !== undefined; const escapedOutlierScoreField = `${resultsField}\\.${OUTLIER_SCORE_FIELD}`; const colorSpec = getColorSpec( + forCustomVisLink, euiTheme, resultsField && escapedOutlierScoreField, color, @@ -282,6 +314,7 @@ export const getScatterplotMatrixVegaLiteSpec = ( spec: { layer: [ getVegaSpecLayer( + forCustomVisLink, false, vegaValues, colorSpec, @@ -298,6 +331,7 @@ export const getScatterplotMatrixVegaLiteSpec = ( if (backgroundValues.length) { schema.spec.layer.unshift( getVegaSpecLayer( + forCustomVisLink, true, backgroundValues, colorSpec, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index c10c3e67be4430..6cb860557b7197 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -225,6 +225,7 @@ export const ExplorationPageWrapper: FC = ({ = React.memo(({ jobId }) = index={jobConfig?.dest.index} resultsField={jobConfig?.dest.results_field} searchQuery={searchQuery} + query={query} /> )} {showLegacyFeatureInfluenceFormatCallout && ( From 502fb009cfbcb5aa2718b4daade2c8a5dbfc5002 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 31 Jan 2023 12:01:00 -0800 Subject: [PATCH 12/59] [DOCS] Lint case API specifications (#149641) --- docs/api-generated/README.md | 2 +- .../cases/case-apis-passthru.asciidoc | 27 +++++++++---------- .../plugins/cases/docs/openapi/bundled.json | 9 ++++--- .../plugins/cases/docs/openapi/bundled.yaml | 9 ++++--- .../openapi/components/headers/kbn_xsrf.yaml | 1 + ...api@cases@{caseid}@user_actions@_find.yaml | 8 +++--- 6 files changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/api-generated/README.md b/docs/api-generated/README.md index 97a94e512e5c6e..97fd32119b8bc8 100644 --- a/docs/api-generated/README.md +++ b/docs/api-generated/README.md @@ -25,7 +25,7 @@ or a similar tool that can generate HTML output from OAS. . Rename the output files. For example: ``` - mv $GIT_HOME/kibana/docs/api-generated/rules/index.html $GIT_HOME/kibana/docs/api-generated/rules/rule-apis-passthru.asciidoc + mv $GIT_HOME/kibana/docs/api-generated/rules/index.html $GIT_HOME/kibana/docs/api-generated/rules/rule-apis-passthru.asciidoc mv $GIT_HOME/kibana/docs/api-generated/cases/index.html $GIT_HOME/kibana/docs/api-generated/cases/case-apis-passthru.asciidoc mv $GIT_HOME/kibana/docs/api-generated/connectors/index.html $GIT_HOME/kibana/docs/api-generated/connectors/connector-apis-passthru.asciidoc mv $GIT_HOME/kibana/docs/api-generated/machine-learning/index.html $GIT_HOME/kibana/docs/api-generated/machine-learning/ml-apis-passthru.asciidoc diff --git a/docs/api-generated/cases/case-apis-passthru.asciidoc b/docs/api-generated/cases/case-apis-passthru.asciidoc index 5801fe57ec0750..d6d5b0d589ac19 100644 --- a/docs/api-generated/cases/case-apis-passthru.asciidoc +++ b/docs/api-generated/cases/case-apis-passthru.asciidoc @@ -78,7 +78,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -198,7 +198,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -306,7 +306,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -362,7 +362,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -410,7 +410,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -482,7 +482,6 @@ Any modifications made to this file will be overwritten.
{
   "userActions" : [ {
     "owner" : "cases",
-    "case_id" : "22df07d0-03b1-11ed-920c-974bfa104448",
     "action" : "create",
     "created_at" : "2022-05-13T09:16:17.416Z",
     "id" : "22fd3e30-03b1-11ed-920c-974bfa104448",
@@ -497,7 +496,6 @@ Any modifications made to this file will be overwritten.
     "version" : "WzM1ODg4LDFd"
   }, {
     "owner" : "cases",
-    "case_id" : "22df07d0-03b1-11ed-920c-974bfa104448",
     "action" : "create",
     "created_at" : "2022-05-13T09:16:17.416Z",
     "id" : "22fd3e30-03b1-11ed-920c-974bfa104448",
@@ -1519,7 +1517,7 @@ Any modifications made to this file will be overwritten.
     
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -1639,7 +1637,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -1740,7 +1738,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -1862,7 +1860,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -1984,7 +1982,7 @@ Any modifications made to this file will be overwritten.
kbn-xsrf (required)
-
Header Parameter — default: null
+
Header Parameter — Cross-site request forgery protection default: null
@@ -3001,7 +2999,6 @@ Any modifications made to this file will be overwritten.
action
-
case_id
comment_id
created_at
Date format: date-time
created_by
@@ -3009,7 +3006,9 @@ Any modifications made to this file will be overwritten.
owner
payload
version
-
type
+
type
String The type of action.
+
Enum:
+
assignees
create_case
comment
connector
description
pushed
tags
title
status
settings
severity
diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index 7260061d76b0b6..37647324ccf7a1 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -2111,9 +2111,9 @@ "description": "The page number to return.", "schema": { "type": "string", - "default": 1 + "default": "1" }, - "example": 1 + "example": "1" }, { "name": "perPage", @@ -2121,9 +2121,9 @@ "description": "The number of user actions to return per page.", "schema": { "type": "string", - "default": 20 + "default": "20" }, - "example": 20 + "example": "20" }, { "name": "sortOrder", @@ -2245,6 +2245,7 @@ }, "in": "header", "name": "kbn-xsrf", + "description": "Cross-site request forgery protection", "required": true }, "space_id": { diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index aa55e180191cb2..8098a2d8787ff7 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -1309,15 +1309,15 @@ paths: description: The page number to return. schema: type: string - default: 1 - example: 1 + default: '1' + example: '1' - name: perPage in: query description: The number of user actions to return per page. schema: type: string - default: 20 - example: 20 + default: '20' + example: '20' - name: sortOrder in: query description: Determines the sort order. @@ -1398,6 +1398,7 @@ components: type: string in: header name: kbn-xsrf + description: Cross-site request forgery protection required: true space_id: in: path diff --git a/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml index 3d8dfae634e68d..fe0402a43aa03c 100644 --- a/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml @@ -2,4 +2,5 @@ schema: type: string in: header name: kbn-xsrf +description: Cross-site request forgery protection required: true diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml index 8ec83b50416efa..8cdeca5b9a7a91 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml @@ -15,15 +15,15 @@ get: description: The page number to return. schema: type: string - default: 1 - example: 1 + default: "1" + example: "1" - name: perPage in: query description: The number of user actions to return per page. schema: type: string - default: 20 - example: 20 + default: "20" + example: "20" - name: sortOrder in: query description: Determines the sort order. From 359be884d2e41bb38810996f63ba2760ccf23c95 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 31 Jan 2023 15:08:59 -0500 Subject: [PATCH 13/59] feat(slo): negative error budget remaining (#149888) --- .../public/pages/slos/components/slo_summary.tsx | 4 ++-- .../server/domain/services/compute_error_budget.test.ts | 8 ++++---- .../server/domain/services/compute_error_budget.ts | 2 +- .../slo/__snapshots__/summary_client.test.ts.snap | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_summary.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_summary.tsx index e9d90c76ae6a8a..029e18e4f38db6 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_summary.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_summary.tsx @@ -64,9 +64,9 @@ export function SloSummary({ slo, historicalSummary = [], historicalSummaryLoadi - + - + { it('computes the error budget with rounded values', () => { const slo = createSLO(); const dateRange = toDateRange(slo.timeWindow); - const errorBudget = computeErrorBudget(slo, { good: 333, total: 777, dateRange }); + const errorBudget = computeErrorBudget(slo, { good: 770, total: 777, dateRange }); expect(errorBudget).toEqual({ initial: 0.001, - consumed: 571.428571, // i.e. 57,142% consumed - remaining: 0, + consumed: 9.009009, // i.e. 900.90% consumed + remaining: -8.009009, // i.e. -800.90% remaining isEstimated: false, }); }); @@ -187,7 +187,7 @@ describe('computeErrorBudget', () => { expect(errorBudget).toEqual({ initial: 0.001, consumed: 1000, // i.e. 100,000% consumed - remaining: 0, + remaining: -999, isEstimated: false, }); }); diff --git a/x-pack/plugins/observability/server/domain/services/compute_error_budget.ts b/x-pack/plugins/observability/server/domain/services/compute_error_budget.ts index 61da9597c2db97..3302ac35678ee1 100644 --- a/x-pack/plugins/observability/server/domain/services/compute_error_budget.ts +++ b/x-pack/plugins/observability/server/domain/services/compute_error_budget.ts @@ -95,7 +95,7 @@ export function toErrorBudget( return { initial: toHighPrecision(initial), consumed: toHighPrecision(consumed), - remaining: Math.max(toHighPrecision(1 - consumed), 0), + remaining: toHighPrecision(1 - consumed), isEstimated, }; } diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_client.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_client.test.ts.snap index e7b4a068d7ae04..56bb33309ea7d1 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_client.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/summary_client.test.ts.snap @@ -19,7 +19,7 @@ Object { "consumed": 100, "initial": 0.001, "isEstimated": false, - "remaining": 0, + "remaining": -99, }, "sliValue": 0.9, "status": "VIOLATED", @@ -32,7 +32,7 @@ Object { "consumed": 2, "initial": 0.05, "isEstimated": false, - "remaining": 0, + "remaining": -1, }, "sliValue": 0.9, "status": "VIOLATED", From 7133aac06e29214a2985f8a28927e340f6faa35d Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Tue, 31 Jan 2023 20:37:52 +0000 Subject: [PATCH 14/59] [Fleet] remove `totalInactive` from get agents by kuery API (#149927) --- .../fleet/common/types/rest_spec/agent.ts | 1 - .../use_last_seen_inactive_agents_count.ts | 1 - .../fleet/server/routes/agent/handlers.ts | 4 +--- .../fleet/server/services/agents/crud.ts | 17 +---------------- .../store/mock_endpoint_result_list.ts | 1 - 5 files changed, 2 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 68b251f21e2944..77bea13dda8393 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -27,7 +27,6 @@ export interface GetAgentsRequest { } export interface GetAgentsResponse extends ListResult { - totalInactive: number; // deprecated in 8.x list?: Agent[]; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_last_seen_inactive_agents_count.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_last_seen_inactive_agents_count.ts index ce767c82eec374..e1baf8f277ff10 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_last_seen_inactive_agents_count.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_last_seen_inactive_agents_count.ts @@ -11,7 +11,6 @@ const LOCAL_STORAGE_KEY = 'fleet.lastSeenInactiveAgentsCount'; export const useLastSeenInactiveAgentsCount = (): [number, (val: number) => void] => { const [lastSeenInactiveAgentsCount, setLastSeenInactiveAgentsCount] = useState(0); - useEffect(() => { const storageValue = localStorage.getItem(LOCAL_STORAGE_KEY); if (storageValue) { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index bb43d32b6cfaf4..a44f17547d60b3 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -186,10 +186,9 @@ export const getAgentsHandler: RequestHandler< kuery: request.query.kuery, sortField: request.query.sortField, sortOrder: request.query.sortOrder, - getTotalInactive: request.query.showInactive, }); - const { total, page, perPage, totalInactive = 0 } = agentRes; + const { total, page, perPage } = agentRes; let { agents } = agentRes; // Assign metrics @@ -201,7 +200,6 @@ export const getAgentsHandler: RequestHandler< list: agents, // deprecated items: agents, total, - totalInactive, page, perPage, }; diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index cd86b5fdf68e78..3445aa36da42e8 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -194,7 +194,6 @@ export async function getAgentsByKuery( soClient: SavedObjectsClientContract, options: ListWithKuery & { showInactive: boolean; - getTotalInactive?: boolean; sortField?: string; sortOrder?: 'asc' | 'desc'; pitId?: string; @@ -205,7 +204,6 @@ export async function getAgentsByKuery( total: number; page: number; perPage: number; - totalInactive?: number; }> { const { page = 1, @@ -217,7 +215,6 @@ export async function getAgentsByKuery( showUpgradeable, searchAfter, pitId, - getTotalInactive = false, } = options; const filters = []; @@ -239,7 +236,7 @@ export async function getAgentsByKuery( ? [{ 'local_metadata.host.hostname.keyword': { order: 'asc' } }] : []; const queryAgents = async (from: number, size: number) => - esClient.search({ + esClient.search({ from, size, track_total_hits: true, @@ -260,13 +257,6 @@ export async function getAgentsByKuery( ignore_unavailable: true, }), ...(pitId && searchAfter ? { search_after: searchAfter, from: 0 } : {}), - ...(getTotalInactive && { - aggregations: { - totalInactive: { - filter: { bool: { must: { terms: { status: ['inactive', 'unenrolled'] } } } }, - }, - }, - }), }); let res; try { @@ -278,10 +268,6 @@ export async function getAgentsByKuery( let agents = res.hits.hits.map(searchHitToAgent); let total = res.hits.total as number; - let totalInactive = 0; - if (getTotalInactive && res.aggregations) { - totalInactive = res.aggregations?.totalInactive?.doc_count ?? 0; - } // filtering for a range on the version string will not work, // nor does filtering on a flattened field (local_metadata), so filter here if (showUpgradeable) { @@ -308,7 +294,6 @@ export async function getAgentsByKuery( total, page, perPage, - ...(getTotalInactive && { totalInactive }), }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 5a983574f7545c..671347dcd27b36 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -156,7 +156,6 @@ const endpointListApiPathHandlerMocks = ({ return { total: totalAgentsUsingEndpoint, items: [], - totalInactive: 0, page: 1, perPage: 10, }; From 3e5b1e4228769eeef916a13bf01346e39b11cc5a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 31 Jan 2023 22:05:40 +0100 Subject: [PATCH 15/59] [Synthetics] Added monitor not found page (#149924) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Fixes https://github.com/elastic/kibana/issues/144366 --- .../prompt/not_found/src/not_found_prompt.tsx | 8 +-- .../plugins/synthetics/common/constants/ui.ts | 2 + .../common/pages/not_found.test.tsx | 19 ------- .../components/common/pages/not_found.tsx | 52 ------------------ .../monitor_details/monitor_details_page.tsx | 14 ++++- .../monitor_not_found_page.tsx | 53 +++++++++++++++++++ .../monitor_details/route_config.tsx | 48 +++++++++++------ .../public/apps/synthetics/routes.tsx | 30 +++++++++-- x-pack/plugins/synthetics/tsconfig.json | 1 + 9 files changed, 129 insertions(+), 98 deletions(-) delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_not_found_page.tsx diff --git a/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx b/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx index bd607e1beae07f..116deb7098c39f 100644 --- a/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx +++ b/packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx @@ -32,12 +32,14 @@ const NOT_FOUND_GO_BACK = i18n.translate('sharedUXPackages.prompt.errors.notFoun interface NotFoundProps { /** Array of buttons, links and other actions to show at the bottom of the `EuiEmptyPrompt`. Defaults to a "Back" button. */ actions?: EuiEmptyPromptProps['actions']; + title?: EuiEmptyPromptProps['title'] | string; + body?: EuiEmptyPromptProps['body']; } /** * Predefined `EuiEmptyPrompt` for 404 pages. */ -export const NotFoundPrompt = ({ actions }: NotFoundProps) => { +export const NotFoundPrompt = ({ actions, title, body }: NotFoundProps) => { const { colorMode } = useEuiTheme(); const [imageSrc, setImageSrc] = useState(); const goBack = useCallback(() => history.back(), []); @@ -71,8 +73,8 @@ export const NotFoundPrompt = ({ actions }: NotFoundProps) => { color="subdued" titleSize="m" icon={icon} - title={

{NOT_FOUND_TITLE}

} - body={NOT_FOUND_BODY} + title={typeof title === 'string' || !title ?

{title ?? NOT_FOUND_TITLE}

: title} + body={body ?? NOT_FOUND_BODY} actions={actions ?? DEFAULT_ACTIONS} /> ); diff --git a/x-pack/plugins/synthetics/common/constants/ui.ts b/x-pack/plugins/synthetics/common/constants/ui.ts index bc6c70f60a7fe2..57737c2cef1920 100644 --- a/x-pack/plugins/synthetics/common/constants/ui.ts +++ b/x-pack/plugins/synthetics/common/constants/ui.ts @@ -7,6 +7,8 @@ export const MONITOR_ROUTE = '/monitor/:monitorId?'; +export const MONITOR_NOT_FOUND_ROUTE = '/monitor-not-found/:monitorId'; + export const MONITOR_HISTORY_ROUTE = '/monitor/:monitorId/history'; export const MONITOR_ERRORS_ROUTE = '/monitor/:monitorId/errors'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx deleted file mode 100644 index 0bade3c639a5ea..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx +++ /dev/null @@ -1,19 +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 { NotFoundPage } from './not_found'; -import { render } from '../../../utils/testing'; - -describe('NotFoundPage', () => { - it('render component', async () => { - const { findByText } = render(); - - expect(await findByText('Page not found')).toBeInTheDocument(); - expect(await findByText('Back to home')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx deleted file mode 100644 index c94a7a7a06b6a8..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx +++ /dev/null @@ -1,52 +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 { - EuiEmptyPrompt, - EuiPanel, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; -import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export const NotFoundPage = () => { - const history = useHistory(); - return ( - - - - -

- -

- - } - body={ - - - - } - /> -
-
-
- ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page.tsx index c493f863d5bc59..fa279a4d64a4c3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_page.tsx @@ -6,11 +6,21 @@ */ import React from 'react'; -import { useSelectedMonitor } from './hooks/use_selected_monitor'; +import { Redirect, useParams } from 'react-router-dom'; + +import { MONITOR_NOT_FOUND_ROUTE } from '../../../../../common/constants'; import { useMonitorListBreadcrumbs } from '../monitors_page/hooks/use_breadcrumbs'; +import { useSelectedMonitor } from './hooks/use_selected_monitor'; export const MonitorDetailsPage: React.FC<{ children: React.ReactElement }> = ({ children }) => { - const { monitor } = useSelectedMonitor(); + const { monitor, error } = useSelectedMonitor(); + + const { monitorId } = useParams<{ monitorId: string }>(); + useMonitorListBreadcrumbs(monitor ? [{ text: monitor?.name ?? '' }] : []); + + if (error?.body.statusCode === 404) { + return ; + } return children; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_not_found_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_not_found_page.tsx new file mode 100644 index 00000000000000..2d05f24d9d51d1 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_not_found_page.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useParams } from 'react-router-dom'; +import { CreateMonitorButton } from '../monitors_page/create_monitor_button'; +import { PLUGIN } from '../../../../../common/constants/plugin'; +import { ClientPluginsStart } from '../../../../plugin'; + +export const MonitorNotFoundPage: React.FC = () => { + const { application } = useKibana().services; + const { monitorId } = useParams<{ monitorId: string }>(); + + return ( + {monitorId} }} + /> + } + actions={[ + , + { + application.navigateToApp(PLUGIN.SYNTHETICS_PLUGIN_ID); + }} + > + {i18n.translate('xpack.synthetics.routes.createNewMonitor', { + defaultMessage: 'Go to Home', + })} + , + ]} + /> + ); +}; + +const NOT_FOUND_TITLE = i18n.translate('xpack.synthetics.prompt.errors.notFound.title', { + defaultMessage: 'Monitor not found', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx index 8f2c7f36dfcb44..8acf81c10e7789 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/route_config.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { useHistory, useRouteMatch } from 'react-router-dom'; import { EuiIcon, EuiPageHeaderProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { MonitorNotFoundPage } from './monitor_not_found_page'; import { MonitorDetailsPageTitle } from './monitor_details_page_title'; import { EditMonitorLink } from '../common/links/edit_monitor'; import { RunTestManually } from './run_test_manually'; @@ -23,8 +24,9 @@ import { MonitorDetailsPage } from './monitor_details_page'; import { MONITOR_ERRORS_ROUTE, MONITOR_HISTORY_ROUTE, + MONITOR_NOT_FOUND_ROUTE, MONITOR_ROUTE, - OVERVIEW_ROUTE, + MONITORS_ROUTE, } from '../../../../../common/constants'; import { RouteProps } from '../../routes'; @@ -76,9 +78,36 @@ export const getMonitorDetailsRoute = ( dataTestSubj: 'syntheticsMonitorHistoryPage', pageHeader: getMonitorSummaryHeader(history, syntheticsPath, 'errors'), }, + { + title: i18n.translate('xpack.synthetics.monitorNotFound.title', { + defaultMessage: 'Synthetics Monitor Not Found | {baseTitle}', + values: { baseTitle }, + }), + path: MONITOR_NOT_FOUND_ROUTE, + component: () => , + dataTestSubj: 'syntheticsMonitorNotFoundPage', + pageHeader: { + breadcrumbs: [getMonitorsBreadcrumb(syntheticsPath)], + }, + }, ]; }; +const getMonitorsBreadcrumb = (syntheticsPath: string) => ({ + text: ( + <> + {' '} + + + ), + color: 'primary' as const, + 'aria-current': false, + href: `${syntheticsPath}${MONITORS_ROUTE}`, +}); + const getMonitorSummaryHeader = ( history: ReturnType, syntheticsPath: string, @@ -96,22 +125,7 @@ const getMonitorSummaryHeader = ( return { pageTitle: , - breadcrumbs: [ - { - text: ( - <> - {' '} - - - ), - color: 'primary', - 'aria-current': false, - href: `${syntheticsPath}${OVERVIEW_ROUTE}`, - }, - ], + breadcrumbs: [getMonitorsBreadcrumb(syntheticsPath)], rightSideItems: [ , , diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index d2452a40f2426d..d530b557fa3566 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -7,15 +7,16 @@ import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types'; import React, { FC, useEffect } from 'react'; -import { EuiLink, useEuiTheme } from '@elastic/eui'; +import { EuiButtonEmpty, EuiLink, useEuiTheme } from '@elastic/eui'; import { Route, Switch, useHistory } from 'react-router-dom'; import { OutPortal } from 'react-reverse-portal'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found'; 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 { useInspectorContext } from '@kbn/observability-plugin/public'; import { ClientPluginsStart } from '../../plugin'; import { getMonitorsRoute } from './components/monitors_page/route_config'; import { getMonitorDetailsRoute } from './components/monitor_details/route_config'; @@ -26,10 +27,9 @@ import { TestRunDetails } from './components/test_run_details/test_run_details'; import { MonitorAddPageWithServiceAllowed } from './components/monitor_add_edit/monitor_add_page'; import { MonitorEditPageWithServiceAllowed } from './components/monitor_add_edit/monitor_edit_page'; import { GettingStartedPage } from './components/getting_started/getting_started_page'; -import { NotFoundPage } from './components/common/pages/not_found'; import { - MonitorTypePortalNode, MonitorDetailsLinkPortalNode, + MonitorTypePortalNode, } from './components/monitor_add_edit/portals'; import { GETTING_STARTED_ROUTE, @@ -210,7 +210,27 @@ export const PageRouter: FC = () => { ) )} - + ( + + { + application.navigateToApp(PLUGIN.SYNTHETICS_PLUGIN_ID); + }} + > + {i18n.translate('xpack.synthetics.routes.goToSynthetics', { + defaultMessage: 'Go to Synthetics Home Page', + })} + , + ]} + /> + + )} + /> ); }; diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index e752376f4e9084..384edfb5f75224 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -71,6 +71,7 @@ "@kbn/core-elasticsearch-server", "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-saved-objects-server", + "@kbn/shared-ux-prompt-not-found", ], "exclude": [ "target/**/*", From 55702f446e4cc76f453b90477b59320c6b4da180 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 31 Jan 2023 15:14:29 -0700 Subject: [PATCH 16/59] [Maps] fix Kibana Maps UI upload geojson failure should be received as such (#149969) Fixes https://github.com/elastic/kibana/issues/138122 PR updates geo spatial file upload to show error callout if all features fail indexing. PR updates geo spatial file upload to show warning callout if some features fail indexing. Screen Shot 2023-01-31 at 11 34 49 AM Screen Shot 2023-01-31 at 11 34 32 AM To test save the following geojson samples as a file with the name `.geojson`. Then upload the files in kibana. Verify errors are displayed as expected all invalid features ``` { "type" : "FeatureCollection", "features" : [{ "type" : "Feature", "geometry" : { "type" : "Point", "coordinates" : [ 100000, 42.417500 ] } }] } ``` some invalid features ``` { "type" : "FeatureCollection", "features" : [{ "type" : "Feature", "geometry" : { "type" : "Point", "coordinates" : [ 0, 0 ] } }, { "type" : "Feature", "geometry" : { "type" : "Point", "coordinates" : [ 100000, 0 ] } }] } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../import_complete_view.test.tsx.snap | 519 ++++++++++++++++++ .../public/components/geo_upload_wizard.tsx | 20 + .../components/import_complete_view.test.tsx | 115 ++++ .../components/import_complete_view.tsx | 46 +- .../file_upload/public/components/utils.ts | 27 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 710 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/file_upload/public/components/__snapshots__/import_complete_view.test.tsx.snap create mode 100644 x-pack/plugins/file_upload/public/components/import_complete_view.test.tsx create mode 100644 x-pack/plugins/file_upload/public/components/utils.ts diff --git a/x-pack/plugins/file_upload/public/components/__snapshots__/import_complete_view.test.tsx.snap b/x-pack/plugins/file_upload/public/components/__snapshots__/import_complete_view.test.tsx.snap new file mode 100644 index 00000000000000..d24f877c21cb63 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/__snapshots__/import_complete_view.test.tsx.snap @@ -0,0 +1,519 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should render error when upload fails from elasticsearch request failure 1`] = ` + + +

+ Error: simulated elasticsearch request failure +

+
+ + + +

+ Import response +

+
+
+ + + + + +
+
+ +
+ +
+`; + +exports[`Should render error when upload fails from http request timeout 1`] = ` + + +

+ Error: simulated http request timeout +

+
+ + + +

+ Import response +

+
+
+ + + + + +
+
+ +
+ +
+`; + +exports[`Should render success 1`] = ` + + +

+ Indexed 10 features. +

+
+ + + +

+ Import response +

+
+
+ + + + + +
+
+ +
+ + + + +

+ Data view response +

+
+
+ + + + + +
+
+ +
+ + +

+ + + + +

+
+
+`; + +exports[`Should render warning when some features failed import 1`] = ` + + +

+ Unable to index 1 of 10 features. +

+
+ + + +

+ Import response +

+
+
+ + + + + +
+
+ +
+ + + + +

+ Data view response +

+
+
+ + + + + +
+
+ +
+ + +

+ + + + +

+
+
+`; diff --git a/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx b/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx index 0c7f09c56f36f4..b3d1711b1d2a87 100644 --- a/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx +++ b/x-pack/plugins/file_upload/public/components/geo_upload_wizard.tsx @@ -17,6 +17,7 @@ import { ImportResults } from '../importer'; import { GeoFileImporter } from '../importer/geo'; import type { Settings } from '../../common/types'; import { hasImportPermission } from '../api'; +import { getPartialImportMessage } from './utils'; enum PHASE { CONFIGURE = 'CONFIGURE', @@ -175,6 +176,25 @@ export class GeoUploadWizard extends Component }); this.props.onUploadError(); return; + } else if (importResults.docCount === importResults.failures?.length) { + this.setState({ + // Force importResults into failure shape when no features are indexed + importResults: { + ...importResults, + success: false, + error: { + error: { + reason: getPartialImportMessage( + importResults.failures!.length, + importResults.docCount + ), + }, + }, + }, + phase: PHASE.COMPLETE, + }); + this.props.onUploadError(); + return; } // diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.test.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.test.tsx new file mode 100644 index 00000000000000..a31d8dcd04b841 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.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 { shallow } from 'enzyme'; + +import { ImportCompleteView } from './import_complete_view'; + +jest.mock('../kibana_services', () => ({ + get: jest.fn(), + getDocLinks: () => { + return { + links: { + maps: { + importGeospatialPrivileges: 'linkToPrvilegesDocs', + }, + }, + }; + }, + getHttp: () => { + return { + basePath: { + prepend: (path: string) => `abc${path}`, + }, + }; + }, + getUiSettings: () => { + return { + get: jest.fn(), + }; + }, +})); + +test('Should render success', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should render warning when some features failed import', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should render error when upload fails from http request timeout', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); +}); + +test('Should render error when upload fails from elasticsearch request failure', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index 46f566eb27e2e6..f95aee869f93dc 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -22,6 +22,7 @@ import { import { CodeEditor, KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { getDocLinks, getHttp, getUiSettings } from '../kibana_services'; import { ImportResults } from '../importer'; +import { getPartialImportMessage } from './utils'; const services = { uiSettings: getUiSettings(), @@ -133,7 +134,7 @@ export class ImportCompleteView extends Component { // Display http request error message reason = this.props.importResults.error.body.message; } else if (this.props.importResults?.error?.error?.reason) { - // Display elasticxsearch request error message + // Display elasticsearch request error message reason = this.props.importResults.error.error.reason; } const errorMsg = reason @@ -156,21 +157,25 @@ export class ImportCompleteView extends Component { ); } - const successMsg = i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', { - defaultMessage: 'Indexed {numFeatures} features.', - values: { - numFeatures: this.props.importResults.docCount, - }, - }); - - const failedFeaturesMsg = this.props.importResults.failures?.length - ? i18n.translate('xpack.fileUpload.importComplete.failedFeaturesMsg', { - defaultMessage: 'Unable to index {numFailures} features.', - values: { - numFailures: this.props.importResults.failures.length, - }, - }) - : ''; + if (this.props.importResults.failures?.length) { + return ( + +

+ {getPartialImportMessage( + this.props.importResults.failures!.length, + this.props.importResults.docCount + )} +

+
+ ); + } return ( { })} data-test-subj={STATUS_CALLOUT_DATA_TEST_SUBJ} > -

{`${successMsg} ${failedFeaturesMsg}`}

+

+ {i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', { + defaultMessage: 'Indexed {numFeatures} features.', + values: { + numFeatures: this.props.importResults.docCount, + }, + })} +

); } diff --git a/x-pack/plugins/file_upload/public/components/utils.ts b/x-pack/plugins/file_upload/public/components/utils.ts new file mode 100644 index 00000000000000..0b4df67f3a0ec4 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/utils.ts @@ -0,0 +1,27 @@ +/* + * 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 function getPartialImportMessage(failedFeaturesCount: number, totalFeaturesCount?: number) { + const outOfTotalMsg = + typeof totalFeaturesCount === 'number' + ? i18n.translate('xpack.fileUpload.geoUploadWizard.outOfTotalMsg', { + defaultMessage: 'of {totalFeaturesCount}', + values: { + totalFeaturesCount, + }, + }) + : ''; + return i18n.translate('xpack.fileUpload.geoUploadWizard.partialImportMsg', { + defaultMessage: 'Unable to index {failedFeaturesCount} {outOfTotalMsg} features.', + values: { + failedFeaturesCount, + outOfTotalMsg, + }, + }); +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 83edd358eaa8a6..b7950b56bcca50 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -13244,7 +13244,6 @@ "xpack.fileUpload.geoUploadWizard.creatingDataView": "Création de la vue de données : {indexName}", "xpack.fileUpload.geoUploadWizard.dataIndexingStarted": "Création de l'index : {indexName}", "xpack.fileUpload.geoUploadWizard.writingToIndex": "Écriture dans l'index : {progress} % terminé", - "xpack.fileUpload.importComplete.failedFeaturesMsg": "Impossible d'indexer {numFailures} fonctionnalités.", "xpack.fileUpload.importComplete.permissionFailureMsg": "Vous ne disposez pas d'autorisation pour créer ni importer des données dans l'index \"{indexName}\".", "xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock": "Erreur : {reason}", "xpack.fileUpload.importComplete.uploadSuccessMsg": "{numFeatures} fonctionnalités indexées.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 60f3f7c9ae88de..772e8ac9eaecf1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13231,7 +13231,6 @@ "xpack.fileUpload.geoUploadWizard.creatingDataView": "データビュー{indexName}を作成しています", "xpack.fileUpload.geoUploadWizard.dataIndexingStarted": "インデックスを作成中:{indexName}", "xpack.fileUpload.geoUploadWizard.writingToIndex": "インデックスに書き込み中:{progress}%完了", - "xpack.fileUpload.importComplete.failedFeaturesMsg": "{numFailures}個の特長量にインデックスを作成できませんでした。", "xpack.fileUpload.importComplete.permissionFailureMsg": "インデックス\"{indexName}\"にデータを作成またはインポートするアクセス権がありません。", "xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock": "エラー:{reason}", "xpack.fileUpload.importComplete.uploadSuccessMsg": "{numFeatures}個の特徴量にインデックスを作成しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b3f6dcf5ef9a4c..f705f8d765a44a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13248,7 +13248,6 @@ "xpack.fileUpload.geoUploadWizard.creatingDataView": "正在创建数据视图:{indexName}", "xpack.fileUpload.geoUploadWizard.dataIndexingStarted": "正在创建索引:{indexName}", "xpack.fileUpload.geoUploadWizard.writingToIndex": "正在写入索引:已完成 {progress}%", - "xpack.fileUpload.importComplete.failedFeaturesMsg": "无法索引 {numFailures} 个特征。", "xpack.fileUpload.importComplete.permissionFailureMsg": "您无权创建或将数据导入索引“{indexName}”。", "xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock": "错误:{reason}", "xpack.fileUpload.importComplete.uploadSuccessMsg": "已索引 {numFeatures} 个特征。", From 84b5ca492c8d0928f69fa894022aead1378e3737 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 31 Jan 2023 15:15:13 -0700 Subject: [PATCH 17/59] unskip Failing test: X-Pack Search Sessions Integration.x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search (#149977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/elastic/kibana/issues/103043 Flaky test runner https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1844 Failure in saved object client bulk delete. Resolved by https://github.com/elastic/kibana/pull/149582 ``` [00:00:11] │ debg Cleaning all saved objects { space: undefined } [00:00:11] │ info [o.e.c.m.MetadataMappingService] [ftr] [.kibana_8.7.0_001/0Buw8xPkRQi22GGDb-S18g] update_mapping [_doc] [00:00:11] │ info deleting batch of 104 objects [00:00:12] │ proc [kibana] {"log.level":"info","@timestamp":"2023-01-24T21:52:20.336Z","log":{"logger":"elastic-apm-node"},"ecs":{"version":"1.6.0"},"message":"Sending error to Elastic APM: {\"id\":\"93951b44b2dfb2b8890fed26a1ce2fdd\"}"} [00:00:13] │ERROR [DELETE http://elastic:changeme@localhost:5620/api/saved_objects/epm-packages-assets/f104eb71-d6fe-5506-a359-2770af4e2746?force=true] request failed (attempt=1/5): read ECONNRESET [00:00:13] │ info Taking screenshot "/var/lib/buildkite-agent/builds/kb-n2-4-spot-24a3e2a91102e68e/elastic/kibana-on-merge/kibana/x-pack/test/functional/screenshots/failure/Dashboard before all hook in Dashboard-33b135acce0aa45337f463568919cb7166a26f0546efcb90d3a789017d5338f2.png" [00:00:13] │ info Current URL is: data:, [00:00:13] │ info Saving page source to: /var/lib/buildkite-agent/builds/kb-n2-4-spot-24a3e2a91102e68e/elastic/kibana-on-merge/kibana/x-pack/test/functional/failure_debug/html/Dashboard before all hook in Dashboard-33b135acce0aa45337f463568919cb7166a26f0546efcb90d3a789017d5338f2.html [00:00:13] â””- ✖ fail: Dashboard "before all" hook in "Dashboard" [00:00:13] │ Error: 400 resp: '' [00:00:13] │ at createFailError (dev_cli_errors.ts:27:24) [00:00:13] │ at kbn_client_saved_objects.ts:284:32 [00:00:13] │ at processTicksAndRejections (node:internal/process/task_queues:95:5) [00:00:13] │ at kbn_client_saved_objects.ts:88:50 [00:00:13] │ [00:00:13] │ ``` --- .../tests/apps/dashboard/async_search/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts index 1c947c38524467..deb4a53190ff69 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts @@ -13,8 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - // FLAKY: https://github.com/elastic/kibana/issues/103043 - describe.skip('Dashboard', function () { + describe('Dashboard', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.savedObjects.cleanStandardList(); From 86ad13d70855fc62e55225919948ff8f6d1437bb Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 31 Jan 2023 15:15:37 -0700 Subject: [PATCH 18/59] =?UTF-8?q?unskip=20Failing=20test:=20Chrome=20X-Pac?= =?UTF-8?q?k=20UI=20Functional=20Tests.x-pack/test/functional/apps/dashboa?= =?UTF-8?q?rd/feature=5Fcontrols/dashboard=5Fsecurity=C2=B7ts=20(#149868)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/elastic/kibana/issues/116881 flaky test runner https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1818 --- .../dashboard/group1/feature_controls/dashboard_security.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts index 45d20b92c1e2b8..7cc75446036e95 100644 --- a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts @@ -454,8 +454,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/116881 - describe.skip('no dashboard privileges', () => { + describe('no dashboard privileges', () => { before(async () => { await security.role.create('no_dashboard_privileges_role', { elasticsearch: { From 2207b0cc01119c747c9696083bd36870bf14207c Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:36:19 -0500 Subject: [PATCH 19/59] [Security Solution][RBAC] Add security subfeature descriptions in rbac controls (#149446) ## Summary - [x] Adds descriptions for the 9 security sub features related to endpoint management and response actions # Screenshots image --- .../security_solution/server/features.ts | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index d4e541e3cb7b50..5a0b3b10112272 100644 --- a/x-pack/plugins/security_solution/server/features.ts +++ b/x-pack/plugins/security_solution/server/features.ts @@ -117,6 +117,12 @@ const responseActionSubFeatures: SubFeatureConfig[] = [ defaultMessage: 'Response Actions History', } ), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.responseActionsHistory.description', + { + defaultMessage: 'Access the history of response actions performed on endpoints.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -158,6 +164,10 @@ const responseActionSubFeatures: SubFeatureConfig[] = [ name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.hostIsolation', { defaultMessage: 'Host Isolation', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolation.description', + { defaultMessage: 'Perform the "isolate" and "release" response actions.' } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -188,6 +198,12 @@ const responseActionSubFeatures: SubFeatureConfig[] = [ name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.processOperations', { defaultMessage: 'Process Operations', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.processOperations.description', + { + defaultMessage: 'Perform process-related response actions in the response console.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -288,6 +304,13 @@ const subFeatures: SubFeatureConfig[] = [ name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.endpointList', { defaultMessage: 'Endpoint List', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.endpointList.description', + { + defaultMessage: + 'Displays all hosts running Elastic Defend and their relevant integration details.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -329,6 +352,13 @@ const subFeatures: SubFeatureConfig[] = [ name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.trustedApplications', { defaultMessage: 'Trusted Applications', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.trustedApplications.description', + { + defaultMessage: + 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -379,6 +409,13 @@ const subFeatures: SubFeatureConfig[] = [ defaultMessage: 'Host Isolation Exceptions', } ), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.hostIsolationExceptions.description', + { + defaultMessage: + 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -426,6 +463,13 @@ const subFeatures: SubFeatureConfig[] = [ name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.blockList', { defaultMessage: 'Blocklist', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.blockList.description', + { + defaultMessage: + 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -473,6 +517,13 @@ const subFeatures: SubFeatureConfig[] = [ name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.eventFilters', { defaultMessage: 'Event Filters', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.eventFilters.description', + { + defaultMessage: + 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', @@ -518,8 +569,15 @@ const subFeatures: SubFeatureConfig[] = [ } ), name: i18n.translate('xpack.securitySolution.featureRegistry.subFeatures.policyManagement', { - defaultMessage: 'Policy Management', + defaultMessage: 'Elastic Defend Policy Management', }), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.policyManagement.description', + { + defaultMessage: + 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', + } + ), privilegeGroups: [ { groupType: 'mutually_exclusive', From e2b40de9d69c4b4d36890c45eef666a892b9839e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 1 Feb 2023 00:21:34 +0100 Subject: [PATCH 20/59] Use `auto_expand_replicas` to dynamically adjust number of replicas for apm-source-map index (#149979) --- .../routes/source_maps/create_apm_source_map_index_template.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map_index_template.ts b/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map_index_template.ts index 25a7cf5ae58152..4fa24358e7b071 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map_index_template.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map_index_template.ts @@ -17,8 +17,9 @@ const indexTemplate: IndicesPutIndexTemplateRequest = { index_patterns: [APM_SOURCE_MAP_INDEX], template: { settings: { - number_of_shards: 1, index: { + number_of_shards: 1, + auto_expand_replicas: '0-2', hidden: true, }, }, From 411103aaae54d277f6166c3b0286ce68d9d02aed Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 31 Jan 2023 16:34:02 -0800 Subject: [PATCH 21/59] [DOCS] Improve server log connector, automate screenshots (#149905) --- .../action-types/server-log.asciidoc | 64 +++++++++++------- .../connectors/images/serverlog-connector.png | Bin 68666 -> 70926 bytes .../images/serverlog-params-test.png | Bin 120530 -> 110458 bytes docs/management/connectors/index.asciidoc | 3 +- .../stack_alerting/connector_types.ts | 36 ++++++++++ .../response_ops_docs/stack_alerting/index.ts | 20 +++++- .../stack_alerting/list_view.ts | 14 ---- 7 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/connector_types.ts diff --git a/docs/management/connectors/action-types/server-log.asciidoc b/docs/management/connectors/action-types/server-log.asciidoc index 7d9171ca99ed83..dca6eee379b52b 100644 --- a/docs/management/connectors/action-types/server-log.asciidoc +++ b/docs/management/connectors/action-types/server-log.asciidoc @@ -1,50 +1,64 @@ -[role="xpack"] [[server-log-action-type]] -=== Server log connector and action +== Server log connector and action ++++ Server log ++++ -This connector writes an entry to the {kib} server log. +A server log connector writes an entry to the {kib} server log. -[float] -[[server-log-connector-configuration]] -==== Connector configuration - -Server log connectors have the following configuration properties. - -Name:: The name of the connector. +You can create a server log connector in {kib} or by using the +<>. If you are running {kib} +on-prem, you can also create a preconfigured server log connector. [float] -[[Preconfigured-server-log-configuration]] -==== Preconfigured connector type +[[server-log-connector-configuration]] +=== Connector configuration -[source,text] --- - my-server-log: - name: preconfigured-server-log-connector-type - actionTypeId: .server-log --- +Server log connectors do not have any configuration properties other than a name. [float] [[define-serverlog-ui]] -==== Define connector in {stack-manage-app} +=== Create a connector in {kib} -Define Server log connector properties. +You can create a server log connector in *{stack-manage-app} > {connectors-ui}* +or as needed when you're creating a rule. For example: [role="screenshot"] image::management/connectors/images/serverlog-connector.png[Server log connector] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. -Test Server log action parameters. +[float] +[[preconfigured-server-log-configuration]] +=== Create a preconfigured connector -[role="screenshot"] -image::management/connectors/images/serverlog-params-test.png[Server log params test] +If you are running {kib} on-prem, you can define a server log connector by +adding `xpack.actions.preconfigured` settings to your `kibana.yml` file. +For example: + +[source,text] +-- +xpack.actions.preconfigured: + my-server-log: + name: preconfigured-server-log-connector-type + actionTypeId: .server-log +-- + +For more information, go to <>. [float] [[server-log-action-configuration]] -==== Action configuration +=== Test the connector -Server log actions have the following properties. +You can test your server log connector with the +<> or as you're creating or editing +the connector in {kib}. For example: + +[role="screenshot"] +image::management/connectors/images/serverlog-params-test.png[Server log connector test] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. + +Server log actions have the following properties: Message:: The message to log. Level:: The log level of the message: `trace`, `debug`, `info`, `warn`, `error` or `fatal`. Defaults to `info`. + diff --git a/docs/management/connectors/images/serverlog-connector.png b/docs/management/connectors/images/serverlog-connector.png index 983bb6afadd65b3da5f1137f586008de040eabf5..cc0b8745b2d6efc113b0dea7fcd7dbc7c5d910ec 100644 GIT binary patch literal 70926 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

Es!kB5#4ojLknB7r@QxHMP` zGh3$A^Q^7iBMrhSwVLKN98%!bWBnU{aIj6|jclJ{kq!1wl#Ct+JC{muJ?75VQpUyW z6sT?Jm$&H_R&w%LnoA^sK(i{X8=gK1cBja)w}aB+(;@Z}H;0;s-3H&>?0r_{XndWs zYs3gN66-rRSwSc(snX7bBO;=TMzr|}GeCkhVdFsW9Cvrp;Yd*-*w4y@6K3RW<6;iU z^Lib7X>5Rb;ZVTiYjrWBC@@d#h8`uRb}1X}W?}@IwlZ|B`8!9CGAYZH(pTl?32u3< zVO_GBEEBa@++wB+{iYd9?-ILcr)T%{qcXWVD$G>w3Z3NV%NgS*8}~1m``p+DZ1~fR zVv|t-+#>e+M%Ts-qhmYYu@qca+-jE#$l|)&s7mP;pxtIoQSuv)uB=i~pr@(yz#1u- z2w`zGOwZO~6u!x_C3Sl!&zFICS=RGY-Q_e$@GdlYtUPrL|{YM*avjj?BS-| z&u%s@xIiW1;Ic@w>HvZ(r*9IHs;F4s5}vgUkGU&NrK;-9Cg{5B#k0fx!~;|p^QRZ( zwK5q@I0F7@f81%t&iq_%@yD*w#P|a3uP3+Qtqt)6>#co){K=%nk$~;5QReIVSVNH( zL%sb<;F?$6>dLT%UNQKf2?^+Y8vgnb|0At@s2%+miL0f-KY;qz^B4Ld>yAa?(#77v zs6JJ)e=IKgvTX~bKQSj{d72};jf~%~smm%P&dCWp3iSdxBp~J%^lZ!DajZ2sQ-+$C zC2<0^e$*&@4t)`U2&!&e68Y`b$zu)tDqmeSv=Au0_A7m{0{q$Nflv<(M5nEt{ckw1 zT@(-l+EXtLIq#czvNhRuE`~r_p9(xw23mf+zTf({aZZ{96eeF=Z2Og0^s%`4u7(A1 zPFnaa0aZcaX%Jkjg|3PKxIh||nDEA6BSHMo=;d_WnUEWv>NW@IDOhO%&tXiVR~;-R zaaAw;7sPIOf(RnW#k6nUpiPVLjOG{c^U&6HO9obm=>Wj7BPerf#MI`j&gNPHoD)yO z_vra`IoXrZ`AViYj;8l-#$QO$8cgh3)f$mqaY5XIQtC!vo82qO%*}Fs3B^3Iy%rl;4p|REIWF43 zb??!gtI+%xxEe_AeOV4If;2S+ZJ*ve3|X`HTMawi9ojUVu`3$lhyABdL_qjoT=zg< t1nWNW@;=mLYYIAo*1wHo8G$Q8O&PCjyswjEAwR~SY%Co1m70;_{saw@t^NQ2 diff --git a/docs/management/connectors/images/serverlog-params-test.png b/docs/management/connectors/images/serverlog-params-test.png index 762721c7ead453272135c136e3cb1fcd4fb2d899..789381949bd43d2c7b21f747a1002cdd264129ec 100644 GIT binary patch literal 110458 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 diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index 3315631fd94daa..7a3f8f9cb927cd 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -5,7 +5,7 @@ include::action-types/jira.asciidoc[] include::action-types/teams.asciidoc[] include::action-types/opsgenie.asciidoc[] include::action-types/pagerduty.asciidoc[] -include::action-types/server-log.asciidoc[] +include::action-types/server-log.asciidoc[leveloffset=+1] include::action-types/servicenow.asciidoc[leveloffset=+1] include::action-types/servicenow-sir.asciidoc[leveloffset=+1] include::action-types/servicenow-itom.asciidoc[leveloffset=+1] @@ -17,4 +17,3 @@ include::action-types/webhook.asciidoc[] include::action-types/cases-webhook.asciidoc[leveloffset=+1] include::action-types/xmatters.asciidoc[] include::pre-configured-connectors.asciidoc[leveloffset=+1] - diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/connector_types.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/connector_types.ts new file mode 100644 index 00000000000000..16eb4f7d7a21cc --- /dev/null +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/connector_types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const commonScreenshots = getService('commonScreenshots'); + const screenshotDirectories = ['response_ops_docs', 'stack_alerting']; + const pageObjects = getPageObjects(['common', 'header']); + const actions = getService('actions'); + const testSubjects = getService('testSubjects'); + + describe('connector types', function () { + beforeEach(async () => { + await pageObjects.common.navigateToApp('connectors'); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('serverlog connector screenshot', async () => { + await pageObjects.common.navigateToApp('connectors'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await actions.common.openNewConnectorForm('server-log'); + await testSubjects.setValue('nameInput', 'Server log test connector'); + await commonScreenshots.takeScreenshot('serverlog-connector', screenshotDirectories); + const saveTestButton = await testSubjects.find('create-connector-flyout-save-test-btn'); + await saveTestButton.click(); + await commonScreenshots.takeScreenshot('serverlog-params-test', screenshotDirectories); + const flyOutCancelButton = await testSubjects.find('euiFlyoutCloseButton'); + await flyOutCancelButton.click(); + }); + }); +} diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index.ts index 200a3ae1e8793c..85e118756c4b7f 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/index.ts @@ -7,8 +7,26 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { +export default function ({ loadTestFile, getService }: FtrProviderContext) { + const browser = getService('browser'); + const actions = getService('actions'); + describe('stack alerting', function () { + before(async () => { + await browser.setWindowSize(1920, 1080); + await actions.api.createConnector({ + name: 'server-log-connector', + config: {}, + secrets: {}, + connectorTypeId: '.server-log', + }); + }); + + after(async () => { + await actions.api.deleteAllConnectors(); + }); + loadTestFile(require.resolve('./list_view')); + loadTestFile(require.resolve('./connector_types')); }); } diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/list_view.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/list_view.ts index cdad076a62ba34..2b7df0bfd6e485 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/list_view.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/list_view.ts @@ -11,12 +11,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const commonScreenshots = getService('commonScreenshots'); const screenshotDirectories = ['response_ops_docs', 'stack_alerting']; const pageObjects = getPageObjects(['common', 'header']); - const actions = getService('actions'); const rules = getService('rules'); const testSubjects = getService('testSubjects'); describe('list view', function () { - let serverLogConnectorId: string; let ruleId: string; const indexThresholdRule = { consumer: 'alerts', @@ -38,13 +36,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; before(async () => { - const connectorName = `server-log-connector`; - ({ id: serverLogConnectorId } = await createServerLogConnector(connectorName)); ({ id: ruleId } = await rules.api.createRule(indexThresholdRule)); }); after(async () => { - await actions.api.deleteConnector(serverLogConnectorId); await rules.api.deleteRule(ruleId); }); @@ -87,13 +82,4 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await commonScreenshots.takeScreenshot('snooze-panel', screenshotDirectories, 1400, 1024); }); }); - - const createServerLogConnector = async (name: string) => { - return actions.api.createConnector({ - name, - config: {}, - secrets: {}, - connectorTypeId: '.server-log', - }); - }; } From 01c0739ae097d71e049ee9c3eba8fc6a7f9148a3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 1 Feb 2023 02:31:49 +0000 Subject: [PATCH 22/59] skip flaky suite (#128836) --- .../index_management/__jest__/a11y/indices_tab.a11y.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts b/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts index e6c071225316fa..89717134002eca 100644 --- a/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts +++ b/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts @@ -53,7 +53,8 @@ describe('A11y Indices tab', () => { await expectToBeAccessible(component); }); - describe('index details flyout', () => { + // FLAKY: https://github.com/elastic/kibana/issues/128836 + describe.skip('index details flyout', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadIndicesResponse([ createNonDataStreamIndex('non-data-stream-test-index'), From 9987f5734455347a569fb51b68d16fe297218f71 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 1 Feb 2023 00:55:53 -0500 Subject: [PATCH 23/59] [api-docs] 2023-02-01 Daily api_docs build (#150002) Generated by https://buildkite.com/elastic/kibana-api-docs-daily/builds/235 --- api_docs/actions.mdx | 2 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.mdx | 2 +- api_docs/apm.devdocs.json | 12 +- 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.mdx | 2 +- api_docs/cloud_chat.mdx | 2 +- api_docs/cloud_data_migration.mdx | 2 +- api_docs/cloud_defend.mdx | 2 +- api_docs/cloud_experiments.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/content_management.devdocs.json | 100 ++++ api_docs/content_management.mdx | 43 ++ api_docs/controls.mdx | 2 +- api_docs/core.devdocs.json | 292 +++++++++- api_docs/core.mdx | 4 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.mdx | 2 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.devdocs.json | 16 + 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.devdocs.json | 8 + 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 | 18 +- api_docs/deprecations_by_team.mdx | 2 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- 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.mdx | 2 +- 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.mdx | 2 +- api_docs/expressions.mdx | 2 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/files.mdx | 2 +- api_docs/files_management.mdx | 2 +- api_docs/fleet.devdocs.json | 11 - api_docs/fleet.mdx | 4 +- api_docs/global_search.mdx | 2 +- api_docs/guided_onboarding.mdx | 2 +- api_docs/home.mdx | 2 +- api_docs/image_embeddable.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_analytics_shippers_gainsight.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_synthtrace.mdx | 2 +- .../kbn_apm_synthtrace_client.devdocs.json | 2 +- api_docs/kbn_apm_synthtrace_client.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_cell_actions.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_code_editor.devdocs.json | 65 +++ api_docs/kbn_code_editor.mdx | 30 + api_docs/kbn_code_editor_mocks.devdocs.json | 547 ++++++++++++++++++ api_docs/kbn_code_editor_mocks.mdx | 33 ++ 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_content_editor.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_apps_server_internal.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_custom_branding_browser.mdx | 2 +- ..._core_custom_branding_browser_internal.mdx | 2 +- ...kbn_core_custom_branding_browser_mocks.mdx | 2 +- api_docs/kbn_core_custom_branding_common.mdx | 2 +- ...n_core_custom_branding_server.devdocs.json | 95 ++- api_docs/kbn_core_custom_branding_server.mdx | 4 +- ...n_core_custom_branding_server_internal.mdx | 2 +- .../kbn_core_custom_branding_server_mocks.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 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- ...kbn_core_integrations_browser_internal.mdx | 2 +- .../kbn_core_integrations_browser_mocks.mdx | 2 +- .../kbn_core_lifecycle_browser.devdocs.json | 12 +- api_docs/kbn_core_lifecycle_browser.mdx | 2 +- api_docs/kbn_core_lifecycle_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_server.mdx | 2 +- api_docs/kbn_core_lifecycle_server_mocks.mdx | 2 +- api_docs/kbn_core_logging_browser_mocks.mdx | 2 +- api_docs/kbn_core_logging_common_internal.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_plugins_server.mdx | 2 +- api_docs/kbn_core_plugins_server_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 +- api_docs/kbn_core_root_server_internal.mdx | 2 +- ...ore_saved_objects_api_browser.devdocs.json | 6 +- .../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 +- ...kbn_core_saved_objects_common.devdocs.json | 28 + 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 +- api_docs/kbn_core_test_helpers_kbn_server.mdx | 2 +- ...n_core_test_helpers_so_type_serializer.mdx | 2 +- api_docs/kbn_core_test_helpers_test_utils.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_cypress_config.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.devdocs.json | 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_ecs.mdx | 2 +- api_docs/kbn_es.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_guided_onboarding.devdocs.json | 160 +---- api_docs/kbn_guided_onboarding.mdx | 4 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_hapi_mocks.mdx | 2 +- api_docs/kbn_health_gateway_server.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_i18n_react.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_json_ast.mdx | 2 +- .../kbn_kibana_manifest_schema.devdocs.json | 265 ++++++++- api_docs/kbn_kibana_manifest_schema.mdx | 4 +- .../kbn_language_documentation_popover.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_date_picker.mdx | 2 +- api_docs/kbn_ml_is_defined.mdx | 2 +- api_docs/kbn_ml_is_populated_object.mdx | 2 +- api_docs/kbn_ml_local_storage.mdx | 2 +- api_docs/kbn_ml_nested_property.mdx | 2 +- api_docs/kbn_ml_query_utils.mdx | 2 +- api_docs/kbn_ml_string_hash.mdx | 2 +- api_docs/kbn_ml_url_state.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_file_maps.mdx | 2 +- api_docs/kbn_repo_linter.mdx | 2 +- api_docs/kbn_repo_path.mdx | 2 +- api_docs/kbn_repo_source_classifier.mdx | 2 +- api_docs/kbn_rison.mdx | 2 +- api_docs/kbn_rule_data_utils.devdocs.json | 17 +- api_docs/kbn_rule_data_utils.mdx | 4 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_ecs.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 +- api_docs/kbn_shared_ux_avatar_solution.mdx | 2 +- ...ared_ux_avatar_user_profile_components.mdx | 2 +- .../kbn_shared_ux_button_exit_full_screen.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 +- api_docs/kbn_shared_ux_file_context.mdx | 2 +- api_docs/kbn_shared_ux_file_image.mdx | 2 +- api_docs/kbn_shared_ux_file_image_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_picker.mdx | 2 +- api_docs/kbn_shared_ux_file_upload.mdx | 2 +- api_docs/kbn_shared_ux_file_util.mdx | 2 +- api_docs/kbn_shared_ux_link_redirect_app.mdx | 2 +- .../kbn_shared_ux_link_redirect_app_mocks.mdx | 2 +- api_docs/kbn_shared_ux_markdown.mdx | 2 +- api_docs/kbn_shared_ux_markdown_mocks.mdx | 2 +- ...red_ux_page_analytics_no_data.devdocs.json | 4 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- ..._page_analytics_no_data_mocks.devdocs.json | 66 +++ ...shared_ux_page_analytics_no_data_mocks.mdx | 4 +- ...shared_ux_page_kibana_no_data.devdocs.json | 4 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- ..._ux_page_kibana_no_data_mocks.devdocs.json | 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 +- ...bn_shared_ux_prompt_not_found.devdocs.json | 4 +- api_docs/kbn_shared_ux_prompt_not_found.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_slo_schema.devdocs.json | 69 ++- api_docs/kbn_slo_schema.mdx | 4 +- api_docs/kbn_some_dev_log.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_ts_projects.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_actions_browser.mdx | 2 +- api_docs/kbn_ui_shared_deps_src.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.mdx | 2 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- 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/notifications.mdx | 2 +- api_docs/observability.devdocs.json | 260 +++++++-- api_docs/observability.mdx | 4 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 35 +- 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.devdocs.json | 165 +++++- api_docs/rule_registry.mdx | 4 +- 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.devdocs.json | 2 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.mdx | 2 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.devdocs.json | 15 + api_docs/spaces.mdx | 4 +- 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.devdocs.json | 6 +- api_docs/threat_intelligence.mdx | 4 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.devdocs.json | 30 + api_docs/triggers_actions_ui.mdx | 4 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_field_list.mdx | 2 +- api_docs/unified_histogram.mdx | 2 +- 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 +- 503 files changed, 2624 insertions(+), 764 deletions(-) create mode 100644 api_docs/content_management.devdocs.json create mode 100644 api_docs/content_management.mdx create mode 100644 api_docs/kbn_code_editor.devdocs.json create mode 100644 api_docs/kbn_code_editor.mdx create mode 100644 api_docs/kbn_code_editor_mocks.devdocs.json create mode 100644 api_docs/kbn_code_editor_mocks.mdx diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index a36bd508f0ad0f..221799d01a1238 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 6855f8f9002c83..d79466e2ab8748 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: 2023-01-30 +date: 2023-02-01 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 2afb1f1ffe062d..65d5f0f25e24c3 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index ed0c19af12faf4..04374ce79bf365 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index ec829fca26741e..95bf2cd443bc6c 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -202,7 +202,7 @@ "APMPluginSetupDependencies", ") => { config$: ", "Observable", - "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; searchAggregatedTransactions: ", + "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; maxTraceItems: number; }>; searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; telemetryCollectionEnabled: boolean; metricsInterval: number; agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; forceSyntheticSource: boolean; }>>; getApmIndices: () => Promise>; createApmEventClient: ({ request, context, debug, }: { debug?: boolean | undefined; request: ", { @@ -448,7 +448,7 @@ "label": "config", "description": [], "signature": [ - "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", + "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; readonly telemetryCollectionEnabled: boolean; readonly metricsInterval: number; readonly agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; readonly forceSyntheticSource: boolean; }" ], @@ -840,7 +840,7 @@ "label": "APMConfig", "description": [], "signature": [ - "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", + "{ readonly indices: Readonly<{} & { metric: string; error: string; span: string; transaction: string; onboarding: string; }>; readonly autoCreateApmDataView: boolean; readonly serviceMapEnabled: boolean; readonly serviceMapFingerprintBucketSize: number; readonly serviceMapFingerprintGlobalBucketSize: number; readonly serviceMapTraceIdBucketSize: number; readonly serviceMapTraceIdGlobalBucketSize: number; readonly serviceMapMaxTracesPerRequest: number; readonly ui: Readonly<{} & { enabled: boolean; maxTraceItems: number; }>; readonly searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; readonly telemetryCollectionEnabled: boolean; readonly metricsInterval: number; readonly agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; readonly forceSyntheticSource: boolean; }" ], @@ -4798,7 +4798,7 @@ "section": "def-server.APMRouteHandlerResources", "text": "APMRouteHandlerResources" }, - ", { transactionGroups: { transactionType: string; name: string; latency: number | null; throughput: number; errorRate: number; impact: number; }[]; isAggregationAccurate: boolean; bucketSize: number; }, ", + ", { transactionGroups: { transactionType: string; name: string; latency: number | null; throughput: number; errorRate: number; impact: number; }[]; transactionOverflowCount: number; maxTransactionGroupsExceeded: boolean; }, ", "APMRouteCreateOptions", ">; \"GET /internal/apm/traces/{traceId}/spans/{spanId}\": ", { @@ -6396,7 +6396,7 @@ "AgentName", "; } & { serviceName: string; healthStatus: ", "ServiceHealthStatus", - "; } & { serviceName: string; alertsCount: number; }>; }, ", + "; } & { serviceName: string; alertsCount: number; }>; maxServiceCountExceeded: boolean; serviceOverflowCount: number; }, ", "APMRouteCreateOptions", ">; \"GET /internal/apm/service-map/dependency\": ", { @@ -7679,7 +7679,7 @@ "description": [], "signature": [ "Observable", - "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; transactionGroupBucketSize: number; maxTraceItems: number; }>; searchAggregatedTransactions: ", + "; autoCreateApmDataView: boolean; serviceMapEnabled: boolean; serviceMapFingerprintBucketSize: number; serviceMapFingerprintGlobalBucketSize: number; serviceMapTraceIdBucketSize: number; serviceMapTraceIdGlobalBucketSize: number; serviceMapMaxTracesPerRequest: number; ui: Readonly<{} & { enabled: boolean; maxTraceItems: number; }>; searchAggregatedTransactions: ", "SearchAggregatedTransactionSetting", "; telemetryCollectionEnabled: boolean; metricsInterval: number; agent: Readonly<{} & { migrations: Readonly<{} & { enabled: boolean; }>; }>; forceSyntheticSource: boolean; }>>" ], diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index a0a63f1dd4367b..886fb1f646cbfe 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: 2023-01-30 +date: 2023-02-01 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 bb08afc5888306..deef5b83df2399 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: 2023-01-30 +date: 2023-02-01 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 eae171c59adc6e..514f8dbeb2ec81 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: 2023-01-30 +date: 2023-02-01 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 0d228f8ad9a4d1..8ba0ff20790c54 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: 2023-01-30 +date: 2023-02-01 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 227d548020973e..5cfc63f80c1f88 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: 2023-01-30 +date: 2023-02-01 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 7743510f26d880..ea60e79861967b 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index fe1dec5fe63103..4ee915cd8af701 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index ce2e358ea9f507..b4518b3731828f 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index a90893f737fb1b..0fdfd436b0cd21 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index 0f4a756b7d724e..2be599b951cb58 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index d82d0233f9c1b5..eb16bfe1c51ce2 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: 2023-01-30 +date: 2023-02-01 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 ce8800ed5cb1ce..10b74f47eaef8f 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: 2023-01-30 +date: 2023-02-01 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 35c90d18566f32..f98da45c805b39 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.devdocs.json b/api_docs/content_management.devdocs.json new file mode 100644 index 00000000000000..338c6568fdb4da --- /dev/null +++ b/api_docs/content_management.devdocs.json @@ -0,0 +1,100 @@ +{ + "id": "contentManagement", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "start": { + "parentPluginId": "contentManagement", + "id": "def-public.ContentManagementPublicStart", + "type": "Interface", + "tags": [], + "label": "ContentManagementPublicStart", + "description": [], + "path": "src/plugins/content_management/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "contentManagement", + "id": "def-server.ContentManagementServerSetup", + "type": "Interface", + "tags": [], + "label": "ContentManagementServerSetup", + "description": [], + "path": "src/plugins/content_management/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "contentManagement", + "id": "def-server.ContentManagementServerStart", + "type": "Interface", + "tags": [], + "label": "ContentManagementServerStart", + "description": [], + "path": "src/plugins/content_management/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "contentManagement", + "id": "def-common.API_ENDPOINT", + "type": "string", + "tags": [], + "label": "API_ENDPOINT", + "description": [], + "signature": [ + "\"/api/content_management\"" + ], + "path": "src/plugins/content_management/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "contentManagement", + "id": "def-common.PLUGIN_ID", + "type": "string", + "tags": [], + "label": "PLUGIN_ID", + "description": [], + "signature": [ + "\"contentManagement\"" + ], + "path": "src/plugins/content_management/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx new file mode 100644 index 00000000000000..01cb0b596215cf --- /dev/null +++ b/api_docs/content_management.mdx @@ -0,0 +1,43 @@ +--- +#### +#### 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: kibContentManagementPluginApi +slug: /kibana-dev-docs/api/contentManagement +title: "contentManagement" +image: https://source.unsplash.com/400x175/?github +description: API docs for the contentManagement plugin +date: 2023-02-01 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] +--- +import contentManagementObj from './content_management.devdocs.json'; + +Content management app + +Contact [@elastic/kibana-global-experience](https://github.com/orgs/elastic/teams/@elastic/kibana-global-experience) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 5 | 0 | 5 | 0 | + +## Client + +### Start + + +## Server + +### Setup + + +### Start + + +## Common + +### Consts, variables and types + + diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 266c100addc1c2..4c1f5d51453343 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index 04ff270cbf464d..7c69151e0d644f 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -5098,27 +5098,27 @@ }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" }, { "plugin": "synthetics", @@ -5408,6 +5408,110 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-public.CustomBrandingSetup", + "type": "Interface", + "tags": [], + "label": "CustomBrandingSetup", + "description": [], + "path": "packages/core/custom-branding/core-custom-branding-browser/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-public.CustomBrandingSetup.customBranding$", + "type": "Object", + "tags": [], + "label": "customBranding$", + "description": [], + "signature": [ + "Observable", + "<", + { + "pluginId": "@kbn/core-custom-branding-common", + "scope": "common", + "docId": "kibKbnCoreCustomBrandingCommonPluginApi", + "section": "def-common.CustomBranding", + "text": "CustomBranding" + }, + ">" + ], + "path": "packages/core/custom-branding/core-custom-branding-browser/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "core", + "id": "def-public.CustomBrandingSetup.hasCustomBranding$", + "type": "Object", + "tags": [], + "label": "hasCustomBranding$", + "description": [], + "signature": [ + "Observable", + "" + ], + "path": "packages/core/custom-branding/core-custom-branding-browser/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "core", + "id": "def-public.CustomBrandingStart", + "type": "Interface", + "tags": [], + "label": "CustomBrandingStart", + "description": [], + "path": "packages/core/custom-branding/core-custom-branding-browser/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-public.CustomBrandingStart.customBranding$", + "type": "Object", + "tags": [], + "label": "customBranding$", + "description": [], + "signature": [ + "Observable", + "<", + { + "pluginId": "@kbn/core-custom-branding-common", + "scope": "common", + "docId": "kibKbnCoreCustomBrandingCommonPluginApi", + "section": "def-common.CustomBranding", + "text": "CustomBranding" + }, + ">" + ], + "path": "packages/core/custom-branding/core-custom-branding-browser/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "core", + "id": "def-public.CustomBrandingStart.hasCustomBranding$", + "type": "Object", + "tags": [], + "label": "hasCustomBranding$", + "description": [], + "signature": [ + "Observable", + "" + ], + "path": "packages/core/custom-branding/core-custom-branding-browser/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-public.DeprecationsServiceStart", @@ -15033,15 +15137,15 @@ }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" }, { "plugin": "graph", @@ -21018,6 +21122,14 @@ "plugin": "cases", "path": "x-pack/plugins/cases/server/services/so_references.ts" }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, { "plugin": "lists", "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts" @@ -22727,6 +22839,18 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/share_action.ts" }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts" + }, { "plugin": "infra", "path": "x-pack/plugins/infra/public/common/visualizations/lens/types.ts" @@ -22798,6 +22922,14 @@ { "plugin": "cases", "path": "x-pack/plugins/cases/server/services/user_actions/test_utils.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.test.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.test.ts" } ], "initialIsOpen": false @@ -32746,6 +32878,148 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup", + "type": "Interface", + "tags": [], + "label": "CustomBrandingSetup", + "description": [], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup.register", + "type": "Function", + "tags": [], + "label": "register", + "description": [], + "signature": [ + "(fetchFn: ", + { + "pluginId": "@kbn/core-custom-branding-server", + "scope": "common", + "docId": "kibKbnCoreCustomBrandingServerPluginApi", + "section": "def-common.CustomBrandingFetchFn", + "text": "CustomBrandingFetchFn" + }, + ") => void" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup.register.$1", + "type": "Function", + "tags": [], + "label": "fetchFn", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-custom-branding-server", + "scope": "common", + "docId": "kibKbnCoreCustomBrandingServerPluginApi", + "section": "def-common.CustomBrandingFetchFn", + "text": "CustomBrandingFetchFn" + } + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup.getBrandingFor", + "type": "Function", + "tags": [], + "label": "getBrandingFor", + "description": [], + "signature": [ + "(request: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "common", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-common.KibanaRequest", + "text": "KibanaRequest" + }, + ", options: { unauthenticated?: boolean | undefined; }) => Promise<", + { + "pluginId": "@kbn/core-custom-branding-common", + "scope": "common", + "docId": "kibKbnCoreCustomBrandingCommonPluginApi", + "section": "def-common.CustomBranding", + "text": "CustomBranding" + }, + ">" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup.getBrandingFor.$1", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "common", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-common.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup.getBrandingFor.$2", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.CustomBrandingSetup.getBrandingFor.$2.unauthenticated", + "type": "CompoundType", + "tags": [], + "label": "unauthenticated", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-server.CustomHttpResponseOptions", diff --git a/api_docs/core.mdx b/api_docs/core.mdx index da92cad1c112cc..e5e7a520235a25 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] --- import coreObj from './core.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 | |-------------------|-----------|------------------------|-----------------| -| 2832 | 17 | 1016 | 0 | +| 2845 | 17 | 1029 | 0 | ## Client diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 32e364fe911fb1..9f9d2f9a7704f6 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 800cf884e25951..ec379e1c1630f8 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index cc5ff14a7dff3c..4bc0c9cdfc4f49 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index b8f5b5f037f207..b00740d7bc6cb9 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -11061,6 +11061,14 @@ "plugin": "cases", "path": "x-pack/plugins/cases/server/services/so_references.ts" }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, { "plugin": "lists", "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts" @@ -28906,6 +28914,14 @@ "plugin": "cases", "path": "x-pack/plugins/cases/server/services/so_references.ts" }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, { "plugin": "lists", "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts" diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 2c219784f20b91..65264eba67c933 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: 2023-01-30 +date: 2023-02-01 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 69ec8375434e99..bce5fe2ef43346 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: 2023-01-30 +date: 2023-02-01 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 e7ac4a8b2ea7d6..ccaab0205ad5a9 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: 2023-01-30 +date: 2023-02-01 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 10d3dd109f98b4..515732f643f8d6 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: 2023-01-30 +date: 2023-02-01 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 333a495f43154c..5abd399934e653 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: 2023-01-30 +date: 2023-02-01 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 3dcafe9a4960a1..0bffb1b96517b5 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index c945daf504f472..466a2574976432 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -26220,6 +26220,14 @@ "plugin": "cases", "path": "x-pack/plugins/cases/server/services/so_references.ts" }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, { "plugin": "lists", "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts" diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 3592cc891b24c2..0aec4d3dc3fd2c 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: 2023-01-30 +date: 2023-02-01 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 8ef7e6b0031088..e98c997be37488 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: 2023-01-30 +date: 2023-02-01 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 e47f998948116e..a1edbcbe15b146 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index fd4dae4f0f318f..136bc89aa7fd1c 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -478,8 +478,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject)+ 22 more | - | -| | [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject)+ 38 more | - | +| | [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject)+ 26 more | - | +| | [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [so_references.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/so_references.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [find.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/operations/find.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject), [test_utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/server/services/user_actions/test_utils.ts#:~:text=SavedObject)+ 44 more | - | | | [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=find) | - | | | [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject), [saved_objects_finder.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx#:~:text=SimpleSavedObject)+ 1 more | - | | | [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/common/ui/types.ts#:~:text=ResolvedSimpleSavedObject), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/common/ui/types.ts#:~:text=ResolvedSimpleSavedObject), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/common/ui/types.ts#:~:text=ResolvedSimpleSavedObject), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/cases/common/ui/types.ts#:~:text=ResolvedSimpleSavedObject) | - | @@ -976,7 +976,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectsCreateOptions), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectsCreateOptions), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectsCreateOptions), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectsCreateOptions) | - | | | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectsBatchResponse), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectsBatchResponse) | - | | | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes)+ 2 more | - | -| | [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [memory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/memory.ts#:~:text=SavedObjectReference), [memory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/memory.ts#:~:text=SavedObjectReference), [rx.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/rx.ts#:~:text=SavedObjectReference), [rx.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/rx.ts#:~:text=SavedObjectReference)+ 6 more | - | +| | [log_threshold_references_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts#:~:text=SavedObjectReference), [log_threshold_references_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts#:~:text=SavedObjectReference), [log_threshold_references_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts#:~:text=SavedObjectReference), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [memory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/memory.ts#:~:text=SavedObjectReference)+ 11 more | - | | | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=savedObjects), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=savedObjects), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=savedObjects), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=savedObjects), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=savedObjects), [use_delete_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx#:~:text=savedObjects), [use_delete_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx#:~:text=savedObjects), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=savedObjects), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=savedObjects), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=savedObjects)+ 1 more | - | | | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=savedObjects), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=savedObjects), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=savedObjects), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=savedObjects), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=savedObjects), [use_delete_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx#:~:text=savedObjects), [use_delete_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx#:~:text=savedObjects), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=savedObjects), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=savedObjects), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=savedObjects)+ 1 more | - | | | [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=create) | - | @@ -988,7 +988,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectsCreateOptions), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectsCreateOptions), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectsCreateOptions), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectsCreateOptions) | - | | | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectsBatchResponse), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectsBatchResponse) | - | | | [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_find_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_create_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_get_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes), [use_update_saved_object.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx#:~:text=SavedObjectAttributes)+ 2 more | - | -| | [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [memory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/memory.ts#:~:text=SavedObjectReference), [memory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/memory.ts#:~:text=SavedObjectReference), [rx.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/rx.ts#:~:text=SavedObjectReference), [rx.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/rx.ts#:~:text=SavedObjectReference)+ 6 more | - | +| | [log_threshold_references_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts#:~:text=SavedObjectReference), [log_threshold_references_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts#:~:text=SavedObjectReference), [log_threshold_references_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts#:~:text=SavedObjectReference), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/types.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [cpu.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/cpu.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [load.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/load.ts#:~:text=SavedObjectReference), [memory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/infra/public/common/visualizations/lens/hosts/memory.ts#:~:text=SavedObjectReference)+ 11 more | - | @@ -1506,16 +1506,16 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=SavedObjectsClientContract) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=create), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=create) | - | | | [delete_param.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx#:~:text=bulkDelete) | - | -| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=find), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=find), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find) | - | +| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=find), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=find), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=get), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=get) | - | | | [use_invalid_monitors.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_invalid_monitors.tsx#:~:text=bulkResolve), [use_recently_viewed_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts#:~:text=bulkResolve) | - | | | [synthetics_monitor.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts#:~:text=SimpleSavedObject), [synthetics_monitor.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts#:~:text=SimpleSavedObject), [synthetics_monitor.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts#:~:text=SimpleSavedObject) | - | -| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_invalid_monitors.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_invalid_monitors.tsx#:~:text=savedObjects), [delete_param.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=savedObjects), [use_recently_viewed_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts#:~:text=savedObjects), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=savedObjects)+ 1 more | - | -| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_invalid_monitors.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_invalid_monitors.tsx#:~:text=savedObjects), [delete_param.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=savedObjects), [use_recently_viewed_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts#:~:text=savedObjects), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=savedObjects)+ 1 more | - | +| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_invalid_monitors.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_invalid_monitors.tsx#:~:text=savedObjects), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=savedObjects), [use_recently_viewed_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts#:~:text=savedObjects), [delete_param.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=savedObjects)+ 1 more | - | +| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_invalid_monitors.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_invalid_monitors.tsx#:~:text=savedObjects), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=savedObjects), [use_recently_viewed_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts#:~:text=savedObjects), [delete_param.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx#:~:text=savedObjects), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=savedObjects), [use_locations_api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts#:~:text=savedObjects), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=savedObjects)+ 1 more | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=SavedObjectsClientContract), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=SavedObjectsClientContract) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=create), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=create) | - | | | [delete_param.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx#:~:text=bulkDelete) | - | -| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=find), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=find), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find) | - | +| | [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/use_monitor_name.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/hooks/use_location_monitors.ts#:~:text=find), [use_filters.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts#:~:text=find), [use_location_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts#:~:text=find), [use_monitor_name.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find), [use_filters.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts#:~:text=find) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/state/private_locations/api.ts#:~:text=get), [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/api.ts#:~:text=get) | - | | | [use_invalid_monitors.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_invalid_monitors.tsx#:~:text=bulkResolve), [use_recently_viewed_monitors.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts#:~:text=bulkResolve) | - | | | [synthetics_monitor.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts#:~:text=SimpleSavedObject), [synthetics_monitor.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts#:~:text=SimpleSavedObject), [synthetics_monitor.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/common/types/synthetics_monitor.ts#:~:text=SimpleSavedObject) | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index f30544104b267b..71c1ebf2c534a7 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 021378947c8eed..6aac317221159a 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: 2023-01-30 +date: 2023-02-01 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 99e6bd794bc63e..57272e2c5e4414 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: 2023-01-30 +date: 2023-02-01 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 45ae471ab02344..92c7813c607c29 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 036fc068e8f61f..1261f654bd9bd4 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: 2023-01-30 +date: 2023-02-01 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 1057d65fc99b91..73e81c93a52d9d 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: 2023-01-30 +date: 2023-02-01 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 5ad74c80cf9ec0..1bc9189ecf45fb 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: 2023-01-30 +date: 2023-02-01 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 3f5f4e0ff0b91a..57e5b3d028fe88 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: 2023-01-30 +date: 2023-02-01 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 551d633a05a687..b3ed7e109d2bf5 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: 2023-01-30 +date: 2023-02-01 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 079c249780414c..d8980493ab39d2 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: 2023-01-30 +date: 2023-02-01 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 326a2280f1c2d1..2ae1e4a86c08b6 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: 2023-01-30 +date: 2023-02-01 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 eb2824379b7664..f74e7651597ae3 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: 2023-01-30 +date: 2023-02-01 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 3d1c7b74a14e29..b503d2da2f5610 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index cc540a226955a8..dac8edc8dfa5d7 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 5af5306ba4d058..ba9edb7fc4d14d 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: 2023-01-30 +date: 2023-02-01 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 afc44e3f5b28fa..26e08a00d9f967 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: 2023-01-30 +date: 2023-02-01 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 65876d1ada699b..0308a37af8974b 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: 2023-01-30 +date: 2023-02-01 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 9e02dbb25fdb82..6936d6f7499885 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: 2023-01-30 +date: 2023-02-01 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 2c90a8e7f89af3..cc03b631c22341 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: 2023-01-30 +date: 2023-02-01 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 64ba3e3442a558..0cb42d317eebbe 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: 2023-01-30 +date: 2023-02-01 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 58f1a170b192ed..55f6f26288a8be 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: 2023-01-30 +date: 2023-02-01 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 b0efd4948a4fa6..31d5f46c710432 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: 2023-01-30 +date: 2023-02-01 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 baaf83c93dbc81..6e7ffea406d528 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index ea72821ebaa384..3c6e6dfeacbdc5 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 7d5f816adaa3ab..8931fa70257b99 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index ae4a52e4885f3c..b5538ec05578fe 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: 2023-01-30 +date: 2023-02-01 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 17644650523638..c6b1b0fa71b83c 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: 2023-01-30 +date: 2023-02-01 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 8d5ccdc6123972..7427690517b7e0 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: 2023-01-30 +date: 2023-02-01 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 3a8a8237f0b94d..eac27a443386b4 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 74b7a1f3c88380..ece7b2d45555d8 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.devdocs.json b/api_docs/fleet.devdocs.json index 6d9c800e6de48a..fb8f22cb4a0578 100644 --- a/api_docs/fleet.devdocs.json +++ b/api_docs/fleet.devdocs.json @@ -11206,17 +11206,6 @@ "deprecated": false, "trackAdoption": false, "children": [ - { - "parentPluginId": "fleet", - "id": "def-common.GetAgentsResponse.totalInactive", - "type": "number", - "tags": [], - "label": "totalInactive", - "description": [], - "path": "x-pack/plugins/fleet/common/types/rest_spec/agent.ts", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "fleet", "id": "def-common.GetAgentsResponse.list", diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index b899d6d2de6631..d61c24e7d48848 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Fleet](https://github.com/orgs/elastic/teams/fleet) for questions regar | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 1040 | 3 | 935 | 25 | +| 1039 | 3 | 934 | 25 | ## Client diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index bcf7038f4b8514..407d07b45f5eab 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: 2023-01-30 +date: 2023-02-01 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 4d246c79461e53..cfe276612a1807 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: 2023-01-30 +date: 2023-02-01 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 4854c9abced739..f1e0ed4b1d3577 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index a6699d0e86c3df..25a53cf4027ce1 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 1d9adb71ac0e59..f041f767f3403f 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: 2023-01-30 +date: 2023-02-01 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 1ceea6db436602..5583391b4c5491 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: 2023-01-30 +date: 2023-02-01 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 eb094011f74437..70fde958a94cbd 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: 2023-01-30 +date: 2023-02-01 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 8f1d5f0129a8af..248912181e054f 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: 2023-01-30 +date: 2023-02-01 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 50efdb79e6cfbe..bead9ba897c094 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: 2023-01-30 +date: 2023-02-01 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 e9a76aa665eae0..d116c21384942e 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: 2023-01-30 +date: 2023-02-01 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 8f0fd576c413b5..e52ad4a76232d7 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: 2023-01-30 +date: 2023-02-01 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 7d18a038dbd529..00574cb8f28746 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: 2023-01-30 +date: 2023-02-01 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 24a185f0bcafb8..b0d5a104db7012 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: 2023-01-30 +date: 2023-02-01 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 fb7772ac2e53a0..67a8e2d8c93528 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: 2023-01-30 +date: 2023-02-01 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 b590e8eff06d73..0579e493822f90 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: 2023-01-30 +date: 2023-02-01 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 0d15a40c8abed7..e58af1a56f1300 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: 2023-01-30 +date: 2023-02-01 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 ad86db69ebf2ac..95e3028ab798a2 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: 2023-01-30 +date: 2023-02-01 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 db8d903c066148..e2663779baf75a 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: 2023-01-30 +date: 2023-02-01 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 39db1ca3e617cb..dd15c31d81e0db 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index 1e7983eba24b81..92a136fc57a731 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index b60512f023b359..5b3dc0b49fadfa 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: 2023-01-30 +date: 2023-02-01 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 186c425d73057a..c1e6e441d3a8ac 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.devdocs.json b/api_docs/kbn_apm_synthtrace_client.devdocs.json index b0422331cb101c..7c272f970dbc0c 100644 --- a/api_docs/kbn_apm_synthtrace_client.devdocs.json +++ b/api_docs/kbn_apm_synthtrace_client.devdocs.json @@ -2318,7 +2318,7 @@ "GeoLocation", "; 'client.geo.region_iso_code': string; 'client.geo.region_name': string; 'client.ip': string; 'cloud.account.id': string; 'cloud.account.name': string; 'cloud.availability_zone': string; 'cloud.machine.type': string; 'cloud.project.id': string; 'cloud.project.name': string; 'cloud.provider': string; 'cloud.region': string; 'cloud.service.name': string; 'container.id': string; 'destination.address': string; 'destination.port': number; 'device.id': string; 'device.manufacturer': string; 'device.model.identifier': string; 'device.model.name': string; 'ecs.version': string; 'error.exception': ", "ApmException", - "[]; 'error.grouping_key': string; 'error.grouping_name': string; 'error.id': string; 'event.ingested': number; 'event.name': string; 'event.outcome': string; 'event.outcome_numeric': number | { sum: number; value_count: number; }; 'faas.coldstart': boolean; 'faas.execution': string; 'faas.id': string; 'faas.name': string; 'faas.trigger.type': string; 'faas.version': string; 'host.architecture': string; 'host.hostname': string; 'host.name': string; 'host.os.full': string; 'host.os.name': string; 'host.os.platform': string; 'host.os.type': string; 'host.os.version': string; 'http.request.method': string; 'http.response.status_code': number; 'kubernetes.pod.name': string; 'kubernetes.pod.uid': string; 'metricset.name': string; 'network.carrier.icc': string; 'network.carrier.mcc': string; 'network.carrier.mnc': string; 'network.carrier.name': string; 'network.connection.subtype': string; 'network.connection.type': string; 'observer.type': string; 'observer.version_major': number; 'observer.version': string; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'session.id': string; 'trace.id': string; 'transaction.duration.us': number; 'transaction.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'service.environment': string; 'service.framework.name': string; 'service.framework.version': string; 'service.language.name': string; 'service.language.version': string; 'service.name': string; 'service.node.name': string; 'service.runtime.name': string; 'service.runtime.version': string; 'service.target.name': string; 'service.target.type': string; 'service.version': string; 'span.action': string; 'span.destination.service.resource': string; 'span.destination.service.response_time.count': number; 'span.destination.service.response_time.sum.us': number; 'span.duration.us': number; 'span.id': string; 'span.name': string; 'span.self_time.count': number; 'span.self_time.sum.us': number; 'span.subtype': string; 'span.type': string; 'transaction.result': string; 'transaction.sampled': true; 'span.links': { trace: { id: string; }; span: { id: string; }; }[]; 'url.original': string; }> & Partial<{ 'system.process.memory.size': number; 'system.memory.actual.free': number; 'system.memory.total': number; 'system.cpu.total.norm.pct': number; 'system.process.memory.rss.bytes': number; 'system.process.cpu.total.norm.pct': number; 'jvm.memory.heap.used': number; 'jvm.memory.non_heap.used': number; 'jvm.thread.count': number; 'faas.billed_duration': number; 'faas.timeout': number; 'faas.coldstart_duration': number; 'faas.duration': number; }> & Partial<{ 'metricset.interval': string; 'transaction.duration.summary': string; }>" + "[]; 'error.grouping_key': string; 'error.grouping_name': string; 'error.id': string; 'event.ingested': number; 'event.name': string; 'event.outcome': string; 'event.outcome_numeric': number | { sum: number; value_count: number; }; 'faas.coldstart': boolean; 'faas.execution': string; 'faas.id': string; 'faas.name': string; 'faas.trigger.type': string; 'faas.version': string; 'host.architecture': string; 'host.hostname': string; 'host.name': string; 'host.os.full': string; 'host.os.name': string; 'host.os.platform': string; 'host.os.type': string; 'host.os.version': string; 'http.request.method': string; 'http.response.status_code': number; 'kubernetes.pod.name': string; 'kubernetes.pod.uid': string; 'metricset.name': string; 'network.carrier.icc': string; 'network.carrier.mcc': string; 'network.carrier.mnc': string; 'network.carrier.name': string; 'network.connection.subtype': string; 'network.connection.type': string; 'observer.type': string; 'observer.version_major': number; 'observer.version': string; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'session.id': string; 'trace.id': string; 'transaction.aggregation.overflow_count': number; 'transaction.duration.us': number; 'transaction.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'service.environment': string; 'service.framework.name': string; 'service.framework.version': string; 'service.language.name': string; 'service.language.version': string; 'service.name': string; 'service.node.name': string; 'service.runtime.name': string; 'service.runtime.version': string; 'service.target.name': string; 'service.target.type': string; 'service.version': string; 'span.action': string; 'span.destination.service.resource': string; 'span.destination.service.response_time.count': number; 'span.destination.service.response_time.sum.us': number; 'span.duration.us': number; 'span.id': string; 'span.name': string; 'span.self_time.count': number; 'span.self_time.sum.us': number; 'span.subtype': string; 'span.type': string; 'transaction.result': string; 'transaction.sampled': true; 'span.links': { trace: { id: string; }; span: { id: string; }; }[]; 'url.original': string; }> & Partial<{ 'system.process.memory.size': number; 'system.memory.actual.free': number; 'system.memory.total': number; 'system.cpu.total.norm.pct': number; 'system.process.memory.rss.bytes': number; 'system.process.cpu.total.norm.pct': number; 'jvm.memory.heap.used': number; 'jvm.memory.non_heap.used': number; 'jvm.thread.count': number; 'faas.billed_duration': number; 'faas.timeout': number; 'faas.coldstart_duration': number; 'faas.duration': number; }> & Partial<{ 'metricset.interval': string; 'transaction.duration.summary': string; }>" ], "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts", "deprecated": false, diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index 51fb8b80058583..6e24e3d773ce40 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index fbb95221542f5c..c74dc1ad893f0c 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: 2023-01-30 +date: 2023-02-01 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 35c0fe796b9c59..1efd294af66dca 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: 2023-01-30 +date: 2023-02-01 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 e2eac6071fba49..5b5452e73f2153 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index f0bc5b1fb97e39..2300a4ce345c63 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index 2c4a3016cc5110..71ef1da7f620db 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: 2023-01-30 +date: 2023-02-01 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 77e1342b6a7b84..2f77f83a5cb460 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: 2023-01-30 +date: 2023-02-01 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 9b4395d47a864c..7fe84bbafb7b6c 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: 2023-01-30 +date: 2023-02-01 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 4a84ced7636a97..0fc11118c93085 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: 2023-01-30 +date: 2023-02-01 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 6d1ff577044ab4..40311f52d8ddaa 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_code_editor.devdocs.json b/api_docs/kbn_code_editor.devdocs.json new file mode 100644 index 00000000000000..0bd40c08dd6c44 --- /dev/null +++ b/api_docs/kbn_code_editor.devdocs.json @@ -0,0 +1,65 @@ +{ + "id": "@kbn/code-editor", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/code-editor", + "id": "def-common.CodeEditor", + "type": "Function", + "tags": [], + "label": "CodeEditor", + "description": [], + "signature": [ + "({ languageId, value, onChange, width, options, overrideEditorWillMount, editorDidMount, editorWillMount, useDarkTheme, transparentBackground, suggestionProvider, signatureProvider, hoverProvider, placeholder, languageConfiguration, \"aria-label\": ariaLabel, isCopyable, allowFullScreen, }: React.PropsWithChildren<", + "Props", + ">) => JSX.Element" + ], + "path": "packages/shared-ux/code_editor/impl/code_editor.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor", + "id": "def-common.CodeEditor.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n languageId,\n value,\n onChange,\n width,\n options,\n overrideEditorWillMount,\n editorDidMount,\n editorWillMount,\n useDarkTheme,\n transparentBackground,\n suggestionProvider,\n signatureProvider,\n hoverProvider,\n placeholder,\n languageConfiguration,\n 'aria-label': ariaLabel = i18n.translate('sharedUXPackages.codeEditor.ariaLabel', {\n defaultMessage: 'Code Editor',\n }),\n isCopyable = false,\n allowFullScreen = false,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren<", + "Props", + ">" + ], + "path": "packages/shared-ux/code_editor/impl/code_editor.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx new file mode 100644 index 00000000000000..a7db5345c0dca3 --- /dev/null +++ b/api_docs/kbn_code_editor.mdx @@ -0,0 +1,30 @@ +--- +#### +#### 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: kibKbnCodeEditorPluginApi +slug: /kibana-dev-docs/api/kbn-code-editor +title: "@kbn/code-editor" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/code-editor plugin +date: 2023-02-01 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] +--- +import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 2 | 1 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_code_editor_mocks.devdocs.json b/api_docs/kbn_code_editor_mocks.devdocs.json new file mode 100644 index 00000000000000..1c39d497c16d1d --- /dev/null +++ b/api_docs/kbn_code_editor_mocks.devdocs.json @@ -0,0 +1,547 @@ +{ + "id": "@kbn/code-editor-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock", + "type": "Class", + "tags": [], + "label": "CodeEditorStorybookMock", + "description": [ + "\nStorybook mock for the `CodeEditor` component" + ], + "signature": [ + { + "pluginId": "@kbn/code-editor-mocks", + "scope": "common", + "docId": "kibKbnCodeEditorMocksPluginApi", + "section": "def-common.CodeEditorStorybookMock", + "text": "CodeEditorStorybookMock" + }, + " extends ", + { + "pluginId": "@kbn/shared-ux-storybook-mock", + "scope": "common", + "docId": "kibKbnSharedUxStorybookMockPluginApi", + "section": "def-common.AbstractStorybookMock", + "text": "AbstractStorybookMock" + }, + "<", + "Props", + ", {}, PropArguments, {}>" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments", + "type": "Object", + "tags": [], + "label": "propArguments", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.languageId", + "type": "Object", + "tags": [], + "label": "languageId", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.languageId.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.languageId.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.languageId.options", + "type": "Array", + "tags": [], + "label": "options", + "description": [], + "signature": [ + "string[]" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.languageId.defaultValue", + "type": "string", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.value", + "type": "Object", + "tags": [], + "label": "value", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.value.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.value.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.value.defaultValue", + "type": "string", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.arialabel", + "type": "Object", + "tags": [], + "label": "'aria-label'", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.arialabel.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.arialabel.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.arialabel.defaultValue", + "type": "string", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.allowFullScreen", + "type": "Object", + "tags": [], + "label": "allowFullScreen", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.allowFullScreen.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.allowFullScreen.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.allowFullScreen.defaultValue", + "type": "boolean", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.useDarkTheme", + "type": "Object", + "tags": [], + "label": "useDarkTheme", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.useDarkTheme.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.useDarkTheme.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.useDarkTheme.defaultValue", + "type": "boolean", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.transparentBackground", + "type": "Object", + "tags": [], + "label": "transparentBackground", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.transparentBackground.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.transparentBackground.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.transparentBackground.defaultValue", + "type": "boolean", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.placeholder", + "type": "Object", + "tags": [], + "label": "placeholder", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.placeholder.control", + "type": "Object", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.placeholder.control.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.propArguments.placeholder.defaultValue", + "type": "string", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.serviceArguments", + "type": "Object", + "tags": [], + "label": "serviceArguments", + "description": [], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.dependencies", + "type": "Array", + "tags": [], + "label": "dependencies", + "description": [], + "signature": [ + "never[]" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.getProps", + "type": "Function", + "tags": [], + "label": "getProps", + "description": [], + "signature": [ + "(params?: ", + { + "pluginId": "@kbn/code-editor-mocks", + "scope": "common", + "docId": "kibKbnCodeEditorMocksPluginApi", + "section": "def-common.Params", + "text": "Params" + }, + " | undefined) => ", + "Props" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.getProps.$1", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + { + "pluginId": "@kbn/code-editor-mocks", + "scope": "common", + "docId": "kibKbnCodeEditorMocksPluginApi", + "section": "def-common.Params", + "text": "Params" + }, + " | undefined" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.CodeEditorStorybookMock.getServices", + "type": "Function", + "tags": [], + "label": "getServices", + "description": [], + "signature": [ + "() => { width?: string | number | undefined; height?: string | number | undefined; languageId: enum; value: string; onChange?: ((value: string, event: ", + "editor", + ".IModelContentChangedEvent) => void) | undefined; options?: ", + "editor", + ".IStandaloneEditorConstructionOptions | undefined; suggestionProvider?: ", + "languages", + ".CompletionItemProvider | undefined; signatureProvider?: ", + "languages", + ".SignatureHelpProvider | undefined; hoverProvider?: ", + "languages", + ".HoverProvider | undefined; languageConfiguration?: ", + "languages", + ".LanguageConfiguration | undefined; editorWillMount?: (() => void) | undefined; overrideEditorWillMount?: (() => void) | undefined; editorDidMount?: ((editor: ", + "editor", + ".IStandaloneCodeEditor) => void) | undefined; useDarkTheme?: boolean | undefined; transparentBackground?: boolean | undefined; fullWidth?: boolean | undefined; placeholder?: string | undefined; 'aria-label'?: string | undefined; isCopyable?: boolean | undefined; allowFullScreen?: boolean | undefined; }" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/code-editor-mocks", + "id": "def-common.Params", + "type": "Type", + "tags": [], + "label": "Params", + "description": [], + "signature": [ + "{ value: any; placeholder: any; \"aria-label\": any; allowFullScreen: any; languageId: any; useDarkTheme: any; transparentBackground: any; }" + ], + "path": "packages/shared-ux/code_editor/mocks/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_code_editor_mocks.mdx b/api_docs/kbn_code_editor_mocks.mdx new file mode 100644 index 00000000000000..d2763366f0f019 --- /dev/null +++ b/api_docs/kbn_code_editor_mocks.mdx @@ -0,0 +1,33 @@ +--- +#### +#### 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: kibKbnCodeEditorMocksPluginApi +slug: /kibana-dev-docs/api/kbn-code-editor-mocks +title: "@kbn/code-editor-mocks" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/code-editor-mocks plugin +date: 2023-02-01 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mocks'] +--- +import kbnCodeEditorMocksObj from './kbn_code_editor_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 37 | 0 | 36 | 0 | + +## Common + +### Classes + + +### Consts, variables and types + + diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 069f23a0c68710..cf62d1fdd176cb 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: 2023-01-30 +date: 2023-02-01 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 444b276790315a..f02f6470e5293b 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: 2023-01-30 +date: 2023-02-01 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 4f30ccfb5a5eaa..77d8e550fb7639 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: 2023-01-30 +date: 2023-02-01 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 e1cf4c7c81a865..5311dcb05deb2c 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 90acfb8d3c42d5..65409b1480c711 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index 989bfddc5ad4c4..9d6182b5537674 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: 2023-01-30 +date: 2023-02-01 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 9207788b71dcb5..2beae539bfec73 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: 2023-01-30 +date: 2023-02-01 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 10ff4b00e813d1..f39ca428da8dac 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: 2023-01-30 +date: 2023-02-01 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 0a3ad2de810c4b..20790101022456 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: 2023-01-30 +date: 2023-02-01 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 5e25155fe99b3a..1283a3c3910a5c 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: 2023-01-30 +date: 2023-02-01 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 c735376e9bb3d6..6b6cf4ab4e3799 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: 2023-01-30 +date: 2023-02-01 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 fca2385c43a2fb..77f45608e9620c 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: 2023-01-30 +date: 2023-02-01 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 185c8a8c0d5dae..098b044bc5744f 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: 2023-01-30 +date: 2023-02-01 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 eca95e4f9bb4db..1fe0ab33b6825c 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: 2023-01-30 +date: 2023-02-01 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 e3685fc2066d88..e9975d7d0676e1 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: 2023-01-30 +date: 2023-02-01 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 65fd987d8573ef..cc4ed0cf981da3 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: 2023-01-30 +date: 2023-02-01 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 18e2f252c0c859..1aa8e70521510c 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: 2023-01-30 +date: 2023-02-01 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 6964da198d9a66..a4711f44713aab 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: 2023-01-30 +date: 2023-02-01 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_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 841808f6c5d2de..76589d63b7b9ed 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index c7c6e08deedc7e..ef2ab6b0d676c8 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: 2023-01-30 +date: 2023-02-01 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 53833a277ff3b6..a4340f8ace2ced 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: 2023-01-30 +date: 2023-02-01 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 4626b08cc07817..abe22997801083 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: 2023-01-30 +date: 2023-02-01 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 4e578e84321ddf..dbf572cd5938b1 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: 2023-01-30 +date: 2023-02-01 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 69bbcc7424ca2c..072b658f363fbe 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: 2023-01-30 +date: 2023-02-01 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 9f9920fbfcb44d..bff0d619657c54 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: 2023-01-30 +date: 2023-02-01 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 18f8e597892b94..f821fa0c13abdd 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: 2023-01-30 +date: 2023-02-01 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 22282d38e237a9..86fe91c2d17fae 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: 2023-01-30 +date: 2023-02-01 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 6851754193ed15..0a8bf1c86489ed 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: 2023-01-30 +date: 2023-02-01 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 4603c1b5db2c05..2c321aba988aca 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: 2023-01-30 +date: 2023-02-01 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 d66ae746b8dd1a..46a811ac6b85c9 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: 2023-01-30 +date: 2023-02-01 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_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index 4607944fe34f22..6de686cfa251e9 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index d36082c846d199..c93092cfea5469 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index 227fe7e79e6b04..502bcf575a946c 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index e8525bba73e6f9..6f76eb40edc511 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.devdocs.json b/api_docs/kbn_core_custom_branding_server.devdocs.json index bf777d23856054..ed18f14cb7ff5f 100644 --- a/api_docs/kbn_core_custom_branding_server.devdocs.json +++ b/api_docs/kbn_core_custom_branding_server.devdocs.json @@ -76,6 +76,88 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-custom-branding-server", + "id": "def-common.CustomBrandingSetup.getBrandingFor", + "type": "Function", + "tags": [], + "label": "getBrandingFor", + "description": [], + "signature": [ + "(request: ", + { + "pluginId": "@kbn/core-http-server", + "scope": "common", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-common.KibanaRequest", + "text": "KibanaRequest" + }, + ", options: { unauthenticated?: boolean | undefined; }) => Promise<", + { + "pluginId": "@kbn/core-custom-branding-common", + "scope": "common", + "docId": "kibKbnCoreCustomBrandingCommonPluginApi", + "section": "def-common.CustomBranding", + "text": "CustomBranding" + }, + ">" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-custom-branding-server", + "id": "def-common.CustomBrandingSetup.getBrandingFor.$1", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-server", + "scope": "common", + "docId": "kibKbnCoreHttpServerPluginApi", + "section": "def-common.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-custom-branding-server", + "id": "def-common.CustomBrandingSetup.getBrandingFor.$2", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-custom-branding-server", + "id": "def-common.CustomBrandingSetup.getBrandingFor.$2.unauthenticated", + "type": "CompoundType", + "tags": [], + "label": "unauthenticated", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] } ], "initialIsOpen": false @@ -112,7 +194,7 @@ "section": "def-common.KibanaRequest", "text": "KibanaRequest" }, - ") => ", + ", unauthenticated: boolean) => ", { "pluginId": "@kbn/utility-types", "scope": "common", @@ -155,6 +237,17 @@ "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-custom-branding-server", + "id": "def-common.CustomBrandingFetchFn.$2", + "type": "boolean", + "tags": [], + "label": "unauthenticated", + "description": [], + "path": "packages/core/custom-branding/core-custom-branding-server/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index f191340be9c139..039d92aab71213 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 6 | 0 | 6 | 0 | +| 11 | 0 | 11 | 0 | ## Common diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index b7c7ce11ed2c2c..bb1594a6822d5f 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index bcf53c6b63efc8..9b1287438d7285 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index a969e55e059697..0815d0825c3a9f 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: 2023-01-30 +date: 2023-02-01 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 74d1f269f1bc58..1de79e8e0917e8 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: 2023-01-30 +date: 2023-02-01 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 af85ababf4a9f2..10912630173e44 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: 2023-01-30 +date: 2023-02-01 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 f5f50113bb8303..00290f5415616d 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: 2023-01-30 +date: 2023-02-01 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 20535709f9bf46..0d66218afe4090 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: 2023-01-30 +date: 2023-02-01 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 e6f46741b98dba..0b64cf1c4d7afe 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: 2023-01-30 +date: 2023-02-01 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 411104ede9491a..2cf2e8e232c975 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: 2023-01-30 +date: 2023-02-01 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 2e0f829c6f5d48..5448f89edab09e 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: 2023-01-30 +date: 2023-02-01 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 d046b63b65748f..f414981ecbe071 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: 2023-01-30 +date: 2023-02-01 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 db61e55d08c73a..cbefeb615f12a9 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: 2023-01-30 +date: 2023-02-01 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 e0b8cf8c6275d2..f96ed485483a20 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: 2023-01-30 +date: 2023-02-01 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 f81a4b1dca0b23..f9170085bd0cd4 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: 2023-01-30 +date: 2023-02-01 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 878429f1f432ce..176413c48a8c71 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: 2023-01-30 +date: 2023-02-01 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 b74d61fd48bc0f..eea069dabba9d2 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: 2023-01-30 +date: 2023-02-01 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 a0f6c751f1322b..ce96fc2438d40e 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: 2023-01-30 +date: 2023-02-01 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 675d43d95945ca..99a9801ebe1051 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: 2023-01-30 +date: 2023-02-01 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 3468fa27b79676..7b5aa7edbd95ca 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: 2023-01-30 +date: 2023-02-01 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 9f58849fedb910..2cb9458d1d2e36 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: 2023-01-30 +date: 2023-02-01 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 2f5d1108069ed9..52c388cbb61c80 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: 2023-01-30 +date: 2023-02-01 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 ddbdafb1c895bb..c886f385d94168 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: 2023-01-30 +date: 2023-02-01 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 8e317783178fe6..ef7a19484cd565 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: 2023-01-30 +date: 2023-02-01 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 e18968f71d09c9..8e6e52b1de4535 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: 2023-01-30 +date: 2023-02-01 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 3bbc3dcb1ad3f1..ab96f02695b0be 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: 2023-01-30 +date: 2023-02-01 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 7bc1f355f3c503..6813312f4008db 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: 2023-01-30 +date: 2023-02-01 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 1412f197a11781..149a501ac005fc 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: 2023-01-30 +date: 2023-02-01 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 2f6dde2fb228d2..c9d26fb2b8bbe5 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: 2023-01-30 +date: 2023-02-01 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 acd6f5098cc1b6..72ae38011da7ea 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: 2023-01-30 +date: 2023-02-01 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 c27ee0968e3c6b..679c7f1a3f03b4 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: 2023-01-30 +date: 2023-02-01 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 0228a0b7f398cc..5526a243ae9efc 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: 2023-01-30 +date: 2023-02-01 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 cb8eb785650898..9bd8c24d66cd31 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: 2023-01-30 +date: 2023-02-01 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 e807dd4b2bf2e2..7b4d27ab0f5504 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: 2023-01-30 +date: 2023-02-01 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 4a3e1bb95abf04..ed788d9d0142f6 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: 2023-01-30 +date: 2023-02-01 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 ed4d689190141e..bbba6b2c0c77ad 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: 2023-01-30 +date: 2023-02-01 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 66cbcfc913e759..3bd91014589018 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: 2023-01-30 +date: 2023-02-01 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 16f80c2ef88c9f..bc74ce1ce9f150 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: 2023-01-30 +date: 2023-02-01 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 c76203c29fdd97..009de5c51edbbf 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: 2023-01-30 +date: 2023-02-01 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 de8f7fb32b44d5..7bee4ae5e46769 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: 2023-01-30 +date: 2023-02-01 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 eaacfeda3d0dc7..caac0f15b41fda 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: 2023-01-30 +date: 2023-02-01 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 3520dc1324a6e8..1833b29b44bf61 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: 2023-01-30 +date: 2023-02-01 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 6b7d2bcd26a8a2..617f3ee0384cfb 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: 2023-01-30 +date: 2023-02-01 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 2d88ab176bdcf0..51a88fbffcb664 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: 2023-01-30 +date: 2023-02-01 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 d6147f89f809ec..b3ac40cbcf54e3 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: 2023-01-30 +date: 2023-02-01 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 799798ef3955b4..c959ac88b00519 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: 2023-01-30 +date: 2023-02-01 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 2061333d751bab..47826f1992a760 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: 2023-01-30 +date: 2023-02-01 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 0fdb792899e32a..1109046184dba8 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: 2023-01-30 +date: 2023-02-01 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 84d49042270118..0c4cf114e1eb61 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: 2023-01-30 +date: 2023-02-01 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_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 742add69ed5a77..99fd90b828adb5 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: 2023-01-30 +date: 2023-02-01 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 1f47663a9540e0..89cd1ddaaec76e 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: 2023-01-30 +date: 2023-02-01 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 1dc629cf9d2e0e..6715e20c8b786e 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: 2023-01-30 +date: 2023-02-01 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.devdocs.json b/api_docs/kbn_core_lifecycle_browser.devdocs.json index 23c3e5ea7bcb41..d7b3a88c9551c4 100644 --- a/api_docs/kbn_core_lifecycle_browser.devdocs.json +++ b/api_docs/kbn_core_lifecycle_browser.devdocs.json @@ -803,27 +803,27 @@ }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/global_params/delete_param.tsx" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_selector/use_recently_viewed_monitors.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_locations_api.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" }, { "plugin": "synthetics", diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 859261d83c6797..5bd8b4fb8637df 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: 2023-01-30 +date: 2023-02-01 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 e7b57a81de800d..043b3a1a6bc2ae 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: 2023-01-30 +date: 2023-02-01 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_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index ac5ebf3e71e508..1e5240c6f60762 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index a3fed21b9979b3..89551ade8b6280 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 52ef44431a4d5b..e26b0c64c9a264 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index 23624669a3f8fc..b86d2d46a836be 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 2449965e824741..8a00f03dff58f0 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: 2023-01-30 +date: 2023-02-01 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 65ffa182d4220c..b24610b43af978 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: 2023-01-30 +date: 2023-02-01 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 a0df57e09c9796..debdb6573cf77b 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: 2023-01-30 +date: 2023-02-01 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 c67dd8b1dd1cad..4343bd48758057 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: 2023-01-30 +date: 2023-02-01 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 c8ef726cdeb7d0..4ff50e2d184c03 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: 2023-01-30 +date: 2023-02-01 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 832de383b526ef..c42993ddc7317a 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: 2023-01-30 +date: 2023-02-01 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 6ffc31d1fca270..b415539e7b8a09 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: 2023-01-30 +date: 2023-02-01 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 4596f78a805ca5..26083be71f436b 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: 2023-01-30 +date: 2023-02-01 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 069fcffdcb4540..8ecf2674465bd2 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: 2023-01-30 +date: 2023-02-01 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 a6bd44f60b7c84..f5d5ba2f3401ff 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: 2023-01-30 +date: 2023-02-01 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 3b1e9026a48b99..f184df2cf48b2a 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: 2023-01-30 +date: 2023-02-01 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 fc77245dc3cf34..f5ed618d664575 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: 2023-01-30 +date: 2023-02-01 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 4600e9d212c849..fe505e2a95f96d 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: 2023-01-30 +date: 2023-02-01 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 49d93560e30796..5e7088bec3e024 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: 2023-01-30 +date: 2023-02-01 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 fb4b14a8a85428..92db82741b2e6e 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: 2023-01-30 +date: 2023-02-01 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 3fdf81609d4d28..f08b3ac8f238e8 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: 2023-01-30 +date: 2023-02-01 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 5c6dbc9a9ccb90..8eec5b674bec56 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: 2023-01-30 +date: 2023-02-01 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 008e70efbdf434..19083e19794fdc 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: 2023-01-30 +date: 2023-02-01 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 f76760c79e8c4e..6a0378f61fd29a 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: 2023-01-30 +date: 2023-02-01 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 7fa45105d44160..1565e4acf57aa6 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: 2023-01-30 +date: 2023-02-01 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_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 2fa60c13044277..07bd1183cf0b8c 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index 6ab1c7d6d41e28..05dd9332793d29 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 915d1bee315957..8050722d6309b3 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: 2023-01-30 +date: 2023-02-01 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 9196688eec7e79..196285a03a1cbf 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: 2023-01-30 +date: 2023-02-01 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 300ff8710b6d6f..12695d092963ee 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: 2023-01-30 +date: 2023-02-01 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 f8a1aeaed572f6..3e95fac0ac13e4 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: 2023-01-30 +date: 2023-02-01 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 b3dec96995eec0..9987be67b4db94 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: 2023-01-30 +date: 2023-02-01 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_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 50e74752502a2b..effbbc3d521aa5 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.devdocs.json b/api_docs/kbn_core_saved_objects_api_browser.devdocs.json index 27cd4d841d19d7..cf0e48e2ee8845 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.devdocs.json +++ b/api_docs/kbn_core_saved_objects_api_browser.devdocs.json @@ -2305,15 +2305,15 @@ }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/hooks/use_location_monitors.ts" }, { "plugin": "synthetics", - "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts" + "path": "x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_name.ts" }, { "plugin": "graph", diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 387bf244f3e842..a6edaff7e5b526 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: 2023-01-30 +date: 2023-02-01 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 7b51ae2386e4f8..8c03cc1a94d12a 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: 2023-01-30 +date: 2023-02-01 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 732c0f388c4924..5d588bdb8555a5 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: 2023-01-30 +date: 2023-02-01 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 4dbed3adb50a80..e6ad91427b5d96 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: 2023-01-30 +date: 2023-02-01 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 168a6de3884bf8..65b999860fb93f 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: 2023-01-30 +date: 2023-02-01 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 5ed0f53c385409..0ebbe272cdce8e 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: 2023-01-30 +date: 2023-02-01 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 3d3d8c73724070..23c26af387456c 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: 2023-01-30 +date: 2023-02-01 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 66402b1e339779..eaf9d5e5c98ee6 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: 2023-01-30 +date: 2023-02-01 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 05461ace74602e..873ca1eddb1e23 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: 2023-01-30 +date: 2023-02-01 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.devdocs.json b/api_docs/kbn_core_saved_objects_common.devdocs.json index 0b1701f00d67d1..35b92637b91909 100644 --- a/api_docs/kbn_core_saved_objects_common.devdocs.json +++ b/api_docs/kbn_core_saved_objects_common.devdocs.json @@ -1564,6 +1564,14 @@ "plugin": "cases", "path": "x-pack/plugins/cases/server/services/so_references.ts" }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, + { + "plugin": "cases", + "path": "x-pack/plugins/cases/server/services/so_references.ts" + }, { "plugin": "lists", "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts" @@ -3289,6 +3297,18 @@ "plugin": "lens", "path": "x-pack/plugins/lens/public/app_plugin/share_action.ts" }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.ts" + }, { "plugin": "infra", "path": "x-pack/plugins/infra/public/common/visualizations/lens/types.ts" @@ -3360,6 +3380,14 @@ { "plugin": "cases", "path": "x-pack/plugins/cases/server/services/user_actions/test_utils.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.test.ts" + }, + { + "plugin": "infra", + "path": "x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_references_manager.test.ts" } ], "initialIsOpen": false diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 12c46e4a55d1fe..642ed5d9f5669e 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: 2023-01-30 +date: 2023-02-01 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 2df56466d7b396..b641c0e5f2a900 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: 2023-01-30 +date: 2023-02-01 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 3e322719e6c3f6..a311e283959222 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: 2023-01-30 +date: 2023-02-01 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 0c78327852c142..61b3107ed2e385 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: 2023-01-30 +date: 2023-02-01 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 e7c2e6d106e8b6..13aac634a0dbe4 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: 2023-01-30 +date: 2023-02-01 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 4fccae05fcfb99..06ed0898c9e3af 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: 2023-01-30 +date: 2023-02-01 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 3968ab5e944ea7..5c4cedca39cd47 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: 2023-01-30 +date: 2023-02-01 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 5c8209b1dd1601..7cdcfb95a84ae4 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: 2023-01-30 +date: 2023-02-01 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 a28d111a550b20..3dffe1a065d049 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: 2023-01-30 +date: 2023-02-01 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 2bab6b5ae95353..8ed3896dae54d8 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: 2023-01-30 +date: 2023-02-01 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 357fa837304358..1727f27bd6d69e 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: 2023-01-30 +date: 2023-02-01 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 8dc92118c079f8..afd9f57ffe411e 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: 2023-01-30 +date: 2023-02-01 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 f8c6c61f848ccb..b12ab118289ed2 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: 2023-01-30 +date: 2023-02-01 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 8f5a6fbadf09b5..2caed0589d6b6c 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: 2023-01-30 +date: 2023-02-01 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 390f909ce2513c..5d4930f4f8a0e1 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: 2023-01-30 +date: 2023-02-01 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 ca0f19aa71b3ad..f6637682ead744 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: 2023-01-30 +date: 2023-02-01 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_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index de94f700b713fe..e7760ed2dbeeb7 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.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 815aa3601ead06..2ed6e76198a279 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: 2023-01-30 +date: 2023-02-01 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_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 3c2f405761f698..01e170178bfc7d 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index df472a1696b825..afbf87e89087f3 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: 2023-01-30 +date: 2023-02-01 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 eee37891451519..deaaf3ab690c93 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: 2023-01-30 +date: 2023-02-01 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 105503f3e6a6c3..b9cba735ac7442 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: 2023-01-30 +date: 2023-02-01 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 4457d3c3e80d2b..fe44d2b07fadfe 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: 2023-01-30 +date: 2023-02-01 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 e621db85bb8e1b..c211345676ac2d 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: 2023-01-30 +date: 2023-02-01 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 35c0617de2b6f7..7b608f7b644fb0 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: 2023-01-30 +date: 2023-02-01 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 dc1710d40a9275..7f975e31e5b545 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: 2023-01-30 +date: 2023-02-01 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 50d479a7042901..b98debb2b85f3d 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: 2023-01-30 +date: 2023-02-01 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 9b4f92f88656a5..4d4474a404c936 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: 2023-01-30 +date: 2023-02-01 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 655d36907edc1e..20a6052320b7ca 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: 2023-01-30 +date: 2023-02-01 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 721850f712396e..d9acef7aa740c8 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: 2023-01-30 +date: 2023-02-01 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 d94d8f39d44d2e..bbc68824fcf529 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: 2023-01-30 +date: 2023-02-01 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 fe32fe6d09742d..b3fb6316a7eec6 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: 2023-01-30 +date: 2023-02-01 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 f6f73955e945d8..7591e1167b2f18 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: 2023-01-30 +date: 2023-02-01 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 aa57bcafa5d950..0a95700eb48bf9 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index 31aa95ab485f3e..d56fe5d87d0464 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 5c7513c4776efe..39f76e4addcaad 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: 2023-01-30 +date: 2023-02-01 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 20efd16d9c4d4b..18d5de0d711fd0 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: 2023-01-30 +date: 2023-02-01 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 93a3bdf8fb6990..fd0812f80843f0 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: 2023-01-30 +date: 2023-02-01 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 d2574656ee11c0..c1862d838ece91 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: 2023-01-30 +date: 2023-02-01 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 b027188acb6d69..b7cc038b527773 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index 02ef2e3f1f1492..95bd54c7055c26 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -840,7 +840,7 @@ "label": "fleet", "description": [], "signature": [ - "{ readonly beatsAgentComparison: string; readonly guide: string; readonly fleetServer: string; readonly fleetServerAddFleetServer: string; readonly settings: string; readonly settingsFleetServerHostSettings: string; readonly settingsFleetServerProxySettings: string; readonly troubleshooting: string; readonly elasticAgent: string; readonly datastreams: string; readonly datastreamsILM: string; readonly datastreamsNamingScheme: string; readonly installElasticAgent: string; readonly installElasticAgentStandalone: string; readonly packageSignatures: string; readonly upgradeElasticAgent: string; readonly learnMoreBlog: string; readonly apiKeysLearnMore: string; readonly onPremRegistry: string; readonly secureLogstash: string; readonly agentPolicy: string; }" + "{ readonly beatsAgentComparison: string; readonly guide: string; readonly fleetServer: string; readonly fleetServerAddFleetServer: string; readonly settings: string; readonly settingsFleetServerHostSettings: string; readonly settingsFleetServerProxySettings: string; readonly troubleshooting: string; readonly elasticAgent: string; readonly datastreams: string; readonly datastreamsILM: string; readonly datastreamsNamingScheme: string; readonly datastreamsManualRollover: string; readonly installElasticAgent: string; readonly installElasticAgentStandalone: string; readonly packageSignatures: string; readonly upgradeElasticAgent: string; readonly learnMoreBlog: string; readonly apiKeysLearnMore: string; readonly onPremRegistry: string; readonly secureLogstash: string; readonly agentPolicy: string; }" ], "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index a9529773dfc77e..e3635341290447 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: 2023-01-30 +date: 2023-02-01 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 d809cce3b62744..f8ffc55cb1ff35 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: 2023-01-30 +date: 2023-02-01 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 f6524ce9c0971c..e41b559cb454e9 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index 84af0a7b98494a..df1510473c72dc 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 360f4b68d6d87a..1e38d7a4f03612 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 714d4f044fe8c6..7a4bed39fffa6e 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: 2023-01-30 +date: 2023-02-01 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 3c3a74d96af979..3ee87cadca56f5 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: 2023-01-30 +date: 2023-02-01 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 da781ad196acd5..d469864ce6eeec 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: 2023-01-30 +date: 2023-02-01 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 5c495dd6496582..8572b03b09bc49 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: 2023-01-30 +date: 2023-02-01 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 7c430079479503..f4a9ee3a1ba091 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: 2023-01-30 +date: 2023-02-01 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 159dc92930b437..e81cde7c6cb0c7 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: 2023-01-30 +date: 2023-02-01 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 d3c1d8f62910f1..046beb59d02050 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: 2023-01-30 +date: 2023-02-01 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 b91ea5b281d51b..0937e3bc242c40 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: 2023-01-30 +date: 2023-02-01 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 413dd63d35735f..efa0e41495a923 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: 2023-01-30 +date: 2023-02-01 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 64a9c1e5b7bf26..495408a2de7e1c 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/get-repo-files'] --- import kbnGetRepoFilesObj from './kbn_get_repo_files.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.devdocs.json b/api_docs/kbn_guided_onboarding.devdocs.json index 09b27f31eede78..246ef8f89dd34d 100644 --- a/api_docs/kbn_guided_onboarding.devdocs.json +++ b/api_docs/kbn_guided_onboarding.devdocs.json @@ -21,31 +21,31 @@ "functions": [ { "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.GuideCard", + "id": "def-common.GuideCards", "type": "Function", "tags": [], - "label": "GuideCard", + "label": "GuideCards", "description": [], "signature": [ - "({ useCase, guides, activateGuide, isDarkTheme, addBasePath, }: ", - "GuideCardProps", + "(props: ", + "GuideCardsProps", ") => JSX.Element" ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_card.tsx", + "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_cards.tsx", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.GuideCard.$1", + "id": "def-common.GuideCards.$1", "type": "Object", "tags": [], - "label": "{\n useCase,\n guides,\n activateGuide,\n isDarkTheme,\n addBasePath,\n}", + "label": "props", "description": [], "signature": [ - "GuideCardProps" + "GuideCardsProps" ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_card.tsx", + "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_cards.tsx", "deprecated": false, "trackAdoption": false, "isRequired": true @@ -56,143 +56,32 @@ }, { "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard", + "id": "def-common.GuideFilters", "type": "Function", "tags": [], - "label": "InfrastructureLinkCard", + "label": "GuideFilters", "description": [], "signature": [ - "({ navigateToApp, isDarkTheme, addBasePath, }: { navigateToApp: (appId: string, options?: ", - { - "pluginId": "@kbn/core-application-browser", - "scope": "common", - "docId": "kibKbnCoreApplicationBrowserPluginApi", - "section": "def-common.NavigateToAppOptions", - "text": "NavigateToAppOptions" - }, - " | undefined) => Promise; isDarkTheme: boolean; addBasePath: (url: string) => string; }) => JSX.Element" + "({ activeFilter, setActiveFilter }: GuideFiltersProps) => JSX.Element" ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", + "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1", + "id": "def-common.GuideFilters.$1", "type": "Object", "tags": [], - "label": "{\n navigateToApp,\n isDarkTheme,\n addBasePath,\n}", + "label": "{ activeFilter, setActiveFilter }", "description": [], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", + "signature": [ + "GuideFiltersProps" + ], + "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1.navigateToApp", - "type": "Function", - "tags": [], - "label": "navigateToApp", - "description": [], - "signature": [ - "(appId: string, options?: ", - { - "pluginId": "@kbn/core-application-browser", - "scope": "common", - "docId": "kibKbnCoreApplicationBrowserPluginApi", - "section": "def-common.NavigateToAppOptions", - "text": "NavigateToAppOptions" - }, - " | undefined) => Promise" - ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1.navigateToApp.$1", - "type": "string", - "tags": [], - "label": "appId", - "description": [], - "signature": [ - "string" - ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1.navigateToApp.$2", - "type": "Object", - "tags": [], - "label": "options", - "description": [], - "signature": [ - { - "pluginId": "@kbn/core-application-browser", - "scope": "common", - "docId": "kibKbnCoreApplicationBrowserPluginApi", - "section": "def-common.NavigateToAppOptions", - "text": "NavigateToAppOptions" - }, - " | undefined" - ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [] - }, - { - "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1.isDarkTheme", - "type": "boolean", - "tags": [], - "label": "isDarkTheme", - "description": [], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1.addBasePath", - "type": "Function", - "tags": [], - "label": "addBasePath", - "description": [], - "signature": [ - "(url: string) => string" - ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.InfrastructureLinkCard.$1.addBasePath.$1", - "type": "string", - "tags": [], - "label": "url", - "description": [], - "signature": [ - "string" - ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/infrastructure_link_card.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - } - ] + "isRequired": true } ], "returnComment": [], @@ -638,15 +527,16 @@ "misc": [ { "parentPluginId": "@kbn/guided-onboarding", - "id": "def-common.GuideCardUseCase", + "id": "def-common.GuideFilterValues", "type": "Type", "tags": [], - "label": "GuideCardUseCase", + "label": "GuideFilterValues", "description": [], "signature": [ - "\"search\" | \"kubernetes\" | \"siem\"" + "\"all\" | ", + "GuideCardSolutions" ], - "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_card.tsx", + "path": "packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx", "deprecated": false, "trackAdoption": false, "initialIsOpen": false diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index e078a0ec242850..cf07b2c8de710c 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 58 | 0 | 56 | 1 | +| 52 | 0 | 50 | 2 | ## Common diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 126aba552fcff5..b4e2b37bdcee5e 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: 2023-01-30 +date: 2023-02-01 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 6adb4eb34c18ef..b0b1612d91570f 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 2e095a65db2927..895d598ea4fe5d 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 2b295a805e4322..4e710b3dab4f9b 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: 2023-01-30 +date: 2023-02-01 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 c397501db921fc..edbbf4e8169da2 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: 2023-01-30 +date: 2023-02-01 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 2a49529919548a..abc1ac2d7e6a88 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 5fcd0953e989dc..b5493cc1b38968 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 2c7b594374be42..c05e243c2af8ea 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: 2023-01-30 +date: 2023-02-01 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 b578d6f0ff054b..43de89d4d2e6e0 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: 2023-01-30 +date: 2023-02-01 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 a5fd9cbf4e7ddd..d5ed1281c13da1 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: 2023-01-30 +date: 2023-02-01 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 11853327bf2391..bcee792974fe79 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: 2023-01-30 +date: 2023-02-01 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 11b23f85db0ea2..ce7ab220e8464b 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index 3927f612978154..754afcc9fc8311 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.devdocs.json b/api_docs/kbn_kibana_manifest_schema.devdocs.json index fb46dedb5099d9..e9509c172d6e81 100644 --- a/api_docs/kbn_kibana_manifest_schema.devdocs.json +++ b/api_docs/kbn_kibana_manifest_schema.devdocs.json @@ -1034,6 +1034,269 @@ "trackAdoption": false } ] + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build", + "type": "Object", + "tags": [], + "label": "build", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties", + "type": "Object", + "tags": [], + "label": "properties", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.extraExcludes", + "type": "Object", + "tags": [], + "label": "extraExcludes", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.extraExcludes.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.extraExcludes.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.extraExcludes.items", + "type": "Object", + "tags": [], + "label": "items", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.extraExcludes.items.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ] + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.noParse", + "type": "Object", + "tags": [], + "label": "noParse", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.noParse.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.noParse.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.noParse.items", + "type": "Object", + "tags": [], + "label": "items", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.build.properties.noParse.items.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ] + } + ] + } + ] + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.serviceFolders", + "type": "Object", + "tags": [], + "label": "serviceFolders", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.serviceFolders.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.serviceFolders.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.serviceFolders.items", + "type": "Object", + "tags": [], + "label": "items", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.serviceFolders.items.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.serviceFolders.deprecated", + "type": "boolean", + "tags": [], + "label": "deprecated", + "description": [], + "signature": [ + "true" + ], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.description", + "type": "Object", + "tags": [], + "label": "description", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.description.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/kibana-manifest-schema", + "id": "def-common.MANIFEST_V2.properties.description.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", + "deprecated": false, + "trackAdoption": false + } + ] } ] }, @@ -1045,7 +1308,7 @@ "label": "oneOf", "description": [], "signature": [ - "({ type: string; properties: { type: { enum: string[]; }; plugin: { type: string; required: string[]; properties: { id: { type: string; pattern: string; }; configPath: { description: string; type: string; items: { type: string; pattern: string; }; }; requiredPlugins: { type: string; items: { type: string; pattern: string; }; }; optionalPlugins: { type: string; items: { type: string; pattern: string; }; }; description: { description: string; type: string; }; enabledOnAnonymousPages: { description: string; type: string; }; serviceFolders: { description: string; type: string; items: { type: string; }; }; }; }; }; } | { type: string; properties: { type: { const: string; }; sharedBrowserBundle: { type: string; description: string; }; }; } | { type: string; properties: { type: { enum: string[]; }; }; })[]" + "({ type: string; required: string[]; properties: { type: { const: string; }; plugin: { type: string; required: string[]; properties: { id: { type: string; pattern: string; }; configPath: { description: string; oneOf: ({ type: string; items: { type: string; }; } | { type: string; })[]; }; requiredPlugins: { type: string; items: { type: string; pattern: string; }; }; optionalPlugins: { type: string; items: { type: string; pattern: string; }; }; enabledOnAnonymousPages: { description: string; type: string; }; type: { description: string; enum: string[]; }; browser: { type: string; description: string; }; server: { type: string; description: string; }; }; }; }; } | { type: string; properties: { type: { const: string; }; sharedBrowserBundle: { type: string; description: string; }; }; } | { type: string; properties: { type: { enum: string[]; }; }; })[]" ], "path": "packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts", "deprecated": false, diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 3e923c48e785f7..bda910dd759c71 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 86 | 0 | 85 | 0 | +| 108 | 0 | 107 | 0 | ## Common diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index ec6755a037e54c..b7a9027702ecd7 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 2884e1a95e53e7..46ffc65b380563 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: 2023-01-30 +date: 2023-02-01 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 1f5e5a603529ac..bc8d8d9b625dc4 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: 2023-01-30 +date: 2023-02-01 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 e4fa5b1fc44065..f14bc4ece14571 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: 2023-01-30 +date: 2023-02-01 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 9073884e3e9060..927425c96d2984 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: 2023-01-30 +date: 2023-02-01 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 50230f0c37c4e3..bacbe676b54d40 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: 2023-01-30 +date: 2023-02-01 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_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index e0a807ae7fcfc8..5ecb21bf538527 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index 8663b0973a4faf..f52e97ed37226d 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index bd86f138c7f45e..09bcf080430d0a 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: 2023-01-30 +date: 2023-02-01 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_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index e8945cdbae8528..2fe324778985d1 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index 32b05613d68625..669cdaaff96459 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index 19e67e69be75c5..dae7c87a336f8c 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index 726577ba097d1c..83189bab3740d4 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index a5b520a3593663..0ab1f83f644970 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 9b21b62a02005b..95d746e2d8a841 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: 2023-01-30 +date: 2023-02-01 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 c1c4409f67b830..dc5b6494ced155 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: 2023-01-30 +date: 2023-02-01 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 b6f86dc0cd4fa3..dc84080dfbf45a 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: 2023-01-30 +date: 2023-02-01 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 0229ccceff20ce..a4e4d010108710 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: 2023-01-30 +date: 2023-02-01 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 1ac67e0201af66..37ae702f55a2ed 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: 2023-01-30 +date: 2023-02-01 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 182b3d18d13173..81f20bd4881b3d 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: 2023-01-30 +date: 2023-02-01 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 4a2f7a15483900..2e0ec4d77093ba 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: 2023-01-30 +date: 2023-02-01 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 bcc9621f6eaf50..22e0f619f59c08 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index a8aa63023c828f..832404df2d18c5 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index 323e39b6192ffb..258a9f199552d8 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index eae842c4320389..98787ab292c774 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index 4dd0be8388470d..b49a61b295ca2d 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index 977aecbf64ae48..394f805e6cb623 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.devdocs.json b/api_docs/kbn_rule_data_utils.devdocs.json index 41036062e55907..1b93a3e0236c89 100644 --- a/api_docs/kbn_rule_data_utils.devdocs.json +++ b/api_docs/kbn_rule_data_utils.devdocs.json @@ -328,6 +328,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/rule-data-utils", + "id": "def-common.ALERT_LAST_DETECTED", + "type": "string", + "tags": [], + "label": "ALERT_LAST_DETECTED", + "description": [], + "signature": [ + "\"kibana.alert.last_detected\"" + ], + "path": "packages/kbn-rule-data-utils/src/default_alerts_as_data.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/rule-data-utils", "id": "def-common.ALERT_NAMESPACE", @@ -1296,7 +1311,7 @@ "label": "DefaultAlertFieldName", "description": [], "signature": [ - "\"kibana\" | \"kibana.alert.rule.rule_type_id\" | \"kibana.alert.rule.consumer\" | \"kibana.alert.rule.execution.uuid\" | \"kibana.alert\" | \"kibana.alert.rule\" | \"kibana.alert.rule.parameters\" | \"kibana.alert.rule.producer\" | \"kibana.space_ids\" | \"kibana.alert.uuid\" | \"kibana.alert.start\" | \"kibana.alert.time_range\" | \"kibana.alert.end\" | \"kibana.alert.duration.us\" | \"kibana.alert.status\" | \"kibana.alert.flapping\" | \"kibana.version\" | \"kibana.alert.workflow_status\" | \"kibana.alert.action_group\" | \"kibana.alert.reason\" | \"kibana.alert.rule.category\" | \"kibana.alert.rule.uuid\" | \"kibana.alert.rule.name\" | \"kibana.alert.rule.tags\" | \"kibana.alert.id\"" + "\"kibana\" | \"kibana.alert.rule.rule_type_id\" | \"kibana.alert.rule.consumer\" | \"kibana.alert.rule.execution.uuid\" | \"kibana.alert\" | \"kibana.alert.rule\" | \"kibana.alert.rule.parameters\" | \"kibana.alert.rule.producer\" | \"kibana.space_ids\" | \"kibana.alert.uuid\" | \"kibana.alert.start\" | \"kibana.alert.time_range\" | \"kibana.alert.end\" | \"kibana.alert.duration.us\" | \"kibana.alert.status\" | \"kibana.alert.flapping\" | \"kibana.version\" | \"kibana.alert.workflow_status\" | \"kibana.alert.action_group\" | \"kibana.alert.reason\" | \"kibana.alert.rule.category\" | \"kibana.alert.rule.uuid\" | \"kibana.alert.rule.name\" | \"kibana.alert.rule.tags\" | \"kibana.alert.last_detected\" | \"kibana.alert.id\"" ], "path": "packages/kbn-rule-data-utils/src/default_alerts_as_data.ts", "deprecated": false, diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index aaae88b9fcbb1e..d2a1c175ac15b3 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 99 | 0 | 96 | 0 | +| 100 | 0 | 97 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 9d59e07d5c1044..fd1cdb5c4d7e89 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index 4dd583520714eb..a006a87f124eec 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index dd045c36fcb757..44d76174ea74b4 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: 2023-01-30 +date: 2023-02-01 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 0d5b772d8a0dd7..47e63f79951703 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: 2023-01-30 +date: 2023-02-01 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 1ae1b5f1ccda97..e7e48598c9bf89 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: 2023-01-30 +date: 2023-02-01 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 bbe20a810001cb..9eee5fdf0e6092 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: 2023-01-30 +date: 2023-02-01 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 7d5c918352fe66..b1899b9468872e 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: 2023-01-30 +date: 2023-02-01 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 df64ef56e48f6d..f514e8f37a65c5 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: 2023-01-30 +date: 2023-02-01 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 515fbef38847ac..ab436077e4f77f 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: 2023-01-30 +date: 2023-02-01 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 2bb6eec809af18..907e0343d6ecd9 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: 2023-01-30 +date: 2023-02-01 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 69ccb1593f4d86..c417878f856bcb 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: 2023-01-30 +date: 2023-02-01 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 f85008faeacc64..11007f3ef82bc3 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: 2023-01-30 +date: 2023-02-01 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 0e2bbcefdc4d86..74cba902b01e53 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: 2023-01-30 +date: 2023-02-01 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 3ba4c88efcf2e2..e61f99c7727d5e 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: 2023-01-30 +date: 2023-02-01 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 b3be901ccd1308..186208539f0e1b 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: 2023-01-30 +date: 2023-02-01 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 1354047ee3695f..b0438ee5ca7044 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: 2023-01-30 +date: 2023-02-01 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 576d0e5930ffd1..1d38ea6f878aad 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: 2023-01-30 +date: 2023-02-01 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 3db2e619c3bade..7536ba82d3aad7 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: 2023-01-30 +date: 2023-02-01 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 0ddbad2831322a..3487a0a506f86a 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: 2023-01-30 +date: 2023-02-01 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_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 613403ecdf0682..152766810636e6 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.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 129b0d1f90c20a..6de8f422aa0335 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: 2023-01-30 +date: 2023-02-01 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.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index 0ddb60c7d66113..8942ece76a75ee 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.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 a1ae33e86b3f99..dbd3e398962e8d 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: 2023-01-30 +date: 2023-02-01 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 64cc68ecff23d6..d380c77cce04b1 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: 2023-01-30 +date: 2023-02-01 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 30dd624ba08892..7fb95d070966fb 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: 2023-01-30 +date: 2023-02-01 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 4c8a14cb5301d3..9a7e46e6939468 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: 2023-01-30 +date: 2023-02-01 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_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 1f27f197f6aa7c..d0b2fc0a9ceed4 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index d6fa862f09ead6..e1fd6be1eaa2ff 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index 36d55cb5d5ca2d..a72f5a93df3352 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 6b94f4f24bbca6..baad7c1de6f71a 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index 060c2573ec94b1..e449c2a4856edd 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index bb9b58d2daf61d..8ae88cf8710cc7 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 663368f7eff440..5e7c20d7324a8f 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index 729eaf3eaaa432..6d3d52245e244e 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.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 837d0a3e579df9..167b29b57ad0ce 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: 2023-01-30 +date: 2023-02-01 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_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index 03ee5c2618af18..c65c446c3147be 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index cd35f7e5cf03d3..9b8fe08216386e 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.devdocs.json b/api_docs/kbn_shared_ux_page_analytics_no_data.devdocs.json index 82ac24347e0541..217e49d5572e4d 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.devdocs.json +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.devdocs.json @@ -66,7 +66,7 @@ "\nA pure component of an entire page that can be displayed when Kibana \"has no data\", specifically for Analytics." ], "signature": [ - "({ kibanaGuideDocLink, onDataViewCreated, allowAdHocDataView, }: ", + "({ kibanaGuideDocLink, onDataViewCreated, allowAdHocDataView, showPlainSpinner, }: ", "Props", ") => JSX.Element" ], @@ -79,7 +79,7 @@ "id": "def-common.AnalyticsNoDataPage.$1", "type": "Object", "tags": [], - "label": "{\n kibanaGuideDocLink,\n onDataViewCreated,\n allowAdHocDataView,\n}", + "label": "{\n kibanaGuideDocLink,\n onDataViewCreated,\n allowAdHocDataView,\n showPlainSpinner,\n}", "description": [], "signature": [ "Props" 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 dac874e6216916..76d2cfede6e3d4 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: 2023-01-30 +date: 2023-02-01 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.devdocs.json b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json index ee58f4094a5e35..7bd3c319b4cfd8 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json @@ -108,6 +108,54 @@ "trackAdoption": false } ] + }, + { + "parentPluginId": "@kbn/shared-ux-page-analytics-no-data-mocks", + "id": "def-common.StorybookMock.serviceArguments.customBranding", + "type": "Object", + "tags": [], + "label": "customBranding", + "description": [], + "path": "packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-page-analytics-no-data-mocks", + "id": "def-common.StorybookMock.serviceArguments.customBranding.hasCustomBranding$", + "type": "Object", + "tags": [], + "label": "hasCustomBranding$", + "description": [], + "path": "packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-page-analytics-no-data-mocks", + "id": "def-common.StorybookMock.serviceArguments.customBranding.hasCustomBranding$.control", + "type": "string", + "tags": [], + "label": "control", + "description": [], + "path": "packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/shared-ux-page-analytics-no-data-mocks", + "id": "def-common.StorybookMock.serviceArguments.customBranding.hasCustomBranding$.defaultValue", + "type": "boolean", + "tags": [], + "label": "defaultValue", + "description": [], + "path": "packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ] } ] }, @@ -219,6 +267,24 @@ "children": [], "returnComment": [], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/shared-ux-page-analytics-no-data-mocks", + "id": "def-common.getServicesMockCustomBranding", + "type": "Function", + "tags": [], + "label": "getServicesMockCustomBranding", + "description": [], + "signature": [ + "() => ", + "AnalyticsNoDataPageServices" + ], + "path": "packages/shared-ux/page/analytics_no_data/mocks/src/jest.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false } ], "interfaces": [], 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 246359c3f16711..9852fad0567f07 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: 2023-01-30 +date: 2023-02-01 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'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 12 | 0 | 12 | 0 | +| 17 | 0 | 17 | 0 | ## Common diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.devdocs.json b/api_docs/kbn_shared_ux_page_kibana_no_data.devdocs.json index e313723153e65b..0076a7f37dc809 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.devdocs.json +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.devdocs.json @@ -29,7 +29,7 @@ "\nA page to display when Kibana has no data, prompting a person to add integrations or create a new data view." ], "signature": [ - "({ onDataViewCreated, noDataConfig, allowAdHocDataView, }: ", + "({ onDataViewCreated, noDataConfig, allowAdHocDataView, showPlainSpinner, }: ", "KibanaNoDataPageProps", ") => JSX.Element | null" ], @@ -42,7 +42,7 @@ "id": "def-common.KibanaNoDataPage.$1", "type": "Object", "tags": [], - "label": "{\n onDataViewCreated,\n noDataConfig,\n allowAdHocDataView,\n}", + "label": "{\n onDataViewCreated,\n noDataConfig,\n allowAdHocDataView,\n showPlainSpinner,\n}", "description": [], "signature": [ "KibanaNoDataPageProps" 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 b9c8d7b042daca..55c1ac09093151 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: 2023-01-30 +date: 2023-02-01 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.devdocs.json b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json index 6baad57602555c..2134860750376b 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json @@ -293,7 +293,7 @@ "section": "def-common.Params", "text": "Params" }, - ") => { noDataConfig: { solution: any; logo: any; action: { elasticAgent: { title: string; }; }; docsLink: string; }; onDataViewCreated: ", + ") => { showPlainSpinner: boolean; noDataConfig: { solution: any; logo: any; action: { elasticAgent: { title: string; }; }; docsLink: string; }; onDataViewCreated: ", "HandlerFunction", "; }" ], 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 4ab936ba53775d..0c1e7b9e706a95 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: 2023-01-30 +date: 2023-02-01 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 d67cbc6398ee7e..9cc367f56e98ed 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: 2023-01-30 +date: 2023-02-01 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 0295d42172b33b..01a5fc9ee832d1 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: 2023-01-30 +date: 2023-02-01 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 6e7c46200490d7..8d45bf81844d66 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: 2023-01-30 +date: 2023-02-01 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 5306cc61ad8236..4df85a30b0d54e 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: 2023-01-30 +date: 2023-02-01 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 953a303571a2ec..5238d26b1ad74d 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: 2023-01-30 +date: 2023-02-01 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 c975b40c2d3c03..b6dbf00781ed33 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: 2023-01-30 +date: 2023-02-01 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 ce5590d9103d9d..ddc86afca0ffd0 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: 2023-01-30 +date: 2023-02-01 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 66bdc9c0ac3f56..d23ed78e678987 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: 2023-01-30 +date: 2023-02-01 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 fea5b27e93efea..76561c82a3f306 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: 2023-01-30 +date: 2023-02-01 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_prompt_not_found.devdocs.json b/api_docs/kbn_shared_ux_prompt_not_found.devdocs.json index 99a5e86068dfbb..8b804e94e8b1d4 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.devdocs.json +++ b/api_docs/kbn_shared_ux_prompt_not_found.devdocs.json @@ -29,7 +29,7 @@ "\nPredefined `EuiEmptyPrompt` for 404 pages." ], "signature": [ - "({ actions }: NotFoundProps) => JSX.Element" + "({ actions, title, body }: NotFoundProps) => JSX.Element" ], "path": "packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx", "deprecated": false, @@ -40,7 +40,7 @@ "id": "def-common.NotFoundPrompt.$1", "type": "Object", "tags": [], - "label": "{ actions }", + "label": "{ actions, title, body }", "description": [], "signature": [ "NotFoundProps" diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index 0d60150cd58b88..e160301c5ecc04 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index e6ce595ac568ea..a8c45986c6704a 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: 2023-01-30 +date: 2023-02-01 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 920afbc20a329d..7ec14b31d171f6 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: 2023-01-30 +date: 2023-02-01 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 5baf421f29c629..c97b2c79c32458 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: 2023-01-30 +date: 2023-02-01 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 5c97689451e8b7..d8885d1f869e81 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: 2023-01-30 +date: 2023-02-01 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 ff2270fc7658b8..b1e4ccbe45ff69 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.devdocs.json b/api_docs/kbn_slo_schema.devdocs.json index e0241d83dc24d5..74ba5192027780 100644 --- a/api_docs/kbn_slo_schema.devdocs.json +++ b/api_docs/kbn_slo_schema.devdocs.json @@ -520,7 +520,7 @@ "label": "FindSLOResponse", "description": [], "signature": [ - "{ page: number; perPage: number; total: number; results: ({ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; })[]; }" + "{ page: number; perPage: number; total: number; results: ({ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; })[]; }" ], "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", "deprecated": false, @@ -535,7 +535,7 @@ "label": "GetSLOResponse", "description": [], "signature": [ - "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }" + "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }" ], "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", "deprecated": false, @@ -557,6 +557,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/slo-schema", + "id": "def-common.ManageSLOParams", + "type": "Type", + "tags": [], + "label": "ManageSLOParams", + "description": [], + "signature": [ + "{ id: string; }" + ], + "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/slo-schema", "id": "def-common.SLOResponse", @@ -565,7 +580,7 @@ "label": "SLOResponse", "description": [], "signature": [ - "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; }" + "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; }" ], "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", "deprecated": false, @@ -580,7 +595,7 @@ "label": "SLOWithSummaryResponse", "description": [], "signature": [ - "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }" + "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }" ], "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", "deprecated": false, @@ -665,7 +680,7 @@ "label": "UpdateSLOResponse", "description": [], "signature": [ - "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; }" + "{ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; }" ], "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", "deprecated": false, @@ -1527,7 +1542,9 @@ "section": "def-common.Duration", "text": "Duration" }, - ", string, unknown>; }>; createdAt: ", + ", string, unknown>; }>; enabled: ", + "BooleanC", + "; createdAt: ", "Type", "; updatedAt: ", "Type", @@ -1787,7 +1804,9 @@ "section": "def-common.Duration", "text": "Duration" }, - ", string, unknown>; }>; createdAt: ", + ", string, unknown>; }>; enabled: ", + "BooleanC", + "; createdAt: ", "Type", "; updatedAt: ", "Type", @@ -2092,6 +2111,26 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/slo-schema", + "id": "def-common.manageSLOParamsSchema", + "type": "Object", + "tags": [], + "label": "manageSLOParamsSchema", + "description": [], + "signature": [ + "TypeC", + "<{ path: ", + "TypeC", + "<{ id: ", + "StringC", + "; }>; }>" + ], + "path": "packages/kbn-slo-schema/src/rest_specs/slo.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/slo-schema", "id": "def-common.objectiveSchema", @@ -2447,7 +2486,9 @@ "section": "def-common.Duration", "text": "Duration" }, - ", string, unknown>; }>; createdAt: ", + ", string, unknown>; }>; enabled: ", + "BooleanC", + "; createdAt: ", "Type", "; updatedAt: ", "Type", @@ -2659,6 +2700,8 @@ }, ", string, unknown>; }>; revision: ", "NumberC", + "; enabled: ", + "BooleanC", "; createdAt: ", "Type", "; updatedAt: ", @@ -2873,7 +2916,9 @@ "section": "def-common.Duration", "text": "Duration" }, - ", string, unknown>; }>; createdAt: ", + ", string, unknown>; }>; enabled: ", + "BooleanC", + "; createdAt: ", "Type", "; updatedAt: ", "Type", @@ -3113,6 +3158,8 @@ }, ", string, unknown>; }>; revision: ", "NumberC", + "; enabled: ", + "BooleanC", "; createdAt: ", "Type", "; updatedAt: ", @@ -3685,7 +3732,9 @@ "section": "def-common.Duration", "text": "Duration" }, - ", string, unknown>; }>; createdAt: ", + ", string, unknown>; }>; enabled: ", + "BooleanC", + "; createdAt: ", "Type", "; updatedAt: ", "Type", diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index e9ef7a7af2377d..d98678ff620b9a 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 73 | 0 | 73 | 0 | +| 75 | 0 | 75 | 0 | ## Common diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index cae8b252bb9199..9707c52a672b61 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index d03ada1ae175ed..fb8b27f0307c3f 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: 2023-01-30 +date: 2023-02-01 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 e3a26453bf0823..4b69a114d31c67 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: 2023-01-30 +date: 2023-02-01 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 04ac3608174b9b..0dec92b0d93882 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: 2023-01-30 +date: 2023-02-01 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 960d40b23b79cf..c4b6957ba4a64a 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: 2023-01-30 +date: 2023-02-01 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 1e2ace57b79905..eab411a9e9a89e 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: 2023-01-30 +date: 2023-02-01 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 935d7de444987e..2d28278c774c9c 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: 2023-01-30 +date: 2023-02-01 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 44fd681205e76e..808dadac6823fa 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: 2023-01-30 +date: 2023-02-01 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 951d9b19af659c..22c1111321b565 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index 68479d0480a5f8..dc53137f78be6c 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 70dad33d210fa2..46f24f68bb5d8b 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: 2023-01-30 +date: 2023-02-01 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_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index 1c2e1f15f96542..e83d6f4589d1eb 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index 673383ef97b679..1159e29cb79498 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 89f4e93704d271..1cb4c5f23ca013 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: 2023-01-30 +date: 2023-02-01 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 fce46e8052a663..3ed69ffa0adac4 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: 2023-01-30 +date: 2023-02-01 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 e71ed78870a3b1..6971b78773a5c7 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: 2023-01-30 +date: 2023-02-01 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 6e4fd9785a7dc1..0ded8222bee0ed 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: 2023-01-30 +date: 2023-02-01 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 00e072693095ce..707b3c2b7db6e0 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: 2023-01-30 +date: 2023-02-01 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 db5e4488f97584..3808cc6be65039 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: 2023-01-30 +date: 2023-02-01 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 e5fd98c4ccc0b7..4852d6072f7afc 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: 2023-01-30 +date: 2023-02-01 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 6352f703cc5613..651b1364ddc457 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: 2023-01-30 +date: 2023-02-01 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 fe1bcfa772cf69..04e9b5976f489b 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: 2023-01-30 +date: 2023-02-01 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 96d556658dbdf8..c0c777ea22526b 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 29a49dcef5bb72..a96869c9a9e09d 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 54828ce874a67a..6cb7cb99663ec7 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: 2023-01-30 +date: 2023-02-01 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 d627d5d0c68938..b6a176756c1c4c 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index e9199a657935af..6cca0e0beff9b2 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: 2023-01-30 +date: 2023-02-01 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 2096ce85981280..ed0c71295e27fb 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: 2023-01-30 +date: 2023-02-01 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 713b6e659790a4..6e44a2a6abe885 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: 2023-01-30 +date: 2023-02-01 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 362ddfc8be3aad..fa53d9fff26d76 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: 2023-01-30 +date: 2023-02-01 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 a0ec73841ae037..a823653af0f55f 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: 2023-01-30 +date: 2023-02-01 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 f8db2ec390c7af..2fe67de9561602 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: 2023-01-30 +date: 2023-02-01 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 7372a92b629787..c701aff90516fa 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: 2023-01-30 +date: 2023-02-01 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 dfad2c51573b25..a59eee2fc5fdd1 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: 2023-01-30 +date: 2023-02-01 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 e2be5b8a93ce79..06dc4e1ef55d47 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: 2023-01-30 +date: 2023-02-01 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 e935ba982517e9..557df4f10cc2cb 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 38a0fe38afb2b2..36b7b8cac7d0e0 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index d369b808ecb526..d345c5b0e87838 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -4353,6 +4353,26 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "observability", + "id": "def-public.ObservabilityPublicPluginsStart.unifiedSearch", + "type": "Object", + "tags": [], + "label": "unifiedSearch", + "description": [], + "signature": [ + { + "pluginId": "unifiedSearch", + "scope": "public", + "docId": "kibUnifiedSearchPluginApi", + "section": "def-public.UnifiedSearchPublicPluginStart", + "text": "UnifiedSearchPublicPluginStart" + } + ], + "path": "x-pack/plugins/observability/public/plugin.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "observability", "id": "def-public.ObservabilityPublicPluginsStart.home", @@ -10002,7 +10022,85 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; }, ", + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; }, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteCreateOptions", + "text": "ObservabilityRouteCreateOptions" + }, + "> | undefined; \"GET /api/observability/slos/{id}\"?: ", + { + "pluginId": "@kbn/server-route-repository", + "scope": "public", + "docId": "kibKbnServerRouteRepositoryPluginApi", + "section": "def-public.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /api/observability/slos/{id}\", ", + "TypeC", + "<{ path: ", + "TypeC", + "<{ id: ", + "StringC", + "; }>; }>, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteHandlerResources", + "text": "ObservabilityRouteHandlerResources" + }, + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteCreateOptions", + "text": "ObservabilityRouteCreateOptions" + }, + "> | undefined; \"GET /api/observability/slos\"?: ", + { + "pluginId": "@kbn/server-route-repository", + "scope": "public", + "docId": "kibKbnServerRouteRepositoryPluginApi", + "section": "def-public.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /api/observability/slos\", ", + "PartialC", + "<{ query: ", + "PartialC", + "<{ name: ", + "StringC", + "; indicatorTypes: ", + "Type", + "; page: ", + "StringC", + "; perPage: ", + "StringC", + "; sortBy: ", + "UnionC", + "<[", + "LiteralC", + "<\"name\">, ", + "LiteralC", + "<\"indicatorType\">]>; sortDirection: ", + "UnionC", + "<[", + "LiteralC", + "<\"asc\">, ", + "LiteralC", + "<\"desc\">]>; }>; }>, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteHandlerResources", + "text": "ObservabilityRouteHandlerResources" + }, + ", { page: number; perPage: number; total: number; results: ({ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; })[]; }, ", { "pluginId": "observability", "scope": "server", @@ -10042,7 +10140,7 @@ "section": "def-server.ObservabilityRouteCreateOptions", "text": "ObservabilityRouteCreateOptions" }, - "> | undefined; \"GET /api/observability/slos/{id}\"?: ", + "> | undefined; \"POST /api/observability/slos/{id}/enable\"?: ", { "pluginId": "@kbn/server-route-repository", "scope": "public", @@ -10050,7 +10148,7 @@ "section": "def-public.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/observability/slos/{id}\", ", + "<\"POST /api/observability/slos/{id}/enable\", ", "TypeC", "<{ path: ", "TypeC", @@ -10064,7 +10162,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }, ", + ", void, ", { "pluginId": "observability", "scope": "server", @@ -10072,7 +10170,7 @@ "section": "def-server.ObservabilityRouteCreateOptions", "text": "ObservabilityRouteCreateOptions" }, - "> | undefined; \"GET /api/observability/slos\"?: ", + "> | undefined; \"POST /api/observability/slos/{id}/disable\"?: ", { "pluginId": "@kbn/server-route-repository", "scope": "public", @@ -10080,31 +10178,13 @@ "section": "def-public.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/observability/slos\", ", - "PartialC", - "<{ query: ", - "PartialC", - "<{ name: ", - "StringC", - "; indicatorTypes: ", - "Type", - "; page: ", - "StringC", - "; perPage: ", + "<\"POST /api/observability/slos/{id}/disable\", ", + "TypeC", + "<{ path: ", + "TypeC", + "<{ id: ", "StringC", - "; sortBy: ", - "UnionC", - "<[", - "LiteralC", - "<\"name\">, ", - "LiteralC", - "<\"indicatorType\">]>; sortDirection: ", - "UnionC", - "<[", - "LiteralC", - "<\"asc\">, ", - "LiteralC", - "<\"desc\">]>; }>; }>, ", + "; }>; }>, ", { "pluginId": "observability", "scope": "server", @@ -10112,7 +10192,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { page: number; perPage: number; total: number; results: ({ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; })[]; }, ", + ", void, ", { "pluginId": "observability", "scope": "server", @@ -10656,7 +10736,85 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; }, ", + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; }, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteCreateOptions", + "text": "ObservabilityRouteCreateOptions" + }, + "> | undefined; \"GET /api/observability/slos/{id}\"?: ", + { + "pluginId": "@kbn/server-route-repository", + "scope": "public", + "docId": "kibKbnServerRouteRepositoryPluginApi", + "section": "def-public.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /api/observability/slos/{id}\", ", + "TypeC", + "<{ path: ", + "TypeC", + "<{ id: ", + "StringC", + "; }>; }>, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteHandlerResources", + "text": "ObservabilityRouteHandlerResources" + }, + ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteCreateOptions", + "text": "ObservabilityRouteCreateOptions" + }, + "> | undefined; \"GET /api/observability/slos\"?: ", + { + "pluginId": "@kbn/server-route-repository", + "scope": "public", + "docId": "kibKbnServerRouteRepositoryPluginApi", + "section": "def-public.ServerRoute", + "text": "ServerRoute" + }, + "<\"GET /api/observability/slos\", ", + "PartialC", + "<{ query: ", + "PartialC", + "<{ name: ", + "StringC", + "; indicatorTypes: ", + "Type", + "; page: ", + "StringC", + "; perPage: ", + "StringC", + "; sortBy: ", + "UnionC", + "<[", + "LiteralC", + "<\"name\">, ", + "LiteralC", + "<\"indicatorType\">]>; sortDirection: ", + "UnionC", + "<[", + "LiteralC", + "<\"asc\">, ", + "LiteralC", + "<\"desc\">]>; }>; }>, ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.ObservabilityRouteHandlerResources", + "text": "ObservabilityRouteHandlerResources" + }, + ", { page: number; perPage: number; total: number; results: ({ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; enabled: boolean; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; })[]; }, ", { "pluginId": "observability", "scope": "server", @@ -10696,7 +10854,7 @@ "section": "def-server.ObservabilityRouteCreateOptions", "text": "ObservabilityRouteCreateOptions" }, - "> | undefined; \"GET /api/observability/slos/{id}\"?: ", + "> | undefined; \"POST /api/observability/slos/{id}/enable\"?: ", { "pluginId": "@kbn/server-route-repository", "scope": "public", @@ -10704,7 +10862,7 @@ "section": "def-public.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/observability/slos/{id}\", ", + "<\"POST /api/observability/slos/{id}/enable\", ", "TypeC", "<{ path: ", "TypeC", @@ -10718,7 +10876,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; }, ", + ", void, ", { "pluginId": "observability", "scope": "server", @@ -10726,7 +10884,7 @@ "section": "def-server.ObservabilityRouteCreateOptions", "text": "ObservabilityRouteCreateOptions" }, - "> | undefined; \"GET /api/observability/slos\"?: ", + "> | undefined; \"POST /api/observability/slos/{id}/disable\"?: ", { "pluginId": "@kbn/server-route-repository", "scope": "public", @@ -10734,31 +10892,13 @@ "section": "def-public.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/observability/slos\", ", - "PartialC", - "<{ query: ", - "PartialC", - "<{ name: ", - "StringC", - "; indicatorTypes: ", - "Type", - "; page: ", - "StringC", - "; perPage: ", + "<\"POST /api/observability/slos/{id}/disable\", ", + "TypeC", + "<{ path: ", + "TypeC", + "<{ id: ", "StringC", - "; sortBy: ", - "UnionC", - "<[", - "LiteralC", - "<\"name\">, ", - "LiteralC", - "<\"indicatorType\">]>; sortDirection: ", - "UnionC", - "<[", - "LiteralC", - "<\"asc\">, ", - "LiteralC", - "<\"desc\">]>; }>; }>, ", + "; }>; }>, ", { "pluginId": "observability", "scope": "server", @@ -10766,7 +10906,7 @@ "section": "def-server.ObservabilityRouteHandlerResources", "text": "ObservabilityRouteHandlerResources" }, - ", { page: number; perPage: number; total: number; results: ({ id: string; name: string; description: string; indicator: { type: \"sli.apm.transactionDuration\"; params: { environment: string; service: string; transactionType: string; transactionName: string; 'threshold.us': number; } & { index?: string | undefined; }; } | { type: \"sli.apm.transactionErrorRate\"; params: { environment: string; service: string; transactionType: string; transactionName: string; } & { goodStatusCodes?: (\"2xx\" | \"3xx\" | \"4xx\" | \"5xx\")[] | undefined; index?: string | undefined; }; } | { type: \"sli.kql.custom\"; params: { index: string; filter: string; good: string; total: string; }; }; timeWindow: { duration: string; isRolling: boolean; } | { duration: string; calendar: { startTime: string; }; }; budgetingMethod: \"occurrences\" | \"timeslices\"; objective: { target: number; } & { timesliceTarget?: number | undefined; timesliceWindow?: string | undefined; }; revision: number; settings: { timestampField: string; syncDelay: string; frequency: string; }; createdAt: string; updatedAt: string; } & { summary: { status: \"NO_DATA\" | \"HEALTHY\" | \"DEGRADING\" | \"VIOLATED\"; sliValue: number; errorBudget: { initial: number; consumed: number; remaining: number; isEstimated: boolean; }; }; })[]; }, ", + ", void, ", { "pluginId": "observability", "scope": "server", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index eee80c7d075015..24c92d56111bd2 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Observability UI](https://github.com/orgs/elastic/teams/observability-u | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 606 | 41 | 600 | 32 | +| 607 | 41 | 601 | 32 | ## Client diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index dd794c0fbfba7d..bfcef3ec140019 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: 2023-01-30 +date: 2023-02-01 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 96bd8997cd457e..41c9a4e0b0e66b 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 564 | 464 | 43 | +| 568 | 467 | 44 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 70017 | 527 | 59226 | 1206 | +| 70116 | 527 | 59323 | 1210 | ## Plugin Directory @@ -47,8 +47,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | cloudLinks | [Kibana Core](https://github.com/orgs/elastic/teams/@kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 | | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 17 | 0 | 2 | 2 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | +| | [@elastic/kibana-global-experience](https://github.com/orgs/elastic/teams/@elastic/kibana-global-experience) | Content management app | 5 | 0 | 5 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 268 | 0 | 264 | 9 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2832 | 17 | 1016 | 0 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2845 | 17 | 1029 | 0 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | customBranding | [global-experience](https://github.com/orgs/elastic/teams/kibana-global-experience) | Enables customization of Kibana | 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 | @@ -89,7 +90,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [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 | | | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/team:AppServicesUx) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 254 | 1 | 45 | 5 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/@elastic/appex-sharedux) | Simple UI for managing files in Kibana | 2 | 1 | 2 | 0 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1040 | 3 | 935 | 25 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1039 | 3 | 934 | 25 | | ftrApis | [Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 68 | 0 | 14 | 5 | | globalSearchBar | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | @@ -126,7 +127,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 34 | 0 | 34 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 2 | 0 | 2 | 1 | -| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 606 | 41 | 600 | 32 | +| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 607 | 41 | 601 | 32 | | | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 24 | 0 | 24 | 7 | | painlessLab | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 217 | 7 | 161 | 11 | @@ -134,7 +135,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Reporting Services enables applications to feature reports that the user can automate with Watcher and download later. | 36 | 0 | 16 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 21 | 0 | 21 | 0 | -| | [RAC](https://github.com/orgs/elastic/teams/rac) | - | 241 | 0 | 213 | 10 | +| | [RAC](https://github.com/orgs/elastic/teams/rac) | - | 251 | 0 | 223 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 24 | 0 | 19 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 196 | 2 | 155 | 5 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 16 | 0 | 16 | 0 | @@ -150,7 +151,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [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 | 118 | 0 | 59 | 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 | +| | [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. | 261 | 0 | 65 | 0 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 12 | 0 | 12 | 2 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 4 | 0 | 4 | 0 | | synthetics | [Uptime](https://github.com/orgs/elastic/teams/uptime) | This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions. | 0 | 0 | 0 | 0 | @@ -159,11 +160,11 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 31 | 0 | 26 | 6 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 1 | 0 | 1 | 0 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 5 | 0 | 0 | 0 | -| | [Protections Experience Team](https://github.com/orgs/elastic/teams/protections-experience) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 35 | 0 | 15 | 5 | +| | [Protections Experience Team](https://github.com/orgs/elastic/teams/protections-experience) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 35 | 0 | 14 | 5 | | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 257 | 1 | 214 | 20 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [Kibana Localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 587 | 11 | 558 | 53 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 589 | 11 | 560 | 53 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 134 | 2 | 92 | 9 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 206 | 0 | 140 | 9 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 267 | 0 | 242 | 7 | @@ -217,6 +218,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 3 | 0 | 3 | 0 | | | [Owner missing] | - | 62 | 0 | 17 | 1 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | +| | [Owner missing] | - | 2 | 0 | 2 | 1 | +| | [Owner missing] | - | 37 | 0 | 36 | 0 | | | [Owner missing] | - | 106 | 0 | 80 | 1 | | | Kibana Core | - | 73 | 0 | 44 | 9 | | | Kibana Core | - | 24 | 0 | 24 | 0 | @@ -251,7 +254,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 5 | 0 | 5 | 0 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | [Owner missing] | - | 6 | 0 | 1 | 0 | -| | [Owner missing] | - | 6 | 0 | 6 | 0 | +| | [Owner missing] | - | 11 | 0 | 11 | 0 | | | [Owner missing] | - | 6 | 0 | 6 | 1 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | Kibana Core | - | 9 | 0 | 3 | 0 | @@ -401,7 +404,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 29 | 0 | 29 | 1 | | | [Owner missing] | - | 1 | 0 | 0 | 0 | | | [Owner missing] | - | 6 | 0 | 0 | 0 | -| | [Owner missing] | - | 58 | 0 | 56 | 1 | +| | [Owner missing] | - | 52 | 0 | 50 | 2 | | | [Owner missing] | - | 19 | 1 | 12 | 0 | | | [Owner missing] | - | 3 | 0 | 3 | 0 | | | Kibana Core | - | 1 | 0 | 1 | 0 | @@ -415,7 +418,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 13 | 0 | 13 | 0 | | | [Owner missing] | - | 67 | 0 | 62 | 5 | | | [Owner missing] | - | 32 | 2 | 28 | 0 | -| | [Owner missing] | - | 86 | 0 | 85 | 0 | +| | [Owner missing] | - | 108 | 0 | 107 | 0 | | | [Owner missing] | - | 7 | 0 | 5 | 0 | | | Kibana Core | - | 27 | 0 | 1 | 2 | | | Kibana Core | - | 8 | 0 | 8 | 0 | @@ -443,7 +446,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 13 | 0 | 9 | 0 | | | [Owner missing] | - | 6 | 0 | 6 | 1 | | | [Owner missing] | - | 13 | 2 | 8 | 0 | -| | [Owner missing] | - | 99 | 0 | 96 | 0 | +| | [Owner missing] | - | 100 | 0 | 97 | 0 | | | [Owner missing] | Security Solution auto complete | 56 | 1 | 41 | 1 | | | [Owner missing] | - | 341 | 1 | 337 | 32 | | | [Owner missing] | security solution elastic search utilities to use across plugins such lists, security_solution, cases, etc... | 67 | 0 | 61 | 1 | @@ -482,7 +485,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 2 | 0 | 2 | 1 | | | [Owner missing] | - | 32 | 0 | 31 | 0 | | | [Owner missing] | - | 14 | 0 | 5 | 1 | -| | [Owner missing] | - | 12 | 0 | 12 | 0 | +| | [Owner missing] | - | 17 | 0 | 17 | 0 | | | [Owner missing] | - | 8 | 0 | 3 | 0 | | | [Owner missing] | - | 25 | 0 | 24 | 0 | | | [Owner missing] | - | 11 | 0 | 6 | 0 | @@ -500,7 +503,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 2 | 0 | 0 | 0 | | | [Owner missing] | - | 15 | 0 | 4 | 0 | | | [Owner missing] | - | 9 | 0 | 3 | 0 | -| | [Owner missing] | SLO io-ts schema definition and common models shared between public and server. | 73 | 0 | 73 | 0 | +| | [Owner missing] | SLO io-ts schema definition and common models shared between public and server. | 75 | 0 | 75 | 0 | | | [Owner missing] | - | 20 | 0 | 12 | 0 | | | Kibana Core | - | 97 | 1 | 64 | 1 | | | [Owner missing] | - | 4 | 0 | 2 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index ad4d121202873c..3a62d1aeb725ae 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: 2023-01-30 +date: 2023-02-01 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 148ce4163f1872..b3f136201968d7 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: 2023-01-30 +date: 2023-02-01 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 f86b5ea1ad68bb..6a4c5f000e9cdf 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: 2023-01-30 +date: 2023-02-01 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 10a64ef76481ee..737f9a6fa500a4 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: 2023-01-30 +date: 2023-02-01 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 15e34aff07d779..7b3ca35b4286b9 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.devdocs.json b/api_docs/rule_registry.devdocs.json index 34a50ca6688ef1..bd2a4566d81410 100644 --- a/api_docs/rule_registry.devdocs.json +++ b/api_docs/rule_registry.devdocs.json @@ -2475,7 +2475,7 @@ "signature": [ "(request: TSearchRequest) => Promise<", + ", TAlertDoc = Partial> & OutputOf>>>(request: TSearchRequest) => Promise<", { "pluginId": "@kbn/es-types", "scope": "common", @@ -2483,7 +2483,7 @@ "section": "def-common.ESSearchResponse", "text": "ESSearchResponse" }, - "> & OutputOf>>, TSearchRequest, { restTotalHitsAsInt: false; }>>" + ">" ], "path": "x-pack/plugins/rule_registry/server/rule_data_client/types.ts", "deprecated": false, @@ -3295,6 +3295,86 @@ "trackAdoption": false } ] + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.PersistenceServices.alertWithSuppression", + "type": "Function", + "tags": [], + "label": "alertWithSuppression", + "description": [], + "signature": [ + "(alerts: { _id: string; _source: T; }[], suppressionWindow: string, enrichAlerts?: ((alerts: { _id: string; _source: T; }[], params: { spaceId: string; }) => Promise<{ _id: string; _source: T; }[]>) | undefined, currentTimeOverride?: Date | undefined) => Promise, \"alertsWereTruncated\">>" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "ruleRegistry", + "id": "def-server.PersistenceServices.alertWithSuppression.$1", + "type": "Array", + "tags": [], + "label": "alerts", + "description": [], + "signature": [ + "{ _id: string; _source: T; }[]" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.PersistenceServices.alertWithSuppression.$2", + "type": "string", + "tags": [], + "label": "suppressionWindow", + "description": [], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.PersistenceServices.alertWithSuppression.$3", + "type": "Function", + "tags": [], + "label": "enrichAlerts", + "description": [], + "signature": [ + "((alerts: { _id: string; _source: T; }[], params: { spaceId: string; }) => Promise<{ _id: string; _source: T; }[]>) | undefined" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.PersistenceServices.alertWithSuppression.$4", + "type": "Object", + "tags": [], + "label": "currentTimeOverride", + "description": [], + "signature": [ + "Date | undefined" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + } + ] } ], "initialIsOpen": false @@ -3973,6 +4053,87 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.SuppressedAlertService", + "type": "Type", + "tags": [], + "label": "SuppressedAlertService", + "description": [], + "signature": [ + "(alerts: { _id: string; _source: T; }[], suppressionWindow: string, enrichAlerts?: ((alerts: { _id: string; _source: T; }[], params: { spaceId: string; }) => Promise<{ _id: string; _source: T; }[]>) | undefined, currentTimeOverride?: Date | undefined) => Promise, \"alertsWereTruncated\">>" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "ruleRegistry", + "id": "def-server.SuppressedAlertService.$1", + "type": "Array", + "tags": [], + "label": "alerts", + "description": [], + "signature": [ + "{ _id: string; _source: T; }[]" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.SuppressedAlertService.$2", + "type": "string", + "tags": [], + "label": "suppressionWindow", + "description": [], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.SuppressedAlertService.$3", + "type": "Function", + "tags": [], + "label": "enrichAlerts", + "description": [], + "signature": [ + "((alerts: { _id: string; _source: T; }[], params: { spaceId: string; }) => Promise<{ _id: string; _source: T; }[]>) | undefined" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "ruleRegistry", + "id": "def-server.SuppressedAlertService.$4", + "type": "Object", + "tags": [], + "label": "currentTimeOverride", + "description": [], + "signature": [ + "Date | undefined" + ], + "path": "x-pack/plugins/rule_registry/server/utils/persistence_types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "ruleRegistry", "id": "def-server.Version", diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 2bec837f88bb22..baec7bcf045480 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; @@ -21,7 +21,7 @@ Contact [RAC](https://github.com/orgs/elastic/teams/rac) for questions regarding | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 241 | 0 | 213 | 10 | +| 251 | 0 | 223 | 11 | ## Server diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 795ac6a0fb15d8..2b15f15c5708f0 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: 2023-01-30 +date: 2023-02-01 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 7123a0630fdc9f..9e8cb02421dc2c 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: 2023-01-30 +date: 2023-02-01 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 a85d08d1dd6cb7..abbcfc8c7cb7ba 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: 2023-01-30 +date: 2023-02-01 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 bcf415146653e4..5c0e3b32931c91 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: 2023-01-30 +date: 2023-02-01 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 7e69c9aa7f330c..820ddf4e0fbb27 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: 2023-01-30 +date: 2023-02-01 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 fc23fab122614f..b1e5ed9732d4ff 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: 2023-01-30 +date: 2023-02-01 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 2a59f90373c6b2..5c4b2d30cd7b64 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: 2023-01-30 +date: 2023-02-01 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 e81521a36d9f30..18a417ae2f0fc8 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: 2023-01-30 +date: 2023-02-01 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 ab38e735def3e8..bff5c39359c13a 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: 2023-01-30 +date: 2023-02-01 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 db4ef30ffff3ea..0bcb2b698b29c4 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index 71cad56d90bfcd..8d2f84311053b7 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -95,7 +95,7 @@ "label": "experimentalFeatures", "description": [], "signature": [ - "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly disableIsolationUIPendingStatuses: boolean; readonly pendingActionResponsesWithAck: boolean; readonly policyListEnabled: boolean; readonly policyResponseInFleetEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly responseActionsConsoleEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointRbacEnabled: boolean; readonly endpointRbacV1Enabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionGetFileEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly riskyHostsEnabled: boolean; readonly riskyUsersEnabled: boolean; }" + "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly disableIsolationUIPendingStatuses: boolean; readonly pendingActionResponsesWithAck: boolean; readonly policyListEnabled: boolean; readonly policyResponseInFleetEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly responseActionsConsoleEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointRbacEnabled: boolean; readonly endpointRbacV1Enabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionGetFileEnabled: boolean; readonly responseActionExecuteEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly riskyHostsEnabled: boolean; readonly riskyUsersEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/public/plugin.tsx", "deprecated": false, diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 3d410710943c98..86ab70e0f4ddb4 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: 2023-01-30 +date: 2023-02-01 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 47228fc6d64676..e6f401303bb3b9 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 9719d01b3b16e8..32c091c4a20491 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index f391462896f27e..c38dcebec48644 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.devdocs.json b/api_docs/spaces.devdocs.json index b343b7665dcc18..43d132ba173529 100644 --- a/api_docs/spaces.devdocs.json +++ b/api_docs/spaces.devdocs.json @@ -4964,6 +4964,21 @@ ], "enums": [], "misc": [ + { + "parentPluginId": "spaces", + "id": "def-common.DEFAULT_SPACE_ID", + "type": "string", + "tags": [], + "label": "DEFAULT_SPACE_ID", + "description": [], + "signature": [ + "\"default\"" + ], + "path": "x-pack/plugins/spaces/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "spaces", "id": "def-common.ENTER_SPACE_PATH", diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 7e0ae543b84d7a..d3a0d7e229a3b8 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Platform Security](https://github.com/orgs/elastic/teams/kibana-securit | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 260 | 0 | 64 | 0 | +| 261 | 0 | 65 | 0 | ## Client diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index f2df54ac738153..5de27eddfe93e7 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: 2023-01-30 +date: 2023-02-01 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 966397d69a08cf..066a511c807ce5 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: 2023-01-30 +date: 2023-02-01 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 e5e4626cebc7c9..8048fce4529ef4 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: 2023-01-30 +date: 2023-02-01 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 42c7119795c87f..de42eda2139f07 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: 2023-01-30 +date: 2023-02-01 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 1c4babb819ffb4..b3be09888e8818 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: 2023-01-30 +date: 2023-02-01 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 3f20854762aff9..c7517987b9b68e 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: 2023-01-30 +date: 2023-02-01 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 61fc83e8d80d15..4a5ccc7e363c5c 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.devdocs.json b/api_docs/threat_intelligence.devdocs.json index ccce9277fb2406..7632f0a36a0cbe 100644 --- a/api_docs/threat_intelligence.devdocs.json +++ b/api_docs/threat_intelligence.devdocs.json @@ -509,9 +509,11 @@ "type": "Object", "tags": [], "label": "blockList", - "description": [], + "description": [ + "\nAdd to blocklist feature" + ], "signature": [ - "{ exceptionListApiClient: unknown; useSetUrlParams: () => (params: Record, replace?: boolean | undefined) => void; getFlyoutComponent: () => React.NamedExoticComponent<", + "{ canWriteBlocklist: boolean; exceptionListApiClient: unknown; useSetUrlParams: () => (params: Record, replace?: boolean | undefined) => void; getFlyoutComponent: () => React.NamedExoticComponent<", "BlockListFlyoutProps", ">; getFormComponent: () => React.NamedExoticComponent<", "BlockListFormProps", diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index fdefb06e38d466..42341675f4a04e 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Protections Experience Team](https://github.com/orgs/elastic/teams/prot | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 35 | 0 | 15 | 5 | +| 35 | 0 | 14 | 5 | ## Client diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index efdbc8646e1545..9cd2fdcb0867c8 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: 2023-01-30 +date: 2023-02-01 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 d47d5255bcf4f9..7ed79dc1efc309 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 8beabd8957f664..2692b9d183efbe 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -3546,6 +3546,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertsTableProps.showAlertStatusWithFlapping", + "type": "CompoundType", + "tags": [], + "label": "showAlertStatusWithFlapping", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.AlertsTableProps.trailingControlColumns", @@ -8707,6 +8721,22 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.TriggersAndActionsUIPublicPluginStart.getRulesSettingsLink", + "type": "Function", + "tags": [], + "label": "getRulesSettingsLink", + "description": [], + "signature": [ + "() => React.ReactElement>" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] } ], "lifecycle": "start", diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 0eb4b68b7548e7..0a94d435ca1b25 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.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 | |-------------------|-----------|------------------------|-----------------| -| 587 | 11 | 558 | 53 | +| 589 | 11 | 560 | 53 | ## Client diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 495102dc60bead..8701979d5ff22c 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: 2023-01-30 +date: 2023-02-01 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 53e3048ef591ad..df606efa0a1f84 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: 2023-01-30 +date: 2023-02-01 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 2a5c72c19a0553..99da70ff9f1810 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index a23bcfb92f3d49..32e66df9155196 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 03f7841649b463..382bc19c3825c5 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: 2023-01-30 +date: 2023-02-01 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 31747fe10f66a4..d9c0d883eefd8c 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: 2023-01-30 +date: 2023-02-01 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 4b0a87e5f55a3d..ca6b72edfe09d1 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: 2023-01-30 +date: 2023-02-01 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 22d3654c6a69d9..f023aba43da7ad 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: 2023-01-30 +date: 2023-02-01 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 22d7e86e365a2f..127cde6195a238 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: 2023-01-30 +date: 2023-02-01 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 f5d28f485cf742..867f69ded97da8 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: 2023-01-30 +date: 2023-02-01 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 0b3d5343a149eb..39e665bc294d16 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: 2023-01-30 +date: 2023-02-01 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 672d59a578bc61..c32d860339204b 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: 2023-01-30 +date: 2023-02-01 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 827923c9236e31..bebbf827075bf6 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: 2023-01-30 +date: 2023-02-01 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 eef6fd0a968e6d..778281ab6c5ec5 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: 2023-01-30 +date: 2023-02-01 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 2052315ac60fc0..d9d25fa8a71bb7 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: 2023-01-30 +date: 2023-02-01 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 9a668ca887a6f9..0ed4d97f594e0b 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: 2023-01-30 +date: 2023-02-01 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 09a9a548e978c2..9120bf2a52bf80 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: 2023-01-30 +date: 2023-02-01 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 64956776615041..28200b09ad21a0 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: 2023-01-30 +date: 2023-02-01 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 20790046ae6e9c..7c9586ed2eed58 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: 2023-01-30 +date: 2023-02-01 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 2d7bc6c01527c6..8d98f8a4778e38 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: 2023-01-30 +date: 2023-02-01 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; From 519f3450069a8fb7bb818094ab072655a1257e0f Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Reddy Burri Date: Wed, 1 Feb 2023 12:12:34 +0530 Subject: [PATCH 24/59] Add filebeat_input index to agent policy default (#149974) ## Summary Allow agent's inbuilt monitoring to fetch filebeat's `/inputs` metrics into `filbeat_input` metrics datastream ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/fleet/common/constants/agent_policy.ts | 1 + .../__snapshots__/monitoring_permissions.test.ts.snap | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index 8e818ddf206cc3..50cef1fbe43cfa 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -17,6 +17,7 @@ export const AGENT_POLICY_DEFAULT_MONITORING_DATASETS = [ 'elastic_agent.elastic_agent', 'elastic_agent.apm_server', 'elastic_agent.filebeat', + 'elastic_agent.filebeat_input', 'elastic_agent.fleet_server', 'elastic_agent.metricbeat', 'elastic_agent.osquerybeat', diff --git a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap index 3917a7d71533ba..49a11c226ce491 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/monitoring_permissions.test.ts.snap @@ -109,6 +109,7 @@ Object { "logs-elastic_agent.elastic_agent-testnamespace123", "logs-elastic_agent.apm_server-testnamespace123", "logs-elastic_agent.filebeat-testnamespace123", + "logs-elastic_agent.filebeat_input-testnamespace123", "logs-elastic_agent.fleet_server-testnamespace123", "logs-elastic_agent.metricbeat-testnamespace123", "logs-elastic_agent.osquerybeat-testnamespace123", @@ -122,6 +123,7 @@ Object { "metrics-elastic_agent.elastic_agent-testnamespace123", "metrics-elastic_agent.apm_server-testnamespace123", "metrics-elastic_agent.filebeat-testnamespace123", + "metrics-elastic_agent.filebeat_input-testnamespace123", "metrics-elastic_agent.fleet_server-testnamespace123", "metrics-elastic_agent.metricbeat-testnamespace123", "metrics-elastic_agent.osquerybeat-testnamespace123", @@ -152,6 +154,7 @@ Object { "logs-elastic_agent.elastic_agent-testnamespace123", "logs-elastic_agent.apm_server-testnamespace123", "logs-elastic_agent.filebeat-testnamespace123", + "logs-elastic_agent.filebeat_input-testnamespace123", "logs-elastic_agent.fleet_server-testnamespace123", "logs-elastic_agent.metricbeat-testnamespace123", "logs-elastic_agent.osquerybeat-testnamespace123", @@ -182,6 +185,7 @@ Object { "metrics-elastic_agent.elastic_agent-testnamespace123", "metrics-elastic_agent.apm_server-testnamespace123", "metrics-elastic_agent.filebeat-testnamespace123", + "metrics-elastic_agent.filebeat_input-testnamespace123", "metrics-elastic_agent.fleet_server-testnamespace123", "metrics-elastic_agent.metricbeat-testnamespace123", "metrics-elastic_agent.osquerybeat-testnamespace123", From 4cf8f6e33db96db87fbfa3794d8fafc09d6dd0b6 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 1 Feb 2023 08:07:32 +0100 Subject: [PATCH 25/59] Add SLO cloning functionality (#149935) --- .../observability/public/data/slo/slo.ts | 2 +- .../public/hooks/slo/use_create_slo.ts | 6 + .../public/pages/slos/components/slo_list.tsx | 29 ++-- .../slos/components/slo_list_item.stories.tsx | 4 + .../pages/slos/components/slo_list_item.tsx | 61 ++++++++- .../components/slo_list_items.stories.tsx | 2 + .../pages/slos/components/slo_list_items.tsx | 14 +- .../public/pages/slos/index.test.tsx | 126 +++++++++++++++++- 8 files changed, 218 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/observability/public/data/slo/slo.ts b/x-pack/plugins/observability/public/data/slo/slo.ts index 6d22027b908fd8..047083c28986c7 100644 --- a/x-pack/plugins/observability/public/data/slo/slo.ts +++ b/x-pack/plugins/observability/public/data/slo/slo.ts @@ -92,7 +92,7 @@ export const sloList: FindSLOResponse = { }, { ...baseSlo, - id: 'c0f8d669-9177-4706-9098-f397a88173a6', + id: 'c0f8d669-9277-4706-9098-f397a88173a6', summary: buildViolatedSummary(), timeWindow: buildRollingTimeWindow({ duration: '7d' }), }, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts index 31f8202c3c77e8..1b810117fa2884 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts @@ -41,6 +41,9 @@ export function useCreateOrUpdateSlo(): UseCreateOrUpdateSlo { setSuccess(true); } catch (e) { setError(e); + } finally { + setSuccess(false); + setLoading(false); } }, [http] @@ -58,6 +61,9 @@ export function useCreateOrUpdateSlo(): UseCreateOrUpdateSlo { setSuccess(true); } catch (e) { setError(e); + } finally { + setSuccess(false); + setLoading(false); } }, [http] diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index e91d42fb5b4382..ac7785cd4921cf 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -24,11 +24,11 @@ export function SloList() { const [sort, setSort] = useState('name'); const [indicatorTypeFilter, setIndicatorTypeFilter] = useState([]); - const [deleting, setIsDeleting] = useState(false); + const [isCloningOrDeleting, setIsCloningOrDeleting] = useState(false); const [shouldReload, setShouldReload] = useState(false); const { - loading, + loading: isLoadingSloList, error, sloList: { results: sloList = [], total, perPage }, } = useFetchSloList({ @@ -42,16 +42,19 @@ export function SloList() { useEffect(() => { if (shouldReload) { setShouldReload(false); - setIsDeleting(false); } - }, [shouldReload]); - const handleDeleted = () => { - setShouldReload(true); + if (!isLoadingSloList) { + setIsCloningOrDeleting(false); + } + }, [isLoadingSloList, shouldReload]); + + const handleCloningOrDeleting = () => { + setIsCloningOrDeleting(true); }; - const handleDeleting = () => { - setIsDeleting(true); + const handleClonedOrDeleted = () => { + setShouldReload(true); }; const handlePageClick = (pageNumber: number) => { @@ -79,7 +82,7 @@ export function SloList() { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx index 17d7c74ccc066a..9eaec700ba3cf3 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx @@ -29,6 +29,10 @@ const Template: ComponentStory = (props: SloListItemProps) => const defaultProps = { slo: buildSlo(), historicalSummary: historicalSummaryData[HEALTHY_ROLLING_SLO], + onCloned: () => {}, + onCloning: () => {}, + onDeleted: () => {}, + onDeleting: () => {}, }; export const SloListItem = Template.bind({}); diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx index 0ea2f14a97352f..03d5b5326e3a61 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiButtonIcon, EuiContextMenuItem, @@ -20,15 +20,22 @@ import { i18n } from '@kbn/i18n'; import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useKibana } from '../../../utils/kibana_react'; +import { useCreateOrUpdateSlo } from '../../../hooks/slo/use_create_slo'; import { SloSummary } from './slo_summary'; import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal'; import { SloBadges } from './badges/slo_badges'; +import { + transformSloResponseToCreateSloInput, + transformValuesToCreateSLOInput, +} from '../../slo_edit/helpers/process_slo_form_values'; import { paths } from '../../../config'; export interface SloListItemProps { slo: SLOWithSummaryResponse; historicalSummary?: HistoricalSummaryResponse[]; historicalSummaryLoading: boolean; + onCloned: () => void; + onCloning: () => void; onDeleted: () => void; onDeleting: () => void; } @@ -37,6 +44,8 @@ export function SloListItem({ slo, historicalSummary = [], historicalSummaryLoading, + onCloned, + onCloning, onDeleted, onDeleting, }: SloListItemProps) { @@ -45,6 +54,8 @@ export function SloListItem({ http: { basePath }, } = useKibana().services; + const { createSlo, loading: isCloning, success: isCloned } = useCreateOrUpdateSlo(); + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -57,6 +68,15 @@ export function SloListItem({ navigateToUrl(basePath.prepend(paths.observability.sloEdit(slo.id))); }; + const handleClone = () => { + const newSlo = transformValuesToCreateSLOInput( + transformSloResponseToCreateSloInput({ ...slo, name: `[Copy] ${slo.name}` })! + ); + + createSlo(newSlo); + setIsActionsPopoverOpen(false); + }; + const handleDelete = () => { setDeleteConfirmationModalOpen(true); setIsDeleting(true); @@ -73,12 +93,23 @@ export function SloListItem({ onDeleted(); }; + useEffect(() => { + if (isCloning) { + onCloning(); + } + + if (isCloned) { + onCloned(); + } + }, [isCloned, isCloning, onCloned, onCloning]); + return ( {/* CONTENT */} @@ -124,12 +155,32 @@ export function SloListItem({ + {i18n.translate('xpack.observability.slos.slo.item.actions.edit', { defaultMessage: 'Edit', })} , - + + {i18n.translate('xpack.observability.slos.slo.item.actions.clone', { + defaultMessage: 'Clone', + })} + , + {i18n.translate('xpack.observability.slos.slo.item.actions.delete', { defaultMessage: 'Delete', })} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx index 560353e483960a..d51a454d002b9c 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx @@ -24,6 +24,8 @@ const defaultProps: Props = { sloList: sloList.results, loading: false, error: false, + onCloned: () => {}, + onCloning: () => {}, onDeleted: () => {}, onDeleting: () => {}, }; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx index 71ba38092e6f72..c911d8ab19774b 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx @@ -17,11 +17,21 @@ export interface Props { sloList: SLOWithSummaryResponse[]; loading: boolean; error: boolean; + onCloned: () => void; + onCloning: () => void; onDeleted: () => void; onDeleting: () => void; } -export function SloListItems({ sloList, loading, error, onDeleted, onDeleting }: Props) { +export function SloListItems({ + sloList, + loading, + error, + onCloned, + onCloning, + onDeleted, + onDeleting, +}: Props) { const [sloIds, setSloIds] = useState([]); useEffect(() => { setSloIds(sloList.map((slo) => slo.id)); @@ -45,6 +55,8 @@ export function SloListItems({ sloList, loading, error, onDeleted, onDeleting }: slo={slo} historicalSummary={historicalSummaryBySlo[slo.id]} historicalSummaryLoading={historicalSummaryLoading} + onCloned={onCloned} + onCloning={onCloning} onDeleted={onDeleted} onDeleting={onDeleting} /> diff --git a/x-pack/plugins/observability/public/pages/slos/index.test.tsx b/x-pack/plugins/observability/public/pages/slos/index.test.tsx index fc2bf1602063e3..0f98521f409ea7 100644 --- a/x-pack/plugins/observability/public/pages/slos/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos/index.test.tsx @@ -8,8 +8,13 @@ import React from 'react'; import { screen } from '@testing-library/react'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; + import { render } from '../../utils/test_helper'; import { useKibana } from '../../utils/kibana_react'; +import { useCreateOrUpdateSlo } from '../../hooks/slo/use_create_slo'; +import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary'; import { useLicense } from '../../hooks/use_license'; @@ -17,7 +22,6 @@ import { SlosPage } from '.'; import { emptySloList, sloList } from '../../data/slo/slo'; import type { ConfigSchema } from '../../plugin'; import type { Subset } from '../../typings'; -import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { historicalSummaryData } from '../../data/slo/historical_summary_data'; jest.mock('react-router-dom', () => ({ @@ -29,14 +33,26 @@ jest.mock('../../utils/kibana_react'); jest.mock('../../hooks/use_breadcrumbs'); jest.mock('../../hooks/use_license'); jest.mock('../../hooks/slo/use_fetch_slo_list'); +jest.mock('../../hooks/slo/use_create_slo'); +jest.mock('../../hooks/slo/use_delete_slo'); jest.mock('../../hooks/slo/use_fetch_historical_summary'); const useKibanaMock = useKibana as jest.Mock; const useLicenseMock = useLicense as jest.Mock; const useFetchSloListMock = useFetchSloList as jest.Mock; +const useCreateOrUpdateSloMock = useCreateOrUpdateSlo as jest.Mock; +const useDeleteSloMock = useDeleteSlo as jest.Mock; const useFetchHistoricalSummaryMock = useFetchHistoricalSummary as jest.Mock; +const mockCreateSlo = jest.fn(); +useCreateOrUpdateSloMock.mockReturnValue({ createSlo: mockCreateSlo }); + +const mockDeleteSlo = jest.fn(); +useDeleteSloMock.mockReturnValue({ deleteSlo: mockDeleteSlo }); + const mockNavigate = jest.fn(); +const mockAddSuccess = jest.fn(); +const mockAddError = jest.fn(); const mockKibana = () => { useKibanaMock.mockReturnValue({ @@ -48,6 +64,12 @@ const mockKibana = () => { prepend: jest.fn(), }, }, + notifications: { + toasts: { + addSuccess: mockAddSuccess, + addError: mockAddError, + }, + }, }, }); }; @@ -90,9 +112,12 @@ describe('SLOs Page', () => { }); describe('when the correct license is found', () => { + beforeEach(() => { + useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); + }); + it('renders nothing when the API is loading', async () => { useFetchSloListMock.mockReturnValue({ loading: true, sloList: emptySloList }); - useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); const { container } = render(, config); @@ -101,16 +126,15 @@ describe('SLOs Page', () => { it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => { useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList }); - useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); render(, config); expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy(); }); - it('renders the SLOs page when the API has finished loading and there are results', async () => { + it('should have a create new SLO button', () => { useFetchSloListMock.mockReturnValue({ loading: false, sloList }); - useLicenseMock.mockReturnValue({ hasAtLeast: () => true }); + useFetchHistoricalSummaryMock.mockReturnValue({ loading: false, data: historicalSummaryData, @@ -118,8 +142,96 @@ describe('SLOs Page', () => { render(, config); - expect(screen.queryByTestId('slosPage')).toBeTruthy(); - expect(screen.queryByTestId('sloList')).toBeTruthy(); + expect(screen.getByText('Create new SLO')).toBeTruthy(); + }); + + describe('when API has returned results', () => { + it('renders the SLO list with SLO items', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList }); + + useFetchHistoricalSummaryMock.mockReturnValue({ + loading: false, + data: historicalSummaryData, + }); + + render(, config); + + expect(screen.queryByTestId('slosPage')).toBeTruthy(); + expect(screen.queryByTestId('sloList')).toBeTruthy(); + expect(screen.queryAllByTestId('sloItem')).toBeTruthy(); + expect(screen.queryAllByTestId('sloItem').length).toBe(sloList.results.length); + }); + + it('allows editing an SLO', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList }); + + useFetchHistoricalSummaryMock.mockReturnValue({ + loading: false, + data: historicalSummaryData, + }); + + render(, config); + + screen.getAllByLabelText('Actions').at(0)?.click(); + + await waitForEuiPopoverOpen(); + + const button = screen.getByTestId('sloActionsEdit'); + + expect(button).toBeTruthy(); + + button.click(); + + expect(mockNavigate).toBeCalled(); + }); + + it('allows deleting an SLO', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList }); + + useFetchHistoricalSummaryMock.mockReturnValue({ + loading: false, + data: historicalSummaryData, + }); + + render(, config); + + screen.getAllByLabelText('Actions').at(0)?.click(); + + await waitForEuiPopoverOpen(); + + const button = screen.getByTestId('sloActionsDelete'); + + expect(button).toBeTruthy(); + + button.click(); + + screen.getByTestId('confirmModalConfirmButton').click(); + + expect(mockDeleteSlo).toBeCalledWith(sloList.results.at(0)?.id); + }); + + it('allows cloning an SLO', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList }); + + useFetchHistoricalSummaryMock.mockReturnValue({ + loading: false, + data: historicalSummaryData, + }); + + render(, config); + + screen.getAllByLabelText('Actions').at(0)?.click(); + + await waitForEuiPopoverOpen(); + + const button = screen.getByTestId('sloActionsClone'); + + expect(button).toBeTruthy(); + + button.click(); + + expect(mockCreateSlo).toBeCalled(); + }); }); }); }); From ea699561f45b42ab8ec98ee92fa1f909200df98e Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 1 Feb 2023 09:13:58 +0200 Subject: [PATCH 26/59] [Lens] Unskips share tests (#149922) ## Summary Closes https://github.com/elastic/kibana/issues/149163 We have seen this before. We should use the Lens wait for chart rendering function and not the global one. This stabilizes the tests Run 100 times https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1827 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/test/functional/apps/lens/group2/share.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/test/functional/apps/lens/group2/share.ts b/x-pack/test/functional/apps/lens/group2/share.ts index e50eb20ee7c3ce..c7cd22974b289e 100644 --- a/x-pack/test/functional/apps/lens/group2/share.ts +++ b/x-pack/test/functional/apps/lens/group2/share.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBarService = getService('filterBar'); const queryBar = getService('queryBar'); - // Failing: See https://github.com/elastic/kibana/issues/149163 - describe.skip('lens share tests', () => { + describe('lens share tests', () => { before(async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); }); @@ -73,7 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.navigateTo(url); // check that it's the same configuration in the new URL when ready - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization('xyVisChart'); expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( 'Average of bytes' ); @@ -93,7 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBarService.addFilter({ field: 'bytes', operation: 'is', value: '1' }); await queryBar.setQuery('host.keyword www.elastic.co'); await queryBar.submitQuery(); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization('xyVisChart'); const url = await PageObjects.lens.getUrl('snapshot'); await browser.openNewTab(); @@ -102,7 +101,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.navigateTo(url); // check that it's the same configuration in the new URL when ready - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization('xyVisChart'); expect(await filterBarService.getFiltersLabel()).to.eql(['bytes: 1']); expect(await queryBar.getQueryString()).to.be('host.keyword www.elastic.co'); await browser.closeCurrentWindow(); From 0085aaea00875e1b1827b1af891fe9981d1f7691 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 1 Feb 2023 09:23:03 +0100 Subject: [PATCH 27/59] [ML] Transforms: Adds date picker to transform wizard for data view with time fields. (#149049) Adds a date picker to the transform wizard for data views with time fields. The time range will be applied to previews only. --- .../routes/timeseriesexplorer.test.tsx | 5 +- .../transform/common/types/date_picker.ts | 11 + .../public/app/common/data_grid.test.ts | 39 ++- .../transform/public/app/common/data_grid.ts | 8 +- .../transform/public/app/common/index.ts | 6 +- .../public/app/common/request.test.ts | 64 ++-- .../transform/public/app/common/request.ts | 76 ++++- .../public/app/hooks/use_index_data.ts | 61 +++- .../use_search_items/use_search_items.ts | 33 +- ...t.ts => use_transform_config_data.test.ts} | 2 +- ...t_data.ts => use_transform_config_data.ts} | 21 +- .../date_picker_apply_switch.tsx | 34 ++ .../date_picker_apply_switch/index.ts | 8 + .../common/get_default_step_define_state.ts | 1 + .../components/step_define/common/types.ts | 10 +- .../step_define/hooks/use_date_picker.ts | 82 +++++ .../step_define/hooks/use_search_bar.ts | 6 +- .../step_define/hooks/use_step_define_form.ts | 14 +- .../step_define/pivot_function_form.tsx | 120 +++++++ .../step_define/step_define_form.test.tsx | 26 +- .../step_define/step_define_form.tsx | 305 ++++++++++-------- .../step_define/step_define_summary.tsx | 48 ++- .../step_details/step_details_form.tsx | 8 +- .../components/wizard/storage.ts | 21 ++ .../components/wizard/wizard.tsx | 34 +- .../expanded_row_preview_pane.tsx | 14 +- x-pack/plugins/transform/tsconfig.json | 4 + x-pack/test/accessibility/apps/transform.ts | 23 ++ .../index_pattern/creation_index_pattern.ts | 23 +- .../creation_runtime_mappings.ts | 13 +- .../creation_saved_search.ts | 11 + .../apps/transform/edit_clone/cloning.ts | 11 + .../feature_controls/transform_security.ts | 10 +- .../services/transform/date_picker.ts | 49 +++ .../functional/services/transform/discover.ts | 27 +- .../functional/services/transform/index.ts | 3 + .../services/transform/navigation.ts | 4 +- .../services/transform/security_ui.ts | 8 +- .../functional/services/transform/wizard.ts | 17 +- 39 files changed, 931 insertions(+), 329 deletions(-) create mode 100644 x-pack/plugins/transform/common/types/date_picker.ts rename x-pack/plugins/transform/public/app/hooks/{use_pivot_data.test.ts => use_transform_config_data.test.ts} (95%) rename x-pack/plugins/transform/public/app/hooks/{use_pivot_data.ts => use_transform_config_data.ts} (95%) create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts create mode 100644 x-pack/test/functional/services/transform/date_picker.ts diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index fcd413b301108a..4b482e2de1ed8a 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -55,6 +55,7 @@ const getMockedTimefilter = () => { enableAutoRefreshSelector: jest.fn(), getRefreshInterval: jest.fn(), setRefreshInterval: jest.fn(), + getActiveBounds: jest.fn(), getTime: jest.fn(), isAutoRefreshSelectorEnabled: jest.fn(), isTimeRangeSelectorEnabled: jest.fn(), @@ -68,7 +69,7 @@ const getMockedTimefilter = () => { }; }; -const getMockedDatePickeDependencies = () => { +const getMockedDatePickerDependencies = () => { return { data: { query: { @@ -138,7 +139,7 @@ describe('TimeSeriesExplorerUrlStateManager', () => { render( - + diff --git a/x-pack/plugins/transform/common/types/date_picker.ts b/x-pack/plugins/transform/common/types/date_picker.ts new file mode 100644 index 00000000000000..09b5d0cba83be6 --- /dev/null +++ b/x-pack/plugins/transform/common/types/date_picker.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 interface TimeRangeMs { + from: number; + to: number; +} diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts index 31f0a2bc688d84..0cb2ed18c58f75 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.test.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { getPreviewTransformRequestBody, SimpleQuery } from '.'; +import type { DataView } from '@kbn/data-views-plugin/common'; -import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; +import { getPreviewTransformRequestBody, SimpleQuery } from '.'; +import { getIndexDevConsoleStatement, getTransformPreviewDevConsoleStatement } from './data_grid'; describe('Transform: Data Grid', () => { - test('getPivotPreviewDevConsoleStatement()', () => { + test('getTransformPreviewDevConsoleStatement()', () => { const query: SimpleQuery = { query_string: { query: '*', @@ -18,26 +19,30 @@ describe('Transform: Data Grid', () => { }, }; - const request = getPreviewTransformRequestBody('the-index-pattern-title', query, { - pivot: { - group_by: { - 'the-group-by-agg-name': { - terms: { - field: 'the-group-by-field', + const request = getPreviewTransformRequestBody( + { getIndexPattern: () => 'the-index-pattern-title' } as DataView, + query, + { + pivot: { + group_by: { + 'the-group-by-agg-name': { + terms: { + field: 'the-group-by-field', + }, }, }, - }, - aggregations: { - 'the-agg-agg-name': { - avg: { - field: 'the-agg-field', + aggregations: { + 'the-agg-agg-name': { + avg: { + field: 'the-agg-field', + }, }, }, }, - }, - }); + } + ); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + const pivotPreviewDevConsoleStatement = getTransformPreviewDevConsoleStatement(request); expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview { diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 43d2b27f13cf97..c6a740787e2b1f 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -7,15 +7,17 @@ import type { PostTransformsPreviewRequestSchema } from '../../../common/api_schemas/transforms'; -import { PivotQuery } from './request'; +import { TransformConfigQuery } from './request'; export const INIT_MAX_COLUMNS = 20; -export const getPivotPreviewDevConsoleStatement = (request: PostTransformsPreviewRequestSchema) => { +export const getTransformPreviewDevConsoleStatement = ( + request: PostTransformsPreviewRequestSchema +) => { return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; -export const getIndexDevConsoleStatement = (query: PivotQuery, dataViewTitle: string) => { +export const getIndexDevConsoleStatement = (query: TransformConfigQuery, dataViewTitle: string) => { return `GET ${dataViewTitle}/_search\n${JSON.stringify( { query, diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index 1f397ee4285ef1..c7656974ec569d 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -8,7 +8,7 @@ export { isAggName } from './aggregations'; export { getIndexDevConsoleStatement, - getPivotPreviewDevConsoleStatement, + getTransformPreviewDevConsoleStatement, INIT_MAX_COLUMNS, } from './data_grid'; export type { EsDoc, EsDocSource } from './fields'; @@ -64,12 +64,12 @@ export { pivotGroupByFieldSupport, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from './pivot_group_by'; -export type { PivotQuery, SimpleQuery } from './request'; +export type { TransformConfigQuery, SimpleQuery } from './request'; export { defaultQuery, getPreviewTransformRequestBody, getCreateTransformRequestBody, - getPivotQuery, + getTransformConfigQuery, getRequestPayload, isDefaultQuery, isMatchAllQuery, diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 2c4415c56c4664..60ad397f6f30a5 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { DataView } from '@kbn/data-views-plugin/common'; + import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; import { PivotGroupByConfig } from '.'; @@ -19,19 +21,19 @@ import { getPreviewTransformRequestBody, getCreateTransformRequestBody, getCreateTransformSettingsRequestBody, - getPivotQuery, + getTransformConfigQuery, getMissingBucketConfig, getRequestPayload, isDefaultQuery, isMatchAllQuery, isSimpleQuery, matchAllQuery, - PivotQuery, + type TransformConfigQuery, } from './request'; import { LatestFunctionConfigUI } from '../../../common/types/transform'; import type { RuntimeField } from '@kbn/data-views-plugin/common'; -const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; +const simpleQuery: TransformConfigQuery = { query_string: { query: 'airline:AAL' } }; const groupByTerms: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, @@ -62,12 +64,12 @@ describe('Transform: Common', () => { test('isDefaultQuery()', () => { expect(isDefaultQuery(defaultQuery)).toBe(true); - expect(isDefaultQuery(matchAllQuery)).toBe(false); + expect(isDefaultQuery(matchAllQuery)).toBe(true); expect(isDefaultQuery(simpleQuery)).toBe(false); }); - test('getPivotQuery()', () => { - const query = getPivotQuery('the-query'); + test('getTransformConfigQuery()', () => { + const query = getTransformConfigQuery('the-query'); expect(query).toEqual({ query_string: { @@ -78,14 +80,18 @@ describe('Transform: Common', () => { }); test('getPreviewTransformRequestBody()', () => { - const query = getPivotQuery('the-query'); + const query = getTransformConfigQuery('the-query'); - const request = getPreviewTransformRequestBody('the-data-view-title', query, { - pivot: { - aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, - group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, - }, - }); + const request = getPreviewTransformRequestBody( + { getIndexPattern: () => 'the-data-view-title' } as DataView, + query, + { + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, + }, + } + ); expect(request).toEqual({ pivot: { @@ -100,13 +106,17 @@ describe('Transform: Common', () => { }); test('getPreviewTransformRequestBody() with comma-separated index pattern', () => { - const query = getPivotQuery('the-query'); - const request = getPreviewTransformRequestBody('the-data-view-title,the-other-title', query, { - pivot: { - aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, - group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, - }, - }); + const query = getTransformConfigQuery('the-query'); + const request = getPreviewTransformRequestBody( + { getIndexPattern: () => 'the-data-view-title,the-other-title' } as DataView, + query, + { + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, + }, + } + ); expect(request).toEqual({ pivot: { @@ -172,9 +182,9 @@ describe('Transform: Common', () => { }); test('getPreviewTransformRequestBody() with missing_buckets config', () => { - const query = getPivotQuery('the-query'); + const query = getTransformConfigQuery('the-query'); const request = getPreviewTransformRequestBody( - 'the-data-view-title', + { getIndexPattern: () => 'the-data-view-title' } as DataView, query, getRequestPayload([aggsAvg], [{ ...groupByTerms, ...{ missing_bucket: true } }]) ); @@ -194,11 +204,12 @@ describe('Transform: Common', () => { }); test('getCreateTransformRequestBody() skips default values', () => { - const pivotState: StepDefineExposedState = { + const transformConfigState: StepDefineExposedState = { aggList: { 'the-agg-name': aggsAvg }, groupByList: { 'the-group-by-name': groupByTerms }, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, + isDatePickerApplyEnabled: false, sourceConfigUpdated: false, searchLanguage: 'kuery', searchString: 'the-query', @@ -239,8 +250,8 @@ describe('Transform: Common', () => { }; const request = getCreateTransformRequestBody( - 'the-data-view-title', - pivotState, + { getIndexPattern: () => 'the-data-view-title' } as DataView, + transformConfigState, transformDetailsState ); @@ -278,6 +289,7 @@ describe('Transform: Common', () => { groupByList: { 'the-group-by-name': groupByTerms }, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, + isDatePickerApplyEnabled: false, sourceConfigUpdated: false, searchLanguage: 'kuery', searchString: 'the-query', @@ -319,7 +331,7 @@ describe('Transform: Common', () => { }; const request = getCreateTransformRequestBody( - 'the-data-view-title', + { getIndexPattern: () => 'the-data-view-title' } as DataView, pivotState, transformDetailsState ); diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index c66618df209f7a..42162498f3f3cb 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -8,6 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { DataView } from '@kbn/data-views-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { buildBaseFilterCriteria } from '@kbn/ml-query-utils'; import { DEFAULT_CONTINUOUS_MODE_DELAY, @@ -47,9 +48,15 @@ export interface SimpleQuery { }; } -export type PivotQuery = SimpleQuery | SavedSearchQuery; +export interface FilterBasedSimpleQuery { + bool: { + filter: [SimpleQuery]; + }; +} + +export type TransformConfigQuery = FilterBasedSimpleQuery | SimpleQuery | SavedSearchQuery; -export function getPivotQuery(search: string | SavedSearchQuery): PivotQuery { +export function getTransformConfigQuery(search: string | SavedSearchQuery): TransformConfigQuery { if (typeof search === 'string') { return { query_string: { @@ -66,6 +73,16 @@ export function isSimpleQuery(arg: unknown): arg is SimpleQuery { return isPopulatedObject(arg, ['query_string']); } +export function isFilterBasedSimpleQuery(arg: unknown): arg is FilterBasedSimpleQuery { + return ( + isPopulatedObject(arg, ['bool']) && + isPopulatedObject(arg.bool, ['filter']) && + Array.isArray(arg.bool.filter) && + arg.bool.filter.length === 1 && + isSimpleQuery(arg.bool.filter[0]) + ); +} + export const matchAllQuery = { match_all: {} }; export function isMatchAllQuery(query: unknown): boolean { return ( @@ -76,9 +93,14 @@ export function isMatchAllQuery(query: unknown): boolean { ); } -export const defaultQuery: PivotQuery = { query_string: { query: '*' } }; -export function isDefaultQuery(query: PivotQuery): boolean { - return isSimpleQuery(query) && query.query_string.query === '*'; +export const defaultQuery: TransformConfigQuery = { query_string: { query: '*' } }; +export function isDefaultQuery(query: TransformConfigQuery): boolean { + return ( + isMatchAllQuery(query) || + (isSimpleQuery(query) && query.query_string.query === '*') || + (isFilterBasedSimpleQuery(query) && + (query.bool.filter[0].query_string.query === '*' || isMatchAllQuery(query.bool.filter[0]))) + ); } export function getCombinedRuntimeMappings( @@ -171,17 +193,36 @@ export const getRequestPayload = ( }; export function getPreviewTransformRequestBody( - dataViewTitle: DataView['title'], - query: PivotQuery, - partialRequest?: StepDefineExposedState['previewRequest'] | undefined, - runtimeMappings?: StepDefineExposedState['runtimeMappings'] + dataView: DataView, + transformConfigQuery: TransformConfigQuery, + partialRequest?: StepDefineExposedState['previewRequest'], + runtimeMappings?: StepDefineExposedState['runtimeMappings'], + timeRangeMs?: StepDefineExposedState['timeRangeMs'] ): PostTransformsPreviewRequestSchema { + const dataViewTitle = dataView.getIndexPattern(); const index = dataViewTitle.split(',').map((name: string) => name.trim()); + const hasValidTimeField = dataView.timeFieldName !== undefined && dataView.timeFieldName !== ''; + + const baseFilterCriteria = buildBaseFilterCriteria( + dataView.timeFieldName, + timeRangeMs?.from, + timeRangeMs?.to, + isDefaultQuery(transformConfigQuery) ? undefined : transformConfigQuery + ); + + const queryWithBaseFilterCriteria = { + bool: { + filter: baseFilterCriteria, + }, + }; + + const query = hasValidTimeField ? queryWithBaseFilterCriteria : transformConfigQuery; + return { source: { index, - ...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}), + ...(isDefaultQuery(query) ? {} : { query }), ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), }, ...(partialRequest ?? {}), @@ -212,15 +253,18 @@ export const getCreateTransformSettingsRequestBody = ( }; export const getCreateTransformRequestBody = ( - dataViewTitle: DataView['title'], - pivotState: StepDefineExposedState, + dataView: DataView, + transformConfigState: StepDefineExposedState, transformDetailsState: StepDetailsExposedState ): PutTransformsPivotRequestSchema | PutTransformsLatestRequestSchema => ({ ...getPreviewTransformRequestBody( - dataViewTitle, - getPivotQuery(pivotState.searchQuery), - pivotState.previewRequest, - pivotState.runtimeMappings + dataView, + getTransformConfigQuery(transformConfigState.searchQuery), + transformConfigState.previewRequest, + transformConfigState.runtimeMappings, + transformConfigState.isDatePickerApplyEnabled && transformConfigState.timeRangeMs + ? transformConfigState.timeRangeMs + : undefined ), // conditionally add optional description ...(transformDetailsState.transformDescription !== '' diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 22c314c89850a4..d4fbf1c77d0543 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -10,6 +10,9 @@ import { useEffect, useMemo, useState } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { EuiDataGridColumn } from '@elastic/eui'; +import { buildBaseFilterCriteria } from '@kbn/ml-query-utils'; + +import type { TimeRangeMs } from '../../../common/types/date_picker'; import { isEsSearchResponse, isFieldHistogramsResponseSchema, @@ -19,21 +22,23 @@ import { isKeywordDuplicate, removeKeywordPostfix, } from '../../../common/utils/field_utils'; -import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports'; - import { getErrorMessage } from '../../../common/utils/errors'; -import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; -import { SearchItems } from './use_search_items'; -import { useApi } from './use_api'; +import { isRuntimeMappings } from '../../../common/shared_imports'; + +import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports'; +import { isDefaultQuery, matchAllQuery, TransformConfigQuery } from '../common'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common'; -import { isRuntimeMappings } from '../../../common/shared_imports'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; export const useIndexData = ( dataView: SearchItems['dataView'], - query: PivotQuery, - combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'] + query: TransformConfigQuery, + combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'], + timeRangeMs?: TimeRangeMs ): UseIndexDataReturnType => { const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]); @@ -55,6 +60,24 @@ export const useIndexData = ( const [dataViewFields, setDataViewFields] = useState(); + const baseFilterCriteria = buildBaseFilterCriteria( + dataView.timeFieldName, + timeRangeMs?.from, + timeRangeMs?.to, + query + ); + + const defaultQuery = useMemo( + () => (timeRangeMs && dataView.timeFieldName ? baseFilterCriteria[0] : matchAllQuery), + [baseFilterCriteria, dataView, timeRangeMs] + ); + + const queryWithBaseFilterCriteria = { + bool: { + filter: baseFilterCriteria, + }, + }; + // Fetch 500 random documents to determine populated fields. // This is a workaround to avoid passing potentially thousands of unpopulated fields // (for example, as part of filebeat/metricbeat/ECS based indices) @@ -70,7 +93,7 @@ export const useIndexData = ( _source: false, query: { function_score: { - query: { match_all: {} }, + query: defaultQuery, random_score: {}, }, }, @@ -106,7 +129,7 @@ export const useIndexData = ( useEffect(() => { fetchDataGridSampleDocuments(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [timeRangeMs]); const columns: EuiDataGridColumn[] = useMemo(() => { if (typeof dataViewFields === 'undefined') { @@ -165,7 +188,7 @@ export const useIndexData = ( resetPagination(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(query)]); + }, [JSON.stringify([query, timeRangeMs])]); const fetchDataGridData = async function () { setErrorMessage(''); @@ -181,8 +204,7 @@ export const useIndexData = ( body: { fields: ['*'], _source: false, - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - query: isDefaultQuery(query) ? matchAllQuery : query, + query: isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, from: pagination.pageIndex * pagination.pageSize, size: pagination.pageSize, ...(Object.keys(sort).length > 0 ? { sort } : {}), @@ -236,7 +258,7 @@ export const useIndexData = ( type: getFieldType(cT.schema), }; }), - isDefaultQuery(query) ? matchAllQuery : query, + isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, combinedRuntimeMappings ); @@ -263,7 +285,14 @@ export const useIndexData = ( }, [ indexPattern, // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify([query, pagination, sortingColumns, dataViewFields, combinedRuntimeMappings]), + JSON.stringify([ + query, + pagination, + sortingColumns, + dataViewFields, + combinedRuntimeMappings, + timeRangeMs, + ]), ]); useEffect(() => { @@ -276,7 +305,7 @@ export const useIndexData = ( chartsVisible, indexPattern, // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings]), + JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings, timeRangeMs]), ]); const renderCellValue = useRenderCellValue(dataView, pagination, tableItems); diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index cd24d092f754c9..51e6fcaf469e75 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -27,6 +27,13 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const [searchItems, setSearchItems] = useState(undefined); + const isMounted = useRef(true); + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + async function fetchSavedObject(id: string) { let fetchedDataView; let fetchedSavedSearch; @@ -44,7 +51,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { spaces: appDeps.spaces, }); - if (fetchedSavedSearch?.sharingSavedObjectProps?.errorJSON) { + if (isMounted.current && fetchedSavedSearch?.sharingSavedObjectProps?.errorJSON) { setError(await getSavedSearchUrlConflictMessage(fetchedSavedSearch)); return; } @@ -52,17 +59,19 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } - if (!isDataView(fetchedDataView) && fetchedSavedSearch === undefined) { - setError( - i18n.translate('xpack.transform.searchItems.errorInitializationTitle', { - defaultMessage: `An error occurred initializing the Kibana data view or saved search.`, - }) - ); - return; - } + if (isMounted.current) { + if (!isDataView(fetchedDataView) && fetchedSavedSearch === undefined) { + setError( + i18n.translate('xpack.transform.searchItems.errorInitializationTitle', { + defaultMessage: `An error occurred initializing the Kibana data view or saved search.`, + }) + ); + return; + } - setSearchItems(createSearchItems(fetchedDataView, fetchedSavedSearch, uiSettings)); - setError(undefined); + setSearchItems(createSearchItems(fetchedDataView, fetchedSavedSearch, uiSettings)); + setError(undefined); + } } useEffect(() => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.test.ts similarity index 95% rename from x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts rename to x-pack/plugins/transform/public/app/hooks/use_transform_config_data.test.ts index 6c354c1ed953e1..1ee68bdcb48fa1 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getCombinedProperties } from './use_pivot_data'; +import { getCombinedProperties } from './use_transform_config_data'; import { ES_FIELD_TYPES } from '@kbn/field-types'; describe('getCombinedProperties', () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts similarity index 95% rename from x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts rename to x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts index 79976eb6d63556..1baef3e7fe8d37 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts @@ -27,7 +27,7 @@ import { import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies } from '../app_dependencies'; -import { getPreviewTransformRequestBody, PivotQuery } from '../common'; +import { getPreviewTransformRequestBody, type TransformConfigQuery } from '../common'; import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; @@ -95,12 +95,13 @@ export function getCombinedProperties( }; } -export const usePivotData = ( - dataViewTitle: SearchItems['dataView']['title'], - query: PivotQuery, +export const useTransformConfigData = ( + dataView: SearchItems['dataView'], + query: TransformConfigQuery, validationStatus: StepDefineExposedState['validationStatus'], requestPayload: StepDefineExposedState['previewRequest'], - combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'] + combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'], + timeRangeMs?: StepDefineExposedState['timeRangeMs'] ): UseIndexDataReturnType => { const [previewMappingsProperties, setPreviewMappingsProperties] = useState({}); @@ -166,10 +167,11 @@ export const usePivotData = ( setStatus(INDEX_STATUS.LOADING); const previewRequest = getPreviewTransformRequestBody( - dataViewTitle, + dataView, query, requestPayload, - combinedRuntimeMappings + combinedRuntimeMappings, + timeRangeMs ); const resp = await api.getTransformsPreview(previewRequest); @@ -238,7 +240,10 @@ export const usePivotData = ( getPreviewData(); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ - }, [dataViewTitle, JSON.stringify([requestPayload, query, combinedRuntimeMappings])]); + }, [ + dataView.getIndexPattern(), + JSON.stringify([requestPayload, query, combinedRuntimeMappings, timeRangeMs]), + ]); if (sortingColumns.length > 0) { const sortingColumnsWithTypes = sortingColumns.map((c) => ({ diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx new file mode 100644 index 00000000000000..a57d83b75aa10a --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +import { EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { StepDefineFormHook } from '../step_define'; + +export const DatePickerApplySwitch: FC = ({ + datePicker: { + actions: { setDatePickerApplyEnabled }, + state: { isDatePickerApplyEnabled }, + }, +}) => { + return ( + { + setDatePickerApplyEnabled(!isDatePickerApplyEnabled); + }} + data-test-subj="transformDatePickerApplySwitch" + /> + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts new file mode 100644 index 00000000000000..cc1760017caf8f --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/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 { DatePickerApplySwitch } from './date_picker_apply_switch'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts index c75da651f79d0d..7e6d336d57a4bb 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts @@ -21,6 +21,7 @@ export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineE groupByList: {} as PivotGroupByConfigDict, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, + isDatePickerApplyEnabled: false, searchLanguage: QUERY_LANGUAGE_KUERY, searchString: undefined, searchQuery: searchItems.savedSearch !== undefined ? searchItems.combinedQuery : defaultSearch, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index c8dc63cae1f9ae..2e23cc0e9047fd 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -24,8 +24,8 @@ import { PivotConfigDefinition, } from '../../../../../../../common/types/transform'; import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; - import { RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; +import type { TimeRangeMs } from '../../../../../../../common/types/date_picker'; export interface ErrorMessage { query: string; @@ -62,13 +62,15 @@ export interface StepDefineExposedState { sourceConfigUpdated: boolean; valid: boolean; validationStatus: { isValid: boolean; errorMessage?: string }; + runtimeMappings?: RuntimeMappings; + runtimeMappingsUpdated: boolean; + isRuntimeMappingsEditorEnabled: boolean; + timeRangeMs?: TimeRangeMs; + isDatePickerApplyEnabled: boolean; /** * Undefined when the form is incomplete or invalid */ previewRequest: { latest: LatestFunctionConfig } | { pivot: PivotConfigDefinition } | undefined; - runtimeMappings?: RuntimeMappings; - runtimeMappingsUpdated: boolean; - isRuntimeMappingsEditorEnabled: boolean; } export function isPivotPartialRequest(arg: unknown): arg is { pivot: PivotConfigDefinition } { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts new file mode 100644 index 00000000000000..64a825e4242203 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useState } from 'react'; +import { merge } from 'rxjs'; + +import type { TimeRange } from '@kbn/es-query'; +import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; + +import type { TimeRangeMs } from '../../../../../../../common/types/date_picker'; + +import { StepDefineExposedState } from '../common'; +import { StepDefineFormProps } from '../step_define_form'; + +export const useDatePicker = ( + defaults: StepDefineExposedState, + dataView: StepDefineFormProps['searchItems']['dataView'] +) => { + const hasValidTimeField = useMemo( + () => dataView.timeFieldName !== undefined && dataView.timeFieldName !== '', + [dataView.timeFieldName] + ); + + const timefilter = useTimefilter({ + timeRangeSelector: hasValidTimeField, + autoRefreshSelector: false, + }); + + // The internal state of the date picker apply button. + const [isDatePickerApplyEnabled, setDatePickerApplyEnabled] = useState( + defaults.isDatePickerApplyEnabled + ); + + // The time range selected via the date picker + const [timeRange, setTimeRange] = useState(); + + // Set up subscriptions to date picker updates + useEffect(() => { + const updateTimeRange = () => setTimeRange(timefilter.getTime()); + + const timefilterUpdateSubscription = merge( + timefilter.getAutoRefreshFetch$(), + timefilter.getTimeUpdate$(), + mlTimefilterRefresh$ + ).subscribe(updateTimeRange); + + const timefilterEnabledSubscription = timefilter + .getEnabledUpdated$() + .subscribe(updateTimeRange); + + return () => { + timefilterUpdateSubscription.unsubscribe(); + timefilterEnabledSubscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Derive ms timestamps from timeRange updates. + const timeRangeMs: TimeRangeMs | undefined = useMemo(() => { + const timefilterActiveBounds = timefilter.getActiveBounds(); + if ( + timefilterActiveBounds !== undefined && + timefilterActiveBounds.min !== undefined && + timefilterActiveBounds.max !== undefined + ) { + return { + from: timefilterActiveBounds.min.valueOf(), + to: timefilterActiveBounds.max.valueOf(), + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timeRange]); + + return { + actions: { setDatePickerApplyEnabled }, + state: { isDatePickerApplyEnabled, hasValidTimeField, timeRange, timeRangeMs }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts index 4ea56b557c7eee..e8d56fc0029814 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts @@ -10,7 +10,7 @@ import { useState } from 'react'; import { toElasticsearchQuery, fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; import type { Query } from '@kbn/es-query'; -import { getPivotQuery } from '../../../../../common'; +import { getTransformConfigQuery } from '../../../../../common'; import { ErrorMessage, @@ -65,7 +65,7 @@ export const useSearchBar = ( } }; - const pivotQuery = getPivotQuery(searchQuery); + const transformConfigQuery = getTransformConfigQuery(searchQuery); return { actions: { @@ -79,7 +79,7 @@ export const useSearchBar = ( }, state: { errorMessage, - pivotQuery, + transformConfigQuery, searchInput, searchLanguage, searchQuery, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts index 1cd0a154707b32..849883e6c3041b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts @@ -15,6 +15,7 @@ import { StepDefineFormProps } from '../step_define_form'; import { useAdvancedPivotEditor } from './use_advanced_pivot_editor'; import { useAdvancedSourceEditor } from './use_advanced_source_editor'; +import { useDatePicker } from './use_date_picker'; import { usePivotConfig } from './use_pivot_config'; import { useSearchBar } from './use_search_bar'; import { useLatestFunctionConfig } from './use_latest_function_config'; @@ -29,6 +30,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const [transformFunction, setTransformFunction] = useState(defaults.transformFunction); + const datePicker = useDatePicker(defaults, dataView); const searchBar = useSearchBar(defaults, dataView); const pivotConfig = usePivotConfig(defaults, dataView); @@ -39,8 +41,8 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi ); const previewRequest = getPreviewTransformRequestBody( - dataView.getIndexPattern(), - searchBar.state.pivotQuery, + dataView, + searchBar.state.transformConfigQuery, pivotConfig.state.requestPayload, defaults?.runtimeMappings ); @@ -58,8 +60,8 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const runtimeMappings = runtimeMappingsEditor.state.runtimeMappings; if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) { const previewRequestUpdate = getPreviewTransformRequestBody( - dataView.getIndexPattern(), - searchBar.state.pivotQuery, + dataView, + searchBar.state.transformConfigQuery, pivotConfig.state.requestPayload, runtimeMappings ); @@ -79,6 +81,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi groupByList: pivotConfig.state.groupByList, isAdvancedPivotEditorEnabled: advancedPivotEditor.state.isAdvancedPivotEditorEnabled, isAdvancedSourceEditorEnabled: advancedSourceEditor.state.isAdvancedSourceEditorEnabled, + isDatePickerApplyEnabled: datePicker.state.isDatePickerApplyEnabled, searchLanguage: searchBar.state.searchLanguage, searchString: searchBar.state.searchString, searchQuery: searchBar.state.searchQuery, @@ -98,12 +101,14 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi runtimeMappings, runtimeMappingsUpdated: runtimeMappingsEditor.state.runtimeMappingsUpdated, isRuntimeMappingsEditorEnabled: runtimeMappingsEditor.state.isRuntimeMappingsEditorEnabled, + timeRangeMs: datePicker.state.timeRangeMs, }); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ }, [ JSON.stringify(advancedPivotEditor.state), JSON.stringify(advancedSourceEditor.state), + JSON.stringify(datePicker.state), pivotConfig.state, JSON.stringify(searchBar.state), JSON.stringify([ @@ -121,6 +126,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi advancedPivotEditor, advancedSourceEditor, runtimeMappingsEditor, + datePicker, pivotConfig, latestFunctionConfig, searchBar, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx new file mode 100644 index 00000000000000..efa28de596a180 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx @@ -0,0 +1,120 @@ +/* + * 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, { FC } from 'react'; + +import { + EuiButton, + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; + +import { AdvancedPivotEditor } from '../advanced_pivot_editor'; +import { AdvancedPivotEditorSwitch } from '../advanced_pivot_editor_switch'; +import { PivotConfiguration } from '../pivot_configuration'; + +import type { StepDefineFormHook } from './hooks/use_step_define_form'; + +const advancedEditorsSidebarWidth = '220px'; + +interface PivotFunctionFormProps { + applyPivotChangesHandler: () => void; + copyToClipboardPivot: string; + copyToClipboardPivotDescription: string; + stepDefineForm: StepDefineFormHook; +} + +export const PivotFunctionForm: FC = ({ + applyPivotChangesHandler, + copyToClipboardPivot, + copyToClipboardPivotDescription, + stepDefineForm, +}) => { + const { esTransformPivot } = useDocumentationLinks(); + + const { isAdvancedPivotEditorEnabled, isAdvancedPivotEditorApplyButtonEnabled } = + stepDefineForm.advancedPivotEditor.state; + + return ( + + {/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */} + + {!isAdvancedPivotEditorEnabled && } + {isAdvancedPivotEditorEnabled && ( + + )} + + + + + + + + + + + + {(copy: () => void) => ( + + )} + + + + + + {isAdvancedPivotEditorEnabled && ( + + + + <> + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { + defaultMessage: + 'The advanced editor allows you to edit the pivot configuration of the transform.', + })}{' '} + + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { + defaultMessage: 'Learn more about available options.', + })} + + + + + + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorApplyButtonText', { + defaultMessage: 'Apply changes', + })} + + + )} + + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 682d1bde11c326..beb40203784097 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -9,12 +9,11 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n-react'; - +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - import { coreMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -const startMock = coreMock.createStart(); +import { timefilterServiceMock } from '@kbn/data-plugin/public/query/timefilter/timefilter_service.mock'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -28,11 +27,24 @@ import { SearchItems } from '../../../../hooks/use_search_items'; import { getAggNameConflictToastMessages } from './common'; import { StepDefineForm } from './step_define_form'; +import { MlSharedContext } from '../../../../__mocks__/shared_context'; +import { getMlSharedImports } from '../../../../../shared_imports'; + jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); -import { MlSharedContext } from '../../../../__mocks__/shared_context'; -import { getMlSharedImports } from '../../../../../shared_imports'; +const startMock = coreMock.createStart(); + +const getMockedDatePickerDependencies = () => { + return { + data: { + query: { + timefilter: timefilterServiceMock.createStartContract(), + }, + }, + notifications: {}, + } as unknown as DatePickerDependencies; +}; const createMockWebStorage = () => ({ clear: jest.fn(), @@ -75,7 +87,9 @@ describe('Transform: ', () => { - + + + diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index f86d693e605e24..c615e553b89843 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { useMemo, FC } from 'react'; - -import { i18n } from '@kbn/i18n'; +import React, { useEffect, useMemo, FC } from 'react'; +import { merge } from 'rxjs'; import { EuiButton, @@ -17,20 +16,25 @@ import { EuiFlexItem, EuiForm, EuiFormRow, - EuiHorizontalRule, + EuiIconTip, EuiLink, EuiSpacer, EuiText, + EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { mlTimefilterRefresh$, useTimefilter, DatePickerWrapper } from '@kbn/ml-date-picker'; +import { useUrlState } from '@kbn/ml-url-state'; + import { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; import { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; +import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; import { getIndexDevConsoleStatement, - getPivotPreviewDevConsoleStatement, + getTransformPreviewDevConsoleStatement, } from '../../../../common/data_grid'; - import { getPreviewTransformRequestBody, PivotAggsConfigDict, @@ -40,24 +44,36 @@ import { } from '../../../../common'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { useIndexData } from '../../../../hooks/use_index_data'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { SearchItems } from '../../../../hooks/use_search_items'; +import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; -import { AdvancedPivotEditor } from '../advanced_pivot_editor'; -import { AdvancedPivotEditorSwitch } from '../advanced_pivot_editor_switch'; import { AdvancedQueryEditorSwitch } from '../advanced_query_editor_switch'; import { AdvancedSourceEditor } from '../advanced_source_editor'; -import { PivotConfiguration } from '../pivot_configuration'; +import { DatePickerApplySwitch } from '../date_picker_apply_switch'; import { SourceSearchBar } from '../source_search_bar'; +import { AdvancedRuntimeMappingsSettings } from '../advanced_runtime_mappings_settings'; import { StepDefineExposedState } from './common'; import { useStepDefineForm } from './hooks/use_step_define_form'; -import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; import { TransformFunctionSelector } from './transform_function_selector'; -import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; import { LatestFunctionForm } from './latest_function_form'; -import { AdvancedRuntimeMappingsSettings } from '../advanced_runtime_mappings_settings'; +import { PivotFunctionForm } from './pivot_function_form'; + +const ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG = false; + +const advancedEditorsSidebarWidth = '220px'; + +export const ConfigSectionTitle: FC<{ title: string }> = ({ title }) => ( + <> + + + {title} + + + +); export interface StepDefineFormProps { overrides?: StepDefineExposedState; @@ -66,6 +82,7 @@ export interface StepDefineFormProps { } export const StepDefineForm: FC = React.memo((props) => { + const [globalState, setGlobalState] = useUrlState('_g'); const { searchItems } = props; const { dataView } = searchItems; const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]); @@ -75,24 +92,18 @@ export const StepDefineForm: FC = React.memo((props) => { const toastNotifications = useToastNotifications(); const stepDefineForm = useStepDefineForm(props); - const { - advancedEditorConfig, - isAdvancedPivotEditorEnabled, - isAdvancedPivotEditorApplyButtonEnabled, - } = stepDefineForm.advancedPivotEditor.state; + const { advancedEditorConfig } = stepDefineForm.advancedPivotEditor.state; const { advancedEditorSourceConfig, isAdvancedSourceEditorEnabled, isAdvancedSourceEditorApplyButtonEnabled, } = stepDefineForm.advancedSourceEditor.state; - const pivotQuery = stepDefineForm.searchBar.state.pivotQuery; + const { isDatePickerApplyEnabled, timeRangeMs } = stepDefineForm.datePicker.state; + const { transformConfigQuery } = stepDefineForm.searchBar.state; + const { runtimeMappings } = stepDefineForm.runtimeMappingsEditor.state; const indexPreviewProps = { - ...useIndexData( - dataView, - stepDefineForm.searchBar.state.pivotQuery, - stepDefineForm.runtimeMappingsEditor.state.runtimeMappings - ), + ...useIndexData(dataView, transformConfigQuery, runtimeMappings, timeRangeMs), dataTestSubj: 'transformIndexPreview', toastNotifications, }; @@ -101,16 +112,7 @@ export const StepDefineForm: FC = React.memo((props) => { ? stepDefineForm.pivotConfig.state : stepDefineForm.latestFunctionConfig; - const previewRequest = getPreviewTransformRequestBody( - indexPattern, - pivotQuery, - stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT - ? stepDefineForm.pivotConfig.state.requestPayload - : stepDefineForm.latestFunctionConfig.requestPayload, - stepDefineForm.runtimeMappingsEditor.state.runtimeMappings - ); - - const copyToClipboardSource = getIndexDevConsoleStatement(pivotQuery, indexPattern); + const copyToClipboardSource = getIndexDevConsoleStatement(transformConfigQuery, indexPattern); const copyToClipboardSourceDescription = i18n.translate( 'xpack.transform.indexPreview.copyClipboardTooltip', { @@ -118,7 +120,17 @@ export const StepDefineForm: FC = React.memo((props) => { } ); - const copyToClipboardPivot = getPivotPreviewDevConsoleStatement(previewRequest); + const copyToClipboardPreviewRequest = getPreviewTransformRequestBody( + dataView, + transformConfigQuery, + requestPayload, + runtimeMappings, + isDatePickerApplyEnabled ? timeRangeMs : undefined + ); + + const copyToClipboardPivot = getTransformPreviewDevConsoleStatement( + copyToClipboardPreviewRequest + ); const copyToClipboardPivotDescription = i18n.translate( 'xpack.transform.pivotPreview.copyClipboardTooltip', { @@ -126,18 +138,16 @@ export const StepDefineForm: FC = React.memo((props) => { } ); - const pivotPreviewProps = { - ...usePivotData( - indexPattern, - pivotQuery, + const previewProps = { + ...useTransformConfigData( + dataView, + transformConfigQuery, validationStatus, requestPayload, - stepDefineForm.runtimeMappingsEditor.state.runtimeMappings + runtimeMappings, + timeRangeMs ), dataTestSubj: 'transformPivotPreview', - title: i18n.translate('xpack.transform.pivotPreview.transformPreviewTitle', { - defaultMessage: 'Transform preview', - }), toastNotifications, ...(stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? { @@ -192,9 +202,52 @@ export const StepDefineForm: FC = React.memo((props) => { stepDefineForm.advancedPivotEditor.actions.setAdvancedPivotEditorApplyButtonEnabled(false); }; - const { esQueryDsl, esTransformPivot } = useDocumentationLinks(); + const { esQueryDsl } = useDocumentationLinks(); + + const hasValidTimeField = useMemo( + () => dataView.timeFieldName !== undefined && dataView.timeFieldName !== '', + [dataView.timeFieldName] + ); + + const timefilter = useTimefilter({ + timeRangeSelector: dataView?.timeFieldName !== undefined, + autoRefreshSelector: false, + }); + + useEffect(() => { + if (globalState?.time !== undefined) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.time), timefilter]); + + useEffect(() => { + if (globalState?.refreshInterval !== undefined) { + timefilter.setRefreshInterval(globalState.refreshInterval); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.refreshInterval), timefilter]); - const advancedEditorsSidebarWidth = '220px'; + useEffect(() => { + const timeUpdateSubscription = merge( + timefilter.getAutoRefreshFetch$(), + timefilter.getTimeUpdate$(), + mlTimefilterRefresh$ + ).subscribe(() => { + if (setGlobalState) { + setGlobalState({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }); + } + }); + return () => { + timeUpdateSubscription.unsubscribe(); + }; + }); return (

@@ -206,6 +259,8 @@ export const StepDefineForm: FC = React.memo((props) => { /> + + {searchItems.savedSearch === undefined && ( = React.memo((props) => { )} + {hasValidTimeField && ( + + {i18n.translate('xpack.transform.stepDefineForm.datePickerLabel', { + defaultMessage: 'Time range', + })}{' '} + + + } + > + + {/* Flex Column #1: Date Picker */} + + + + {/* Flex Column #2: Apply-To-Config option */} + + {ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG && ( + + + {searchItems.savedSearch === undefined && ( + + )} + + + )} + + + + )} + <> @@ -314,87 +415,30 @@ export const StepDefineForm: FC = React.memo((props) => { - + + + - + + + {stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? ( - - {/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */} - - {!isAdvancedPivotEditorEnabled && ( - - )} - {isAdvancedPivotEditorEnabled && ( - - )} - - - - - - - - - - - - {(copy: () => void) => ( - - )} - - - - - - {isAdvancedPivotEditorEnabled && ( - - - - <> - {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { - defaultMessage: - 'The advanced editor allows you to edit the pivot configuration of the transform.', - })}{' '} - - {i18n.translate( - 'xpack.transform.stepDefineForm.advancedEditorHelpTextLink', - { - defaultMessage: 'Learn more about available options.', - } - )} - - - - - - {i18n.translate( - 'xpack.transform.stepDefineForm.advancedEditorApplyButtonText', - { - defaultMessage: 'Apply changes', - } - )} - - - )} - - - + ) : null} {stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? ( = React.memo((props) => { {(stepDefineForm.transformFunction !== TRANSFORM_FUNCTION.LATEST || stepDefineForm.latestFunctionConfig.sortFieldOptions.length > 0) && ( - <> - - - + + <> + + + + )}
); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 140d58523b38a2..630e6278dcebad 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -14,13 +14,13 @@ import { EuiBadge, EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer, EuiText } from import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { - getPivotQuery, - getPivotPreviewDevConsoleStatement, + getTransformConfigQuery, + getTransformPreviewDevConsoleStatement, getPreviewTransformRequestBody, isDefaultQuery, isMatchAllQuery, } from '../../../../common'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -37,6 +37,8 @@ interface Props { export const StepDefineSummary: FC = ({ formState: { + isDatePickerApplyEnabled, + timeRangeMs, runtimeMappings, searchString, searchQuery, @@ -49,31 +51,33 @@ export const StepDefineSummary: FC = ({ searchItems, }) => { const { - ml: { DataGrid }, + ml: { formatHumanReadableDateTimeSeconds, DataGrid }, } = useAppDependencies(); const toastNotifications = useToastNotifications(); - const pivotQuery = getPivotQuery(searchQuery); + const transformConfigQuery = getTransformConfigQuery(searchQuery); const previewRequest = getPreviewTransformRequestBody( - searchItems.dataView.getIndexPattern(), - pivotQuery, + searchItems.dataView, + transformConfigQuery, partialPreviewRequest, - runtimeMappings + runtimeMappings, + isDatePickerApplyEnabled ? timeRangeMs : undefined ); - const pivotPreviewProps = usePivotData( - searchItems.dataView.getIndexPattern(), - pivotQuery, + const pivotPreviewProps = useTransformConfigData( + searchItems.dataView, + transformConfigQuery, validationStatus, partialPreviewRequest, - runtimeMappings + runtimeMappings, + isDatePickerApplyEnabled ? timeRangeMs : undefined ); const isModifiedQuery = typeof searchString === 'undefined' && - !isDefaultQuery(pivotQuery) && - !isMatchAllQuery(pivotQuery); + !isDefaultQuery(transformConfigQuery) && + !isMatchAllQuery(transformConfigQuery); let uniqueKeys: string[] = []; let sortField = ''; @@ -94,6 +98,18 @@ export const StepDefineSummary: FC = ({ > {searchItems.dataView.getIndexPattern()} + {isDatePickerApplyEnabled && timeRangeMs && ( + + + {formatHumanReadableDateTimeSeconds(timeRangeMs.from)} -{' '} + {formatHumanReadableDateTimeSeconds(timeRangeMs.to)} + + + )} {typeof searchString === 'string' && ( = ({ overflowHeight={300} isCopyable > - {JSON.stringify(pivotQuery, null, 2)} + {JSON.stringify(transformConfigQuery, null, 2)} )} @@ -187,7 +203,7 @@ export const StepDefineSummary: FC = ({ = React.memo( // use an IIFE to avoid returning a Promise to useEffect. (async function () { const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState; - const pivotQuery = getPivotQuery(searchQuery); + const transformConfigQuery = getTransformConfigQuery(searchQuery); const previewRequest = getPreviewTransformRequestBody( - searchItems.dataView.getIndexPattern(), - pivotQuery, + searchItems.dataView, + transformConfigQuery, partialPreviewRequest, stepDefineState.runtimeMappings ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts new file mode 100644 index 00000000000000..0bd126708fde81 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type FrozenTierPreference } from '@kbn/ml-date-picker'; + +export const TRANSFORM_FROZEN_TIER_PREFERENCE = 'transform.frozenDataTierPreference'; + +export type Transform = Partial<{ + [TRANSFORM_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; +}> | null; + +export type TransformKey = keyof Exclude; + +export type TransformStorageMapped = + T extends typeof TRANSFORM_FROZEN_TIER_PREFERENCE ? FrozenTierPreference | undefined : null; + +export const TRANSFORM_STORAGE_KEYS = [TRANSFORM_FROZEN_TIER_PREFERENCE] as const; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 0a221cf735395a..f0ad9227ac672e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -6,16 +6,24 @@ */ import React, { type FC, useRef, useState, createContext, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; +import { pick } from 'lodash'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; +import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { StorageContextProvider } from '@kbn/ml-local-storage'; +import { UrlStateProvider } from '@kbn/ml-url-state'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; + import type { TransformConfigUnion } from '../../../../../../common/types/transform'; import { getCreateTransformRequestBody } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; +import { useAppDependencies } from '../../../../app_dependencies'; import { applyTransformConfigToDefineState, @@ -34,6 +42,10 @@ import { import { WizardNav } from '../wizard_nav'; import type { RuntimeMappings } from '../step_define/common/types'; +import { TRANSFORM_STORAGE_KEYS } from './storage'; + +const localStorage = new Storage(window.localStorage); + enum WIZARD_STEPS { DEFINE, DETAILS, @@ -94,6 +106,7 @@ export const CreateTransformWizardContext = createContext<{ }); export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) => { + const appDependencies = useAppDependencies(); const { dataView } = searchItems; // The current WIZARD_STEP @@ -113,7 +126,7 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) const [stepCreateState, setStepCreateState] = useState(getDefaultStepCreateState); const transformConfig = getCreateTransformRequestBody( - dataView.getIndexPattern(), + dataView, stepDefineState, stepDetailsState ); @@ -206,11 +219,24 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) const stepsConfig = [stepDefine, stepDetails, stepCreate]; + const datePickerDeps = { + ...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']), + toMountPoint, + wrapWithTheme, + uiSettingsKeys: UI_SETTINGS, + }; + return ( - + + + + + + + ); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 51dfc449b89b28..bfc5d4f664b15f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -7,11 +7,13 @@ import React, { useMemo, FC } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/public'; + import { TransformConfigUnion } from '../../../../../../common/types/transform'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; -import { getPivotQuery } from '../../../../common'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { getTransformConfigQuery } from '../../../../common'; +import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { @@ -38,15 +40,15 @@ export const ExpandedRowPreviewPane: FC = ({ transf [transformConfig] ); - const pivotQuery = useMemo(() => getPivotQuery(searchQuery), [searchQuery]); + const transformConfigQuery = useMemo(() => getTransformConfigQuery(searchQuery), [searchQuery]); const dataViewTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; - const pivotPreviewProps = usePivotData( - dataViewTitle, - pivotQuery, + const pivotPreviewProps = useTransformConfigData( + { getIndexPattern: () => dataViewTitle } as DataView, + transformConfigQuery, validationStatus, previewRequest, runtimeMappings diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 8cb77da845d584..ca4191088a8b1f 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -46,6 +46,10 @@ "@kbn/field-types", "@kbn/ml-nested-property", "@kbn/ml-is-defined", + "@kbn/ml-date-picker", + "@kbn/ml-url-state", + "@kbn/ml-local-storage", + "@kbn/ml-query-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/transform.ts index fa54ea4ad67667..417ab317218de4 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/transform.ts @@ -112,6 +112,17 @@ export default function ({ getService }: FtrProviderContext) { ); await transform.sourceSelection.selectSource(ecIndexPattern); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '10 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); await transform.testExecution.logTestStep('displays an empty transform preview'); @@ -191,6 +202,18 @@ export default function ({ getService }: FtrProviderContext) { 'selects the source data and loads the Transform wizard page' ); await transform.sourceSelection.selectSource(ecIndexPattern); + + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '10 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.wizard.assertIndexPreviewLoaded(); await transform.wizard.assertTransformPreviewEmpty(); diff --git a/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts index a35e9759c212a1..1cb93ad03efe3b 100644 --- a/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const canvasElement = getService('canvasElement'); const esArchiver = getService('esArchiver'); const transform = getService('transform'); - const PageObjects = getPageObjects(['discover']); + const pageObjects = getPageObjects(['discover']); describe('creation_index_pattern', function () { before(async () => { @@ -486,6 +486,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await transform.testExecution.logTestStep('has correct transform function selected'); await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); @@ -699,16 +710,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await transform.testExecution.logTestStep('should navigate to discover'); await transform.table.clickTransformRowAction(testData.transformId, 'Discover'); - await PageObjects.discover.waitUntilSearchingHasFinished(); + await pageObjects.discover.waitUntilSearchingHasFinished(); if (testData.discoverAdjustSuperDatePicker) { + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); await transform.discover.assertNoResults(testData.destinationIndex); await transform.testExecution.logTestStep( 'should switch quick select lookback to years' ); - await transform.discover.assertSuperDatePickerToggleQuickMenuButtonExists(); - await transform.discover.openSuperDatePicker(); - await transform.discover.quickSelectYears(); + await transform.datePicker.quickSelect(); } await transform.discover.assertDiscoverQueryHits(testData.expected.discoverQueryHits); diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts index 2c456cb91e083b..3f0adc5783893b 100644 --- a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts @@ -279,6 +279,11 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('has correct transform function selected'); await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + await transform.testExecution.logTestStep('has correct runtime mappings settings'); await transform.wizard.assertRuntimeMappingsEditorSwitchExists(); await transform.wizard.assertRuntimeMappingsEditorSwitchCheckState(false); @@ -291,6 +296,12 @@ export default function ({ getService }: FtrProviderContext) { await transform.wizard.setRuntimeMappingsEditorContent(JSON.stringify(runtimeMappings)); await transform.wizard.applyRuntimeMappings(); + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(10, 'y'); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); @@ -439,7 +450,7 @@ export default function ({ getService }: FtrProviderContext) { if (isLatestTransformTestData(testData)) { const fromTime = 'Feb 7, 2016 @ 00:00:00.000'; const toTime = 'Feb 11, 2016 @ 23:59:54.000'; - await transform.wizard.setDiscoverTimeRange(fromTime, toTime); + await transform.datePicker.setTimeRange(fromTime, toTime); } await transform.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts index 60ab3f93ac3a5b..9f985a16da98d6 100644 --- a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts @@ -144,6 +144,17 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('has correct transform function selected'); await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(10, 'y'); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); diff --git a/x-pack/test/functional/apps/transform/edit_clone/cloning.ts b/x-pack/test/functional/apps/transform/edit_clone/cloning.ts index b823edadaed830..6b3bbac3f7b3db 100644 --- a/x-pack/test/functional/apps/transform/edit_clone/cloning.ts +++ b/x-pack/test/functional/apps/transform/edit_clone/cloning.ts @@ -418,6 +418,17 @@ export default function ({ getService }: FtrProviderContext) { ); } + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.testExecution.logTestStep('should load the index preview'); await transform.wizard.assertIndexPreviewLoaded(); diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts index 5901ee60c72120..c630525d06cf6b 100644 --- a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts +++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts @@ -11,15 +11,15 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'settings', 'security']); + const pageObjects = getPageObjects(['common', 'settings', 'security']); const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); describe('security', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); - await PageObjects.security.forceLogout(); - await PageObjects.common.navigateToApp('home'); + await pageObjects.security.forceLogout(); + await pageObjects.common.navigateToApp('home'); }); after(async () => { @@ -40,7 +40,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should not render the "Stack" section', async () => { - await PageObjects.common.navigateToApp('management'); + await pageObjects.common.navigateToApp('management'); const sections = (await managementMenu.getSections()).map((section) => section.sectionId); expect(sections).to.eql(['insightsAndAlerting', 'kibana']); }); @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should render the "Data" section with Transform', async () => { - await PageObjects.common.navigateToApp('management'); + await pageObjects.common.navigateToApp('management'); const sections = await managementMenu.getSections(); expect(sections).to.have.length(1); expect(sections[0]).to.eql({ diff --git a/x-pack/test/functional/services/transform/date_picker.ts b/x-pack/test/functional/services/transform/date_picker.ts new file mode 100644 index 00000000000000..941a506db6109d --- /dev/null +++ b/x-pack/test/functional/services/transform/date_picker.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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'; + +export function TransformDatePickerProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['timePicker']); + + return { + async assertSuperDatePickerToggleQuickMenuButtonExists() { + await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton'); + }, + + async openSuperDatePicker() { + await this.assertSuperDatePickerToggleQuickMenuButtonExists(); + await testSubjects.click('superDatePickerToggleQuickMenuButton'); + await testSubjects.existOrFail('superDatePickerQuickMenu'); + }, + + async quickSelect(timeValue: number = 15, timeUnit: string = 'y') { + await this.openSuperDatePicker(); + const quickMenuElement = await testSubjects.find('superDatePickerQuickMenu'); + + // No test subject, defaults to select `"Years"` to look back 15 years instead of 15 minutes. + await find.selectValue(`[aria-label*="Time value"]`, timeValue.toString()); + await find.selectValue(`[aria-label*="Time unit"]`, timeUnit); + + // Apply + const applyButton = await quickMenuElement.findByClassName('euiQuickSelect__applyButton'); + const actualApplyButtonText = await applyButton.getVisibleText(); + expect(actualApplyButtonText).to.be('Apply'); + + await applyButton.click(); + await testSubjects.missingOrFail('superDatePickerQuickMenu'); + }, + + async setTimeRange(fromTime: string, toTime: string) { + await pageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + }, + }; +} diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts index 944e65b73f6e22..303bc9b171f803 100644 --- a/x-pack/test/functional/services/transform/discover.ts +++ b/x-pack/test/functional/services/transform/discover.ts @@ -10,7 +10,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformDiscoverProvider({ getService }: FtrProviderContext) { - const find = getService('find'); const testSubjects = getService('testSubjects'); return { @@ -28,6 +27,8 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { }, async assertNoResults(expectedDestinationIndex: string) { + await testSubjects.missingOrFail('unifiedHistogramQueryHits'); + // Discover should use the destination index pattern const actualIndexPatternSwitchLinkText = await ( await testSubjects.find('discover-dataView-switch-link') @@ -39,29 +40,5 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('discoverNoResults'); }, - - async assertSuperDatePickerToggleQuickMenuButtonExists() { - await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton'); - }, - - async openSuperDatePicker() { - await testSubjects.click('superDatePickerToggleQuickMenuButton'); - await testSubjects.existOrFail('superDatePickerQuickMenu'); - }, - - async quickSelectYears() { - const quickMenuElement = await testSubjects.find('superDatePickerQuickMenu'); - - // No test subject, select "Years" to look back 15 years instead of 15 minutes. - await find.selectValue(`[aria-label*="Time unit"]`, 'y'); - - // Apply - const applyButton = await quickMenuElement.findByClassName('euiQuickSelect__applyButton'); - const actualApplyButtonText = await applyButton.getVisibleText(); - expect(actualApplyButtonText).to.be('Apply'); - - await applyButton.click(); - await testSubjects.existOrFail('unifiedHistogramQueryHits'); - }, }; } diff --git a/x-pack/test/functional/services/transform/index.ts b/x-pack/test/functional/services/transform/index.ts index 75f0df67f0919b..61461bafe34b5f 100644 --- a/x-pack/test/functional/services/transform/index.ts +++ b/x-pack/test/functional/services/transform/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { TransformAPIProvider } from './api'; import { TransformEditFlyoutProvider } from './edit_flyout'; +import { TransformDatePickerProvider } from './date_picker'; import { TransformDiscoverProvider } from './discover'; import { TransformManagementProvider } from './management'; import { TransformNavigationProvider } from './navigation'; @@ -25,6 +26,7 @@ import { MachineLearningTestResourcesProvider } from '../ml/test_resources'; export function TransformProvider(context: FtrProviderContext) { const api = TransformAPIProvider(context); const mlApi = MachineLearningAPIProvider(context); + const datePicker = TransformDatePickerProvider(context); const discover = TransformDiscoverProvider(context); const editFlyout = TransformEditFlyoutProvider(context); const management = TransformManagementProvider(context); @@ -39,6 +41,7 @@ export function TransformProvider(context: FtrProviderContext) { return { api, + datePicker, discover, editFlyout, management, diff --git a/x-pack/test/functional/services/transform/navigation.ts b/x-pack/test/functional/services/transform/navigation.ts index 396a99b1b66736..be579cdc0fb428 100644 --- a/x-pack/test/functional/services/transform/navigation.ts +++ b/x-pack/test/functional/services/transform/navigation.ts @@ -8,11 +8,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformNavigationProvider({ getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common']); + const pageObjects = getPageObjects(['common']); return { async navigateTo() { - return await PageObjects.common.navigateToApp('transform'); + return await pageObjects.common.navigateToApp('transform'); }, }; } diff --git a/x-pack/test/functional/services/transform/security_ui.ts b/x-pack/test/functional/services/transform/security_ui.ts index 07d3c2759f42cd..365f2dfc2e7d46 100644 --- a/x-pack/test/functional/services/transform/security_ui.ts +++ b/x-pack/test/functional/services/transform/security_ui.ts @@ -12,15 +12,15 @@ export function TransformSecurityUIProvider( { getPageObjects }: FtrProviderContext, transformSecurityCommon: TransformSecurityCommon ) { - const PageObjects = getPageObjects(['security']); + const pageObjects = getPageObjects(['security']); return { async loginAs(user: USER) { const password = transformSecurityCommon.getPasswordForUser(user); - await PageObjects.security.forceLogout(); + await pageObjects.security.forceLogout(); - await PageObjects.security.login(user, password, { + await pageObjects.security.login(user, password, { expectSuccess: true, }); }, @@ -34,7 +34,7 @@ export function TransformSecurityUIProvider( }, async logout() { - await PageObjects.security.forceLogout(); + await pageObjects.security.forceLogout(); }, }; } diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index df65911cb40987..e1370706d29020 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -29,7 +29,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi const ml = getService('ml'); const toasts = getService('toasts'); - const PageObjects = getPageObjects(['discover', 'timePicker']); + const pageObjects = getPageObjects(['discover', 'timePicker']); return { async clickNextButton() { @@ -80,6 +80,10 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi await testSubjects.existOrFail(selector); }, + async assertIndexPreviewEmpty() { + await this.assertIndexPreviewExists('empty'); + }, + async assertIndexPreviewLoaded() { await this.assertIndexPreviewExists('loaded'); }, @@ -995,19 +999,14 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi async redirectToDiscover() { await retry.tryForTime(60 * 1000, async () => { await testSubjects.click('transformWizardCardDiscover'); - await PageObjects.discover.isDiscoverAppOnScreen(); + await pageObjects.discover.isDiscoverAppOnScreen(); }); }, - async setDiscoverTimeRange(fromTime: string, toTime: string) { - await PageObjects.discover.isDiscoverAppOnScreen(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - }, - async assertDiscoverContainField(field: string) { - await PageObjects.discover.isDiscoverAppOnScreen(); + await pageObjects.discover.isDiscoverAppOnScreen(); await retry.tryForTime(60 * 1000, async () => { - const allFields = await PageObjects.discover.getAllFieldNames(); + const allFields = await pageObjects.discover.getAllFieldNames(); if (Array.isArray(allFields)) { // For some reasons, Discover returns fields with dot (e.g '.avg') with extra space const fields = allFields.map((n) => n.replace('.​', '.')); From 511aaf3004aab84ec2f9ae452e3f739a58876dd8 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:00:09 +0100 Subject: [PATCH 28/59] [Security Solution][Endpoint] Flx and unskip flaky test (#149839) ## Summary Fixes flaky test elastic/kibana/issues/145635 flaky test runners: - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1816 x 50 (successful on all 50) - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1828 x 150 (successful on all 150) - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1829 x 200 (failed on a single run for an [unrelated test](https://github.com/elastic/kibana/blob/92cb000a2f5116fc7408f52794cea06ad40de4bb/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts#L113)) - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1832 x 100 (successful on all 100) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../response_actions_log.test.tsx | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx index 0327c7c356966b..ae8d652aeaeb99 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx @@ -27,7 +27,7 @@ import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../../../../common/endpoint/ import { useUserPrivileges as _useUserPrivileges } from '../../../common/components/user_privileges'; import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; import { waitFor } from '@testing-library/react'; -import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; let mockUseGetEndpointActionList: { isFetched?: boolean; @@ -123,6 +123,7 @@ jest.mock('../../hooks/endpoint/use_get_endpoints_list'); jest.mock('../../../common/experimental_features_service'); jest.mock('../../../common/components/user_privileges'); +const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; let mockUseGetFileInfo: { isFetching?: boolean; @@ -139,12 +140,13 @@ jest.mock('../../hooks/response_actions/use_get_file_info', () => { const mockUseGetEndpointsList = useGetEndpointsList as jest.Mock; -// FLAKY https://github.com/elastic/kibana/issues/145635 -describe.skip('Response actions history', () => { - const useUserPrivilegesMock = _useUserPrivileges as jest.Mock< - ReturnType - >; - +const getBaseMockedActionList = () => ({ + isFetched: true, + isFetching: false, + error: null, + refetch: jest.fn(), +}); +describe('Response actions history', () => { const testPrefix = 'response-actions-list'; let render: ( @@ -155,14 +157,6 @@ describe.skip('Response actions history', () => { let mockedContext: AppContextTestRender; let apiMocks: ReturnType; - const refetchFunction = jest.fn(); - const baseMockedActionList = { - isFetched: true, - isFetching: false, - error: null, - refetch: refetchFunction, - }; - beforeEach(async () => { mockedContext = createAppRootMockRenderer(); ({ history } = mockedContext); @@ -173,7 +167,7 @@ describe.skip('Response actions history', () => { }); mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 13 }), }; @@ -189,20 +183,20 @@ describe.skip('Response actions history', () => { pageSize: 50, total: 50, }); + useUserPrivilegesMock.mockReturnValue({ + endpointPrivileges: getEndpointAuthzInitialStateMock(), + }); }); afterEach(() => { - mockUseGetEndpointActionList = { - ...baseMockedActionList, - }; - jest.clearAllMocks(); - useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); + mockUseGetEndpointActionList = getBaseMockedActionList(); + useUserPrivilegesMock.mockReset(); }); describe('When index does not exist yet', () => { it('should show global loader when waiting for response', () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), isFetched: false, isFetching: true, }; @@ -211,7 +205,7 @@ describe.skip('Response actions history', () => { }); it('should show empty page when there is no index', () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), error: { body: { statusCode: 404, message: 'index_not_found_exception' }, }, @@ -234,7 +228,7 @@ describe.skip('Response actions history', () => { it('should show empty state when there is no data', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 0 }), }; render(); @@ -295,7 +289,7 @@ describe.skip('Response actions history', () => { }; mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data, }; render({ showHostNames: true }); @@ -316,7 +310,7 @@ describe.skip('Response actions history', () => { }; mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data, }; render({ showHostNames: true }); @@ -339,7 +333,7 @@ describe.skip('Response actions history', () => { }; mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data, }; render({ showHostNames: true }); @@ -367,7 +361,7 @@ describe.skip('Response actions history', () => { it('should update per page rows on the table', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 33 }), }; @@ -398,7 +392,7 @@ describe.skip('Response actions history', () => { it('should show 1-1 record label when only 1 record', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 1 }), }; render(); @@ -446,7 +440,7 @@ describe.skip('Response actions history', () => { it('should contain download link in expanded row for `get-file` action WITH file operation permission', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }), }; @@ -475,7 +469,7 @@ describe.skip('Response actions history', () => { it('should show file unavailable for download for `get-file` action WITH file operation permission when file is deleted', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }), }; @@ -507,20 +501,14 @@ describe.skip('Response actions history', () => { }); it('should not contain download link in expanded row for `get-file` action when NO file operation permission', async () => { - const privileges = useUserPrivilegesMock(); - - useUserPrivilegesMock.mockImplementationOnce(() => { - return { - ...privileges, - endpointPrivileges: { - ...privileges.endpointPrivileges, - canWriteFileOperations: false, - }, - }; + useUserPrivilegesMock.mockReturnValue({ + endpointPrivileges: getEndpointAuthzInitialStateMock({ + canWriteFileOperations: false, + }), }); mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }), }; @@ -536,6 +524,7 @@ describe.skip('Response actions history', () => { }); it('should refresh data when autoRefresh is toggled on', async () => { + mockUseGetEndpointActionList = getBaseMockedActionList(); render(); const { getByTestId } = renderResult; @@ -550,16 +539,19 @@ describe.skip('Response actions history', () => { reactTestingLibrary.fireEvent.change(intervalInput, { target: { value: 1 } }); await reactTestingLibrary.waitFor(() => { - expect(refetchFunction).toHaveBeenCalledTimes(3); + expect(mockUseGetEndpointActionList.refetch).toHaveBeenCalledTimes(3); }); }); it('should refresh data when super date picker refresh button is clicked', async () => { + mockUseGetEndpointActionList = getBaseMockedActionList(); render(); const superRefreshButton = renderResult.getByTestId(`${testPrefix}-super-refresh-button`); userEvent.click(superRefreshButton); - expect(refetchFunction).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(mockUseGetEndpointActionList.refetch).toHaveBeenCalled(); + }); }); it('should set date picker with relative dates', async () => { @@ -592,7 +584,7 @@ describe.skip('Response actions history', () => { it('shows completed status badge for successfully completed actions', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 2 }), }; render(); @@ -609,7 +601,7 @@ describe.skip('Response actions history', () => { it('shows Failed status badge for failed actions', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 2, wasSuccessful: false, status: 'failed' }), }; render(); @@ -623,7 +615,7 @@ describe.skip('Response actions history', () => { it('shows Failed status badge for expired actions', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 2, isCompleted: false, @@ -645,7 +637,7 @@ describe.skip('Response actions history', () => { it('shows Pending status badge for pending actions', async () => { mockUseGetEndpointActionList = { - ...baseMockedActionList, + ...getBaseMockedActionList(), data: await getActionListMock({ actionCount: 2, isCompleted: false, status: 'pending' }), }; render(); @@ -693,6 +685,7 @@ describe.skip('Response actions history', () => { 'suspend-process', 'processes', 'get-file', + 'execute', ]); }); From b9320e3bdc4072550cc082e128227480de592a73 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:56:52 +0100 Subject: [PATCH 29/59] [Enterprise Search] Extraction rules UI for crawler (#149686) ## Summary This adds the extraction rules UI for the crawler. --------- Co-authored-by: Rodney Norris Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/types/extraction_rules.ts | 53 ++ .../crawler/_mocks_/crawler_domains.mock.ts | 2 + .../add_extraction_rule_api_logic.test.ts | 42 ++ .../add_extraction_rule_api_logic.ts | 52 ++ .../delete_extraction_rule_api_logic.test.ts | 41 ++ .../delete_extraction_rule_api_logic.ts | 42 ++ .../fetch_extraction_rules_api_logic.test.ts | 33 ++ .../fetch_extraction_rules_api_logic.ts | 37 ++ .../update_extraction_rule_api_logic.test.ts | 52 ++ .../update_extraction_rule_api_logic.ts | 55 ++ .../api/crawler/types.ts | 4 + .../api/crawler/utils.ts | 2 + .../authentication_panel.tsx | 2 +- .../crawl_rules_table.tsx | 4 +- .../crawler_domain_detail_logic.ts | 101 ++-- .../crawler_domain_detail_tabs.tsx | 37 +- .../deduplication_panel.tsx | 2 +- .../entry_points_table.test.tsx | 1 + .../entry_points_table.tsx | 4 +- .../extraction_rules/content_fields_panel.tsx | 116 +++++ .../extraction_rules/edit_extraction_rule.tsx | 446 ++++++++++++++++ .../edit_field_rule_flyout.tsx | 489 ++++++++++++++++++ .../extraction_rules/extraction_rules.tsx | 183 +++++++ .../extraction_rules_logic.tsx | 327 ++++++++++++ .../extraction_rules_table.test.tsx | 302 +++++++++++ .../extraction_rules_table.tsx | 257 +++++++++ .../extraction_rules/field_rules_table.tsx | 151 ++++++ .../sitemaps_table.test.tsx | 1 + .../crawler_domain_detail/sitemaps_table.tsx | 4 +- .../domain_management/domains_table.test.tsx | 2 + .../server/routes/app_search/index.ts | 2 + .../crawler/crawler_extraction_rules.ts | 113 ++++ 32 files changed, 2898 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/common/types/extraction_rules.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/content_fields_panel.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_extraction_rule.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_field_rule_flyout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_logic.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/field_rules_table.tsx create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler_extraction_rules.ts diff --git a/x-pack/plugins/enterprise_search/common/types/extraction_rules.ts b/x-pack/plugins/enterprise_search/common/types/extraction_rules.ts new file mode 100644 index 00000000000000..5bd33bc0af8527 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/extraction_rules.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum FieldType { + HTML = 'html', + URL = 'url', +} + +export enum ExtractionFilter { + BEGINS = 'begins', + ENDS = 'ends', + CONTAINS = 'contains', + REGEX = 'regex', +} + +export enum ContentFrom { + FIXED = 'fixed', + EXTRACTED = 'extracted', +} + +export enum MultipleObjectsHandling { + ARRAY = 'array', + STRING = 'string', +} + +export interface ExtractionRuleFieldRule { + content_from: { + value: string | null; + value_type: ContentFrom; + }; + field_name: string; + multiple_objects_handling: MultipleObjectsHandling; + selector: string; + source_type: FieldType; +} + +export interface ExtractionRuleBase { + description: string; + rules: ExtractionRuleFieldRule[]; + url_filters: Array<{ filter: ExtractionFilter; pattern: string }>; +} + +export type ExtractionRule = ExtractionRuleBase & { + created_at: string; + domain_id: string; + edited_by: string; + id: string; + updated_at: string; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/_mocks_/crawler_domains.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/_mocks_/crawler_domains.mock.ts index 4b11aa699b0816..ad6528ff600aa3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/_mocks_/crawler_domains.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/_mocks_/crawler_domains.mock.ts @@ -69,6 +69,7 @@ export const CRAWLER_DOMAIN_FROM_SERVER: CrawlerDomainFromServer = { deduplication_fields: ['url'], document_count: 400, entry_points: [ENTRY_POINT], + extraction_rules: [], id: '123abc', name: 'https://www.elastic.co', sitemaps: [SITEMAP], @@ -101,6 +102,7 @@ export const CRAWLER_DOMAIN: CrawlerDomain = { deduplicationFields: ['url'], documentCount: 400, entryPoints: [ENTRY_POINT], + extractionRules: [], id: '123abc', sitemaps: [SITEMAP], url: 'https://www.elastic.co', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.test.ts new file mode 100644 index 00000000000000..e564b2717e1263 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * 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 { mockHttpValues } from '../../../../__mocks__/kea_logic'; + +import { addExtractionRule } from './add_extraction_rule_api_logic'; + +describe('AddExtractionRuleApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('addExtractionRule', () => { + it('calls correct api', async () => { + const domainId = 'domain-id'; + const indexName = 'elastic-crawler'; + const rule = { rules: 'fake' } as any; + http.post.mockReturnValue(Promise.resolve('result')); + + const result = addExtractionRule({ + domainId, + indexName, + rule, + }); + expect(http.post).toHaveBeenCalledWith( + `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules`, + { body: JSON.stringify({ extraction_rule: rule }) } + ); + await expect(result).resolves.toEqual('result'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.ts new file mode 100644 index 00000000000000..5be30c66ead2f1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/add_extraction_rule_api_logic.ts @@ -0,0 +1,52 @@ +/* + * 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 { + ExtractionRule, + ExtractionRuleBase, +} from '../../../../../../common/types/extraction_rules'; +import { Actions } from '../../../../shared/api_logic/create_api_logic'; + +import { createApiLogic } from '../../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../../shared/http'; + +export interface AddExtractionRuleArgs { + domainId: string; + indexName: string; + rule: ExtractionRuleBase; +} + +export interface AddExtractionRuleResponse { + extraction_rules: ExtractionRule[]; +} + +export const addExtractionRule = async ({ + domainId, + indexName, + rule: { description, rules, url_filters: urlFilters }, +}: AddExtractionRuleArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules`; + + const params = { + extraction_rule: { + description, + rules, + url_filters: urlFilters, + }, + }; + + return await HttpLogic.values.http.post(route, { + body: JSON.stringify(params), + }); +}; + +export const AddExtractionRuleApiLogic = createApiLogic( + ['add_extraction_rule_api_logic'], + addExtractionRule +); + +export type AddExtractionRuleActions = Actions; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.test.ts new file mode 100644 index 00000000000000..5200a68df84dd7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * 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 { mockHttpValues } from '../../../../__mocks__/kea_logic'; + +import { deleteExtractionRule } from './delete_extraction_rule_api_logic'; + +describe('DeleteExtractionRuleApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('deleteExtractionRule', () => { + it('calls correct api', async () => { + const domainId = 'domain-id'; + const indexName = 'elastic-crawler'; + const extractionRuleId = 'extraction_rule_id'; + http.delete.mockReturnValue(Promise.resolve('result')); + + const result = deleteExtractionRule({ + domainId, + extractionRuleId, + indexName, + }); + expect(http.delete).toHaveBeenCalledWith( + `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules/${extractionRuleId}` + ); + await expect(result).resolves.toEqual('result'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.ts new file mode 100644 index 00000000000000..63b426a95c9ceb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/delete_extraction_rule_api_logic.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExtractionRule } from '../../../../../../common/types/extraction_rules'; +import { Actions } from '../../../../shared/api_logic/create_api_logic'; + +import { createApiLogic } from '../../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../../shared/http'; + +export interface DeleteExtractionRuleArgs { + domainId: string; + extractionRuleId: string; + indexName: string; +} + +export interface DeleteExtractionRuleResponse { + extraction_rules: ExtractionRule[]; +} + +export const deleteExtractionRule = async ({ + domainId, + extractionRuleId, + indexName, +}: DeleteExtractionRuleArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules/${extractionRuleId}`; + + return await HttpLogic.values.http.delete(route); +}; + +export const DeleteExtractionRuleApiLogic = createApiLogic( + ['delete_extraction_rule_api_logic'], + deleteExtractionRule +); + +export type DeleteExtractionRuleActions = Actions< + DeleteExtractionRuleArgs, + DeleteExtractionRuleResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.test.ts new file mode 100644 index 00000000000000..d7fb15501ddb65 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.test.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 { mockHttpValues } from '../../../../__mocks__/kea_logic'; + +import { fetchExtractionRules } from './fetch_extraction_rules_api_logic'; + +describe('FetchExtractionRuleApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('fetchExtractionRule', () => { + it('calls correct api', async () => { + const domainId = 'domain-id'; + const indexName = 'elastic-crawler'; + http.get.mockReturnValue(Promise.resolve('result')); + + const result = fetchExtractionRules({ + domainId, + indexName, + }); + expect(http.get).toHaveBeenCalledWith( + `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules` + ); + await expect(result).resolves.toEqual('result'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.ts new file mode 100644 index 00000000000000..903c4ebb3adfb0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/fetch_extraction_rules_api_logic.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExtractionRule } from '../../../../../../common/types/extraction_rules'; +import { Actions } from '../../../../shared/api_logic/create_api_logic'; + +import { createApiLogic } from '../../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../../shared/http'; + +export interface FetchExtractionRulesArgs { + domainId: string; + indexName: string; +} + +export interface FetchExtractionRulesResponse { + extraction_rules: ExtractionRule[]; +} + +export const fetchExtractionRules = async ({ domainId, indexName }: FetchExtractionRulesArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules`; + + return await HttpLogic.values.http.get(route); +}; + +export const FetchExtractionRulesApiLogic = createApiLogic( + ['fetch_extraction_rule_api_logic'], + fetchExtractionRules +); + +export type FetchExtractionRulesActions = Actions< + FetchExtractionRulesArgs, + FetchExtractionRulesResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.test.ts new file mode 100644 index 00000000000000..ed08a1e06776a2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.test.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ +/* + * 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 { mockHttpValues } from '../../../../__mocks__/kea_logic'; + +import { updateExtractionRule } from './update_extraction_rule_api_logic'; + +describe('UpdateExtractionRuleApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('updateExtractionRule', () => { + it('calls correct api', async () => { + const domainId = 'domain-id'; + const extractionRuleId = 'extraction_rule_id'; + const indexName = 'elastic-crawler'; + const rule = { + description: 'haha', + id: extractionRuleId, + rules: ['a'], + url_filters: ['b'], + } as any; + http.put.mockReturnValue(Promise.resolve('result')); + + const result = updateExtractionRule({ + domainId, + indexName, + rule, + }); + expect(http.put).toHaveBeenCalledWith( + `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules/${extractionRuleId}`, + { + body: JSON.stringify({ + extraction_rule: { description: 'haha', rules: ['a'], url_filters: ['b'] }, + }), + } + ); + await expect(result).resolves.toEqual('result'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.ts new file mode 100644 index 00000000000000..f36e7833a024e6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/extraction_rules/update_extraction_rule_api_logic.ts @@ -0,0 +1,55 @@ +/* + * 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 { + ExtractionRule, + ExtractionRuleBase, +} from '../../../../../../common/types/extraction_rules'; +import { Actions } from '../../../../shared/api_logic/create_api_logic'; + +import { createApiLogic } from '../../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../../shared/http'; + +export interface UpdateExtractionRuleArgs { + domainId: string; + indexName: string; + rule: ExtractionRule; +} + +export interface UpdateExtractionRuleResponse { + extraction_rules: ExtractionRule[]; +} + +export const updateExtractionRule = async ({ + domainId, + indexName, + rule, +}: UpdateExtractionRuleArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}/extraction_rules/${rule.id}`; + + const params: { extraction_rule: ExtractionRuleBase } = { + extraction_rule: { + description: rule.description, + rules: rule.rules, + url_filters: rule.url_filters, + }, + }; + + return await HttpLogic.values.http.put(route, { + body: JSON.stringify(params), + }); +}; + +export const UpdateExtractionRuleApiLogic = createApiLogic( + ['update_extraction_rule_api_logic'], + updateExtractionRule +); + +export type UpdateExtractionRuleActions = Actions< + UpdateExtractionRuleArgs, + UpdateExtractionRuleResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts index ce941613e35873..4e6f4e2ff0c325 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts @@ -4,8 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { Meta } from '../../../../../common/types'; import { CrawlerStatus } from '../../../../../common/types/crawler'; +import { ExtractionRule } from '../../../../../common/types/extraction_rules'; // TODO remove this proxy export, which will affect a lot of files export { CrawlerStatus }; @@ -88,6 +90,7 @@ export interface CrawlerDomainFromServer { default_crawl_rule?: CrawlRule; document_count: number; entry_points: EntryPoint[]; + extraction_rules: ExtractionRule[]; id: string; last_visited_at?: string; name: string; @@ -179,6 +182,7 @@ export interface CrawlerDomain { defaultCrawlRule?: CrawlRule; documentCount: number; entryPoints: EntryPoint[]; + extractionRules: ExtractionRule[]; id: string; lastCrawl?: string; sitemaps: Sitemap[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts index 7886d349044c06..1de8addea5afbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts @@ -44,6 +44,7 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C default_crawl_rule: defaultCrawlRule, document_count: documentCount, entry_points: entryPoints, + extraction_rules: extractionRules, id, last_visited_at: lastCrawl, name, @@ -59,6 +60,7 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C deduplicationFields, documentCount, entryPoints, + extractionRules, id, sitemaps, url: name, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/authentication_panel/authentication_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/authentication_panel/authentication_panel.tsx index 4f406ebe679396..4fe77b0196771b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/authentication_panel/authentication_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/authentication_panel/authentication_panel.tsx @@ -29,7 +29,7 @@ export const AuthenticationPanel: React.FC = () => {
- +

{i18n.translate('xpack.enterpriseSearch.crawler.authenticationPanel.title', { defaultMessage: 'Authentication', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawl_rules_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawl_rules_table.tsx index 6bead7b4314d91..db98f1fb987f74 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawl_rules_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawl_rules_table.tsx @@ -251,9 +251,7 @@ export const CrawlRulesTable: React.FC = ({ updateCrawlRules(newCrawlRules as CrawlRule[]); clearFlashMessages(); }} - title={i18n.translate('xpack.enterpriseSearch.crawler.crawlRulesTable.title', { - defaultMessage: 'Crawl rules', - })} + title="" uneditableItems={defaultCrawlRule ? [defaultCrawlRule] : undefined} canRemoveLastItem /> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_logic.ts index b36671d4927145..d17f3df02550f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_logic.ts @@ -7,7 +7,8 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpError, Status } from '../../../../../../../common/types/api'; +import { Status } from '../../../../../../../common/types/api'; +import { ExtractionRule } from '../../../../../../../common/types/extraction_rules'; import { generateEncodedPath } from '../../../../../shared/encode_path_params'; @@ -42,11 +43,11 @@ export interface CrawlerDomainDetailValues { deleteStatus: Status; domain: CrawlerDomain | null; domainId: string; + extractionRules: ExtractionRule[]; getLoading: boolean; } export interface CrawlerDomainDetailActions { - deleteApiError(error: HttpError): HttpError; deleteApiSuccess(response: DeleteCrawlerDomainResponse): DeleteCrawlerDomainResponse; deleteDomain(): void; deleteMakeRequest(args: DeleteCrawlerDomainArgs): DeleteCrawlerDomainArgs; @@ -59,24 +60,13 @@ export interface CrawlerDomainDetailActions { }; updateCrawlRules(crawlRules: CrawlRule[]): { crawlRules: CrawlRule[] }; updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; + updateExtractionRules(extractionRules: ExtractionRule[]): { extractionRules: ExtractionRule[] }; updateSitemaps(entryPoints: Sitemap[]): { sitemaps: Sitemap[] }; } export const CrawlerDomainDetailLogic = kea< MakeLogicType >({ - path: ['enterprise_search', 'crawler', 'crawler_domain_detail_logic'], - connect: { - actions: [ - DeleteCrawlerDomainApiLogic, - [ - 'apiError as deleteApiError', - 'apiSuccess as deleteApiSuccess', - 'makeRequest as deleteMakeRequest', - ], - ], - values: [DeleteCrawlerDomainApiLogic, ['status as deleteStatus']], - }, actions: { deleteDomain: () => true, deleteDomainComplete: () => true, @@ -86,36 +76,26 @@ export const CrawlerDomainDetailLogic = kea< submitDeduplicationUpdate: ({ fields, enabled }) => ({ enabled, fields }), updateCrawlRules: (crawlRules) => ({ crawlRules }), updateEntryPoints: (entryPoints) => ({ entryPoints }), + updateExtractionRules: (extractionRules) => ({ extractionRules }), updateSitemaps: (sitemaps) => ({ sitemaps }), }, - reducers: ({ props }) => ({ - domain: [ - null, - { - receiveDomainData: (_, { domain }) => domain, - updateCrawlRules: (currentDomain, { crawlRules }) => - ({ ...currentDomain, crawlRules } as CrawlerDomain), - updateEntryPoints: (currentDomain, { entryPoints }) => - ({ ...currentDomain, entryPoints } as CrawlerDomain), - updateSitemaps: (currentDomain, { sitemaps }) => - ({ ...currentDomain, sitemaps } as CrawlerDomain), - }, - ], - domainId: [props.domainId, { fetchDomainData: (_, { domainId }) => domainId }], - getLoading: [ - true, - { - receiveDomainData: () => false, - }, - ], - }), - selectors: ({ selectors }) => ({ - deleteLoading: [ - () => [selectors.deleteStatus], - (deleteStatus: Status) => deleteStatus === Status.LOADING, + connect: { + actions: [ + DeleteCrawlerDomainApiLogic, + ['apiSuccess as deleteApiSuccess', 'makeRequest as deleteMakeRequest'], ], - }), + values: [DeleteCrawlerDomainApiLogic, ['status as deleteStatus']], + }, listeners: ({ actions, values }) => ({ + deleteApiSuccess: () => { + const { indexName } = IndexNameLogic.values; + KibanaLogic.values.navigateToUrl( + generateEncodedPath(SEARCH_INDEX_TAB_PATH, { + indexName, + tabId: SearchIndexTabId.DOMAIN_MANAGEMENT, + }) + ); + }, deleteDomain: async () => { const { domain } = values; const { indexName } = IndexNameLogic.values; @@ -126,15 +106,6 @@ export const CrawlerDomainDetailLogic = kea< }); } }, - deleteApiSuccess: () => { - const { indexName } = IndexNameLogic.values; - KibanaLogic.values.navigateToUrl( - generateEncodedPath(SEARCH_INDEX_TAB_PATH, { - indexName, - tabId: SearchIndexTabId.DOMAIN_MANAGEMENT, - }) - ); - }, fetchDomainData: async ({ domainId }) => { const { http } = HttpLogic.values; const { indexName } = IndexNameLogic.values; @@ -201,4 +172,36 @@ export const CrawlerDomainDetailLogic = kea< } }, }), + path: ['enterprise_search', 'crawler', 'crawler_domain_detail_logic'], + reducers: ({ props }) => ({ + domain: [ + null, + { + receiveDomainData: (_, { domain }) => domain, + updateCrawlRules: (currentDomain, { crawlRules }) => + currentDomain ? { ...currentDomain, crawlRules } : currentDomain, + updateEntryPoints: (currentDomain, { entryPoints }) => + currentDomain ? { ...currentDomain, entryPoints } : currentDomain, + updateSitemaps: (currentDomain, { sitemaps }) => + currentDomain ? { ...currentDomain, sitemaps } : currentDomain, + }, + ], + domainId: [props.domainId, { fetchDomainData: (_, { domainId }) => domainId }], + getLoading: [ + true, + { + receiveDomainData: () => false, + }, + ], + }), + selectors: ({ selectors }) => ({ + deleteLoading: [ + () => [selectors.deleteStatus], + (deleteStatus: Status) => deleteStatus === Status.LOADING, + ], + extractionRules: [ + () => [selectors.domain], + (domain: CrawlerDomain | null) => domain?.extractionRules ?? [], + ], + }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_tabs.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_tabs.tsx index a62dc8bf3d558f..9ca36ba97f7b4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_tabs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/crawler_domain_detail_tabs.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; -import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CrawlerDomain } from '../../../../api/crawler/types'; @@ -16,6 +16,7 @@ import { AuthenticationPanel } from './authentication_panel/authentication_panel import { CrawlRulesTable } from './crawl_rules_table'; import { DeduplicationPanel } from './deduplication_panel/deduplication_panel'; import { EntryPointsTable } from './entry_points_table'; +import { ExtractionRules } from './extraction_rules/extraction_rules'; import { SitemapsTable } from './sitemaps_table'; export enum CrawlerDomainTabId { @@ -23,6 +24,7 @@ export enum CrawlerDomainTabId { AUTHENTICATION = 'authentication', SITE_MAPS = 'site_maps', CRAWL_RULES = 'crawl_rules', + EXTRACTION_RULES = 'extraction_rules', DEDUPLICATION = 'deduplication', } @@ -41,6 +43,13 @@ export const CrawlerDomainDetailTabs: React.FC = ( content: ( <> + +

+ {i18n.translate('xpack.enterpriseSearch.crawler.entryPointsTable.title', { + defaultMessage: 'Entry points', + })} +

+
), @@ -65,6 +74,13 @@ export const CrawlerDomainDetailTabs: React.FC = ( content: ( <> + +

+ {i18n.translate('xpack.enterpriseSearch.crawler.sitemapsTable.title', { + defaultMessage: 'Sitemaps', + })} +

+
), @@ -77,6 +93,13 @@ export const CrawlerDomainDetailTabs: React.FC = ( content: ( <> + +

+ {i18n.translate('xpack.enterpriseSearch.crawler.crawlRulesTable.title', { + defaultMessage: 'Crawl rules', + })} +

+
= ( defaultMessage: 'Crawl rules', }), }, + { + content: ( + <> + + + + ), + id: CrawlerDomainTabId.EXTRACTION_RULES, + name: i18n.translate('xpack.enterpriseSearch.content.crawler.extractionRules', { + defaultMessage: 'Extraction rules', + }), + }, { content: , id: CrawlerDomainTabId.DEDUPLICATION, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/deduplication_panel/deduplication_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/deduplication_panel/deduplication_panel.tsx index 8076b3e49aa1f3..e17815765169e5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/deduplication_panel/deduplication_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/deduplication_panel/deduplication_panel.tsx @@ -57,7 +57,7 @@ export const DeduplicationPanel: React.FC = () => { - +

{i18n.translate('xpack.enterpriseSearch.crawler.deduplicationPanel.title', { defaultMessage: 'Duplicate document handling', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.test.tsx index cf1224cb0dc473..438792b037ff99 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.test.tsx @@ -33,6 +33,7 @@ describe('EntryPointsTable', () => { deduplicationFields: ['title'], documentCount: 10, entryPoints, + extractionRules: [], id: '6113e1407a2f2e6f42489794', sitemaps: [], url: 'https://www.elastic.co', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.tsx index a80ecc85646b3b..8a38abf4efdc71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/entry_points_table.tsx @@ -130,9 +130,7 @@ export const EntryPointsTable: React.FC = ({ domain, inde onAdd={onAdd} onDelete={onDelete} onUpdate={onUpdate} - title={i18n.translate('xpack.enterpriseSearch.crawler.entryPointsTable.title', { - defaultMessage: 'Entry points', - })} + title="" disableReordering /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/content_fields_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/content_fields_panel.tsx new file mode 100644 index 00000000000000..fae0089669538d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/content_fields_panel.tsx @@ -0,0 +1,116 @@ +/* + * 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 { + EuiEmptyPrompt, + EuiText, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ExtractionRuleFieldRule } from '../../../../../../../../common/types/extraction_rules'; + +import { FieldRulesTable } from './field_rules_table'; + +interface ContentFieldsPanelProps { + contentFields: Array; + editExistingField: (id: string) => void; + editNewField: () => void; + removeField: (id: string) => void; +} + +export const ContentFieldsPanel: React.FC = ({ + contentFields, + editNewField, + editExistingField, + removeField, +}) => { + return contentFields.length === 0 ? ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.fieldRules.emptyMessageTitle', + { + defaultMessage: 'This extraction rule has no content fields', + } + )} +

+ } + titleSize="s" + body={ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.fieldRules.emptyMessageDescription', + { + defaultMessage: + 'Create a content field to pinpoint which parts of a webpage to pull data from.', + } + )} + + } + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.fieldRules.emptyMessageAddRuleLabel', + { + defaultMessage: 'Add content fields', + } + )} + + } + /> + ) : ( + <> + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.fieldRules.contentFieldDescription', + { + defaultMessage: + 'Create a content field to pinpoint which parts of a webpage to pull data from.', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.fieldRules.addContentFieldRuleLabel', + { + defaultMessage: 'Add content field rule', + } + )} + + +
+ + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_extraction_rule.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_extraction_rule.tsx new file mode 100644 index 00000000000000..c03a395591bae7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_extraction_rule.tsx @@ -0,0 +1,446 @@ +/* + * 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, useState } from 'react'; + +import { Controller, useFieldArray, useForm } from 'react-hook-form'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiPanel, + EuiRadioGroup, + EuiSelect, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { + ExtractionFilter, + ExtractionRule, + ExtractionRuleBase, +} from '../../../../../../../../common/types/extraction_rules'; + +import { ContentFieldsPanel } from './content_fields_panel'; +import { EditFieldRuleFlyout } from './edit_field_rule_flyout'; +import { ExtractionRulesLogic } from './extraction_rules_logic'; + +interface EditExtractionRuleProps { + cancelEditing: () => void; + extractionRule: ExtractionRule | null; + isNewRule: boolean; + saveRule: (rule: ExtractionRuleBase) => void; +} + +enum UrlState { + ALL = 'all', + SPECIFIC = 'specific', +} + +const getReadableExtractionFilter = (rule: ExtractionFilter) => { + switch (rule) { + case ExtractionFilter.BEGINS: + return i18n.translate( + 'xpack.enterpriseSearch.crawler.extractionRulesExtractionFilter.beginsWithLabel', + { + defaultMessage: 'Begins with', + } + ); + case ExtractionFilter.ENDS: + return i18n.translate( + 'xpack.enterpriseSearch.crawler.extractionRulesExtractionFilter.endsWithLabel', + { + defaultMessage: 'Ends with', + } + ); + case ExtractionFilter.CONTAINS: + return i18n.translate( + 'xpack.enterpriseSearch.crawler.extractionRulesExtractionFilter.containsLabel', + { + defaultMessage: 'Contains', + } + ); + case ExtractionFilter.REGEX: + return i18n.translate( + 'xpack.enterpriseSearch.crawler.extractionRulesExtractionFilter.regexLabel', + { + defaultMessage: 'Regex', + } + ); + } +}; + +const extractionFilterOptions = [ + ExtractionFilter.BEGINS, + ExtractionFilter.ENDS, + ExtractionFilter.CONTAINS, + ExtractionFilter.REGEX, +].map((ruleOption: ExtractionFilter) => ({ + text: getReadableExtractionFilter(ruleOption), + value: ruleOption, +})); + +export const EditExtractionRule: React.FC = ({ + cancelEditing, + extractionRule, + isNewRule, + saveRule, +}) => { + const { closeEditRuleFlyout, openEditRuleFlyout } = useActions(ExtractionRulesLogic); + const { fieldRuleFlyoutVisible, fieldRuleToEdit, fieldRuleToEditIndex, fieldRuleToEditIsNew } = + useValues(ExtractionRulesLogic); + const [urlToggle, setUrlToggle] = useState(UrlState.ALL); + const { control, formState, getValues, handleSubmit, reset, setValue } = + useForm({ + defaultValues: extractionRule ?? { + description: '', + rules: [], + url_filters: [], + }, + mode: 'all', + }); + const { + append: appendUrlFilter, + fields: urlFiltersFields, + remove: removeUrlFilter, + } = useFieldArray({ + control, + name: 'url_filters', + }); + const { + append: appendRule, + fields: rulesFields, + remove: removeRule, + update: updateRule, + } = useFieldArray({ control, name: 'rules' }); + + useEffect(() => { + reset( + extractionRule ?? { + description: '', + rules: [], + url_filters: [], + } + ); + if (extractionRule) { + setUrlToggle(extractionRule.url_filters.length === 0 ? UrlState.ALL : UrlState.SPECIFIC); + } else { + setUrlToggle(UrlState.ALL); + } + }, [extractionRule]); + + return ( + <> + +

+ {isNewRule + ? i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.addRule.title', + { + defaultMessage: 'Create a content extraction rule', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.title', + { + defaultMessage: 'Edit content extraction rule', + } + )} +

+
+ + + { + if (!rule?.trim()) { + return i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.descriptionError', + { + defaultMessage: 'A description is required for a content extraction rule', + } + ); + } + }, + }} + render={({ field, fieldState }) => ( + + + + )} + /> + + { + setUrlToggle(value as UrlState); + // Make sure we always have one url filter when switching to specific URL filters + if (value === UrlState.SPECIFIC && urlFiltersFields.length < 1) { + setValue('url_filters', [{ filter: ExtractionFilter.BEGINS, pattern: '' }]); + } else { + setValue('url_filters', []); + } + }} + /> + + + {urlToggle === UrlState.SPECIFIC && ( + <> + {urlFiltersFields.map((urlFilter, index) => ( + + + ( + + + + )} + /> + + + ( + <> + + + + + + )} + /> + + + {urlFiltersFields.length > 1 && ( + removeUrlFilter(index)} + /> + )} + + + ))} + + appendUrlFilter({ filter: ExtractionFilter.BEGINS, pattern: '' })} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFilters.addFilter', + { + defaultMessage: 'Add URL filter', + } + )} + + + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink', + { + defaultMessage: 'Learn more about URL filters', + } + )} + + + + + openEditRuleFlyout({ + fieldRule: rulesFields.find(({ id: ruleId }) => ruleId === id), + isNewRule: false, + }) + } + editNewField={() => openEditRuleFlyout({ isNewRule: true })} + removeField={(id) => { + const index = rulesFields.findIndex(({ id: ruleId }) => ruleId === id); + if (index >= 0) { + removeRule(index); + } + }} + /> + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + + + saveRule({ ...getValues() })} + disabled={!formState.isValid} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editRule.saveButtonLabel', + { + defaultMessage: 'Save rule', + } + )} + + + + + + {fieldRuleFlyoutVisible && ( + { + if (fieldRuleToEditIsNew) { + appendRule(fieldRule); + } else { + updateRule(fieldRuleToEditIndex ?? 0, fieldRule); + } + closeEditRuleFlyout(); + }} + /> + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_field_rule_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_field_rule_flyout.tsx new file mode 100644 index 00000000000000..2bb28f0d27045e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/edit_field_rule_flyout.tsx @@ -0,0 +1,489 @@ +/* + * 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 { Controller, useForm } from 'react-hook-form'; + +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiFormRow, + EuiPanel, + EuiRadioGroup, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { + ContentFrom, + ExtractionRuleFieldRule, + FieldType, + MultipleObjectsHandling, +} from '../../../../../../../../common/types/extraction_rules'; + +interface EditFieldRuleFlyoutProps { + fieldRule: ExtractionRuleFieldRule | null; + isNewRule: boolean; + onClose: () => void; + saveRule: (fieldRule: ExtractionRuleFieldRule & { id?: string; index?: number }) => void; +} + +const defaultRule = { + content_from: { + value: '', + value_type: undefined, + }, + field_name: '', + multiple_objects_handling: MultipleObjectsHandling.STRING, + selector: '', + source_type: undefined, +}; + +export const EditFieldRuleFlyout: React.FC = ({ + onClose, + fieldRule, + isNewRule, + saveRule, +}) => { + const { control, reset, getValues, formState } = useForm({ + defaultValues: fieldRule ?? defaultRule, + mode: 'all', + }); + + useEffect(() => { + reset(fieldRule ?? defaultRule); + }, [fieldRule]); + + return ( + + + +

+ {isNewRule + ? i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.addContentField.title', + { + defaultMessage: 'Add content field rule', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.title', + { + defaultMessage: 'Edit content field rule', + } + )} +

+
+
+ + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.documentField.title', + { + defaultMessage: 'Document field', + } + )} +

+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.documentField.description', + { + defaultMessage: 'Select a document field to build a rule around.', + } + )} + + + + !!rule?.trim() || + i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.edilidtContentField.documentField.requiredError', + { + defaultMessage: 'A field name is required.', + } + ), + }} + render={({ field, fieldState: { error, isTouched } }) => ( + + + + )} + /> +
+ + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.source.title', + { + defaultMessage: 'Source', + } + )} +

+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.source.description', + { + defaultMessage: 'Where to extract the content for this field from.', + } + )} + + + + !!rule?.trim() || + i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.source.requiredError', + { + defaultMessage: 'A source for the content is required.', + } + ), + }} + render={({ field }) => ( + <> + + + + {!!field.value && ( + <> + + + ( + + )} + /> + + + )} + + )} + /> +
+ + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.content.title', + { + defaultMessage: 'Content', + } + )} +

+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.content.description', + { + defaultMessage: 'Populate the field with content.', + } + )} + + + + + !!field?.trim() || + i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.content.requiredError', + { + defaultMessage: 'A value for this content field is required', + } + ), + }} + render={({ field, fieldState: { error, isTouched } }) => ( + <> + + + + {field.value === ContentFrom.EXTRACTED ? ( + ( + <> + + + + + + )} + /> + ) : ( + field.value === ContentFrom.FIXED && ( + <> + + ( + + + + )} + /> + + ) + )} + + )} + /> +
+
+
+ + + + + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.cancelButton.label', + { + defaultMessage: 'Cancel', + } + )} + + + + { + saveRule({ ...getValues() }); + }} + fill + > + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.extractionRules.editContentField.saveButton.label', + { + defaultMessage: 'Save', + } + )} + + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules.tsx new file mode 100644 index 00000000000000..5f3f734c24cd90 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules.tsx @@ -0,0 +1,183 @@ +/* + * 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 { + EuiButton, + EuiConfirmModal, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../../shared/constants'; + +import { EditExtractionRule } from './edit_extraction_rule'; +import { ExtractionRulesLogic } from './extraction_rules_logic'; +import { ExtractionRulesTable } from './extraction_rules_table'; + +export const ExtractionRules: React.FC = () => { + const { + cancelEditExtractionRule, + deleteExtractionRule, + editNewExtractionRule, + hideDeleteModal, + saveExtractionRule, + } = useActions(ExtractionRulesLogic); + const { + deleteModalVisible, + editingExtractionRule, + extractionRules, + extractionRuleToDelete, + extractionRuleToEdit, + extractionRuleToEditIsNew, + } = useValues(ExtractionRulesLogic); + + return ( + <> + {deleteModalVisible && ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.deleteModal.description', + { + defaultMessage: + 'Removing this rule will also delete {fields, plural, one {one field rule} other {# field rules}}. This action cannot be undone.', + values: { fields: extractionRuleToDelete?.rules.length ?? 0 }, + } + )} + + )} + + + +

+ {i18n.translate('xpack.enterpriseSearch.content.crawler.extractionRules.title', { + defaultMessage: 'Extraction rules', + })} +

+
+
+ {extractionRules.length === 0 ? ( + <> + ) : ( + + + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRulesTable.addRuleLabel', + { + defaultMessage: 'Add extraction rule', + } + )} + + + )} +
+ + +

+ + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.learnMoreLink', + { + defaultMessage: 'Learn more about content extraction rules.', + } + )} + + ), + }} + /> +

+
+ {editingExtractionRule ? ( + + ) : extractionRules.length === 0 ? ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRulesTable.emptyMessageTitle', + { + defaultMessage: 'There are no content extraction rules', + } + )} +

+ } + titleSize="s" + body={ + + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRulesTable.emptyMessageDescription', + { + defaultMessage: + 'Create a content extraction rule to change where document fields get their data during a sync.', + } + )} + + } + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRulesTable.emptyMessageAddRuleLabel', + { + defaultMessage: 'Add content extraction rule', + } + )} + + } + /> + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_logic.tsx new file mode 100644 index 00000000000000..f44d83cb7c99df --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_logic.tsx @@ -0,0 +1,327 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { Status } from '../../../../../../../../common/types/api'; +import { + ExtractionRule, + ExtractionRuleBase, + ExtractionRuleFieldRule, +} from '../../../../../../../../common/types/extraction_rules'; +import { + AddExtractionRuleActions, + AddExtractionRuleApiLogic, +} from '../../../../../api/crawler/extraction_rules/add_extraction_rule_api_logic'; +import { + DeleteExtractionRuleActions, + DeleteExtractionRuleApiLogic, +} from '../../../../../api/crawler/extraction_rules/delete_extraction_rule_api_logic'; +import { + FetchExtractionRulesActions, + FetchExtractionRulesApiLogic, +} from '../../../../../api/crawler/extraction_rules/fetch_extraction_rules_api_logic'; +import { + UpdateExtractionRuleActions, + UpdateExtractionRuleApiLogic, +} from '../../../../../api/crawler/extraction_rules/update_extraction_rule_api_logic'; +import { IndexNameLogic } from '../../../index_name_logic'; + +import { + CrawlerDomainDetailActions, + CrawlerDomainDetailLogic, + CrawlerDomainDetailValues, +} from '../crawler_domain_detail_logic'; + +export type ExtractionRuleView = ExtractionRule & { isExpanded: boolean }; + +interface ExtractionRulesActions { + addExtractionRule: AddExtractionRuleActions['makeRequest']; + addExtractionRuleSuccess: AddExtractionRuleActions['apiSuccess']; + applyDraft: () => void; + cancelEditExtractionRule: () => void; + closeEditRuleFlyout: () => void; + deleteExtractionRule: () => void; + deleteExtractionRuleRequest: DeleteExtractionRuleActions['makeRequest']; + deleteExtractionRuleSuccess: DeleteExtractionRuleActions['apiSuccess']; + deleteFieldRule: () => void; + editExtractionRule(extractionRule: ExtractionRule): { extractionRule: ExtractionRule }; + editNewExtractionRule: () => void; + fetchExtractionRules: FetchExtractionRulesActions['makeRequest']; + fetchExtractionRulesSuccess: FetchExtractionRulesActions['apiSuccess']; + hideDeleteFieldModal: () => void; + hideDeleteModal: () => void; + openEditRuleFlyout({ + fieldRule, + fieldRuleIndex, + isNewRule, + }: { + fieldRule?: ExtractionRuleFieldRule; + fieldRuleIndex?: number; + isNewRule: boolean; + }): { + fieldRule: ExtractionRuleFieldRule; + fieldRuleIndex?: number; + isNewRule: boolean; + }; + fetchDomainData: CrawlerDomainDetailActions['fetchDomainData']; + saveExtractionRule(extractionRule: ExtractionRuleBase): { + extractionRule: ExtractionRuleBase; + }; + setLocalExtractionRules(extractionRules: ExtractionRule[]): { extractionRules: ExtractionRule[] }; + showDeleteFieldModal({ + fieldRuleIndex, + extractionRuleId, + }: { + extractionRuleId: string; + fieldRuleIndex: number; + }): { extractionRuleId: string; fieldRuleIndex: number }; + showDeleteModal(extractionRule: ExtractionRule): { extractionRule: ExtractionRule }; + updateExtractionRule: UpdateExtractionRuleActions['makeRequest']; + updateExtractionRuleSuccess: UpdateExtractionRuleActions['apiSuccess']; +} + +interface ExtractionRulesValues { + addStatus: Status; + deleteFieldModalVisible: boolean; + deleteModalVisible: boolean; + deleteStatus: Status; + domain: CrawlerDomainDetailValues['domain']; + domainExtractionRules: ExtractionRule[] | null; + domainId: string; + editingExtractionRule: boolean; + extractionRuleToDelete: ExtractionRule | null; + extractionRuleToEdit: ExtractionRule | null; + extractionRuleToEditIsNew: boolean; + extractionRules: ExtractionRule[]; + fieldRuleFlyoutVisible: boolean; + fieldRuleToDelete: { extractionRuleId?: string; fieldRuleIndex?: number }; + fieldRuleToEdit: ExtractionRuleFieldRule | null; + fieldRuleToEditIndex: number | null; + fieldRuleToEditIsNew: boolean; + indexName: string; + isLoading: boolean; + isLoadingUpdate: boolean; + jsonValidationError: boolean; + updateStatus: Status; + updatedExtractionRules: ExtractionRule[] | null; +} + +export const ExtractionRulesLogic = kea< + MakeLogicType +>({ + actions: { + cancelEditExtractionRule: true, + closeEditRuleFlyout: true, + deleteExtractionRule: true, + deleteFieldRule: true, + editExtractionRule: (extractionRule) => ({ extractionRule }), + editNewExtractionRule: true, + hideDeleteFieldModal: true, + hideDeleteModal: true, + openEditRuleFlyout: ({ fieldRule, isNewRule }) => ({ + fieldRule, + isNewRule, + }), + saveExtractionRule: (extractionRule: ExtractionRuleBase) => ({ extractionRule }), + showDeleteFieldModal: ({ fieldRuleIndex, extractionRuleId }) => ({ + extractionRuleId, + fieldRuleIndex, + }), + showDeleteModal: (extractionRule: ExtractionRule) => ({ extractionRule }), + }, + connect: { + actions: [ + AddExtractionRuleApiLogic, + ['makeRequest as addExtractionRule', 'apiSuccess as addExtractionRuleSuccess'], + CrawlerDomainDetailLogic, + ['receiveDomainData'], + DeleteExtractionRuleApiLogic, + ['makeRequest as deleteExtractionRuleRequest', 'apiSuccess as deleteExtractionRuleSuccess'], + FetchExtractionRulesApiLogic, + ['makeRequest as fetchExtractionRules', 'apiSuccess as fetchExtractionRulesSuccess'], + UpdateExtractionRuleApiLogic, + ['makeRequest as updateExtractionRule', 'apiSuccess as updateExtractionRuleSuccess'], + ], + values: [ + AddExtractionRuleApiLogic, + ['status as addStatus'], + CrawlerDomainDetailLogic, + ['domain', 'domainId', 'extractionRules as domainExtractionRules', 'getLoading as isLoading'], + DeleteExtractionRuleApiLogic, + ['status as deleteStatus'], + IndexNameLogic, + ['indexName'], + UpdateExtractionRuleApiLogic, + ['status as updateStatus'], + ], + }, + events: ({ actions, values }) => ({ + beforeUnmount: () => { + // This prevents stale data from hanging around on unload + actions.fetchDomainData(values.domainId); + }, + }), + listeners: ({ actions, values }) => ({ + deleteExtractionRule: () => { + if (values.extractionRuleToDelete) { + actions.deleteExtractionRuleRequest({ + domainId: values.domainId, + extractionRuleId: values.extractionRuleToDelete?.id, + indexName: values.indexName, + }); + } + }, + deleteExtractionRuleSuccess: () => { + actions.hideDeleteModal(); + }, + deleteFieldRule: () => { + const { extractionRuleId, fieldRuleIndex } = values.fieldRuleToDelete; + const extractionRule = values.extractionRules.find(({ id }) => id === extractionRuleId); + if (extractionRule) { + const newFieldRules = extractionRule.rules.filter((_, index) => index !== fieldRuleIndex); + actions.updateExtractionRule({ + domainId: values.domainId, + indexName: values.indexName, + rule: { ...extractionRule, rules: newFieldRules }, + }); + } + }, + saveExtractionRule: ({ extractionRule }) => { + if (values.extractionRuleToEditIsNew) { + actions.addExtractionRule({ + domainId: values.domainId, + indexName: values.indexName, + rule: extractionRule, + }); + } else if (values.extractionRuleToEdit) { + actions.updateExtractionRule({ + domainId: values.domainId, + indexName: values.indexName, + rule: { ...values.extractionRuleToEdit, ...extractionRule }, + }); + } + }, + }), + path: ['enterprise_search', 'content', 'crawler', 'extraction_rules'], + reducers: () => ({ + deleteFieldModalVisible: [ + false, + { + hideDeleteFieldModal: () => false, + showDeleteFieldModal: () => true, + updateExtractionRuleSuccess: () => false, + }, + ], + deleteModalVisible: [ + false, + { + deleteExtractionRuleSuccess: () => false, + hideDeleteModal: () => false, + showDeleteModal: () => true, + }, + ], + editingExtractionRule: [ + false, + { + addExtractionRuleSuccess: () => false, + cancelEditExtractionRule: () => false, + editExtractionRule: () => true, + editNewExtractionRule: () => true, + updateExtractionRuleSuccess: () => false, + }, + ], + extractionRuleToDelete: [ + null, + { + deleteExtractionRuleSuccess: () => null, + hideDeleteModal: () => null, + showDeleteModal: (_, { extractionRule }) => extractionRule, + }, + ], + extractionRuleToEdit: [ + null, + { + addSuccess: () => null, + cancelEditExtractionRule: () => null, + editExtractionRule: (_, { extractionRule }) => extractionRule, + updateSuccess: () => null, + }, + ], + extractionRuleToEditIsNew: [ + false, + { + addSuccess: () => false, + editNewExtractionRule: () => true, + }, + ], + fieldRuleFlyoutVisible: [ + false, + { + addExtractionRuleSuccess: () => false, + closeEditRuleFlyout: () => false, + openEditRuleFlyout: () => true, + updateExtractionRuleSuccess: () => false, + }, + ], + fieldRuleToDelete: [ + {}, + { + hideDeleteFieldModal: () => ({}), + showDeleteFieldModal: (_, { extractionRuleId, fieldRuleIndex }) => ({ + extractionRuleId, + fieldRuleIndex, + }), + updateExtractionRuleSuccess: () => ({}), + }, + ], + fieldRuleToEdit: [ + null, + { + closeEditRuleFlyout: () => null, + openEditRuleFlyout: (_, { fieldRule }) => fieldRule ?? null, + }, + ], + fieldRuleToEditIndex: [ + null, + { + closeEditRuleFlyout: () => null, + openEditRuleFlyout: (_, { fieldRuleIndex }) => fieldRuleIndex ?? null, + }, + ], + fieldRuleToEditIsNew: [ + true, + { + closeEditRuleFlyout: () => true, + openEditRuleFlyout: (_, { isNewRule }) => isNewRule, + }, + ], + updatedExtractionRules: [ + null, + { + addExtractionRuleSuccess: (_, { extraction_rules: extractionRules }) => extractionRules, + deleteExtractionRuleSuccess: (_, { extraction_rules: extractionRules }) => extractionRules, + receiveDomainData: () => null, + updateExtractionRuleSuccess: (_, { extraction_rules: extractionRules }) => extractionRules, + }, + ], + }), + selectors: ({ selectors }) => ({ + extractionRules: [ + () => [selectors.domainExtractionRules, selectors.updatedExtractionRules], + ( + domainExtractionRules: ExtractionRule[] | null, + updatedExtractionRules: ExtractionRule[] | null + ) => updatedExtractionRules ?? domainExtractionRules ?? [], + ], + isLoadingUpdate: [ + () => [selectors.updateStatus, selectors.deleteStatus, selectors.addStatus], + (updateStatus: Status, deleteStatus: Status, addStatus: Status) => + [updateStatus, deleteStatus, addStatus].includes(Status.LOADING), + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.test.tsx new file mode 100644 index 00000000000000..2cc9bffb0f54f3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.test.tsx @@ -0,0 +1,302 @@ +/* + * 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 { mockFlashMessageHelpers, setMockActions } from '../../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiFieldText, EuiSelect } from '@elastic/eui'; + +import { GenericEndpointInlineEditableTable } from '../../../../../../shared/tables/generic_endpoint_inline_editable_table'; +import { CrawlerPolicies, CrawlerRules } from '../../../../../api/crawler/types'; + +import { CrawlRulesTable, CrawlRulesTableProps } from '../crawl_rules_table'; + +describe('CrawlRulesTable', () => { + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; + const indexName = 'index-name'; + const crawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + { id: '2', pattern: '*', policy: CrawlerPolicies.deny, rule: CrawlerRules.endsWith }, + ]; + + const DEFAULT_PROPS: CrawlRulesTableProps = { + crawlRules, + domainId: '6113e1407a2f2e6f42489794', + indexName, + }; + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(GenericEndpointInlineEditableTable).exists()).toBe(true); + }); + + describe('columns', () => { + const crawlRule = { + id: '1', + pattern: '*', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.beginsWith, + }; + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + const renderColumn = (index: number) => { + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + return shallow(
{columns[index].render(crawlRule)}
); + }; + + const onChange = jest.fn(); + const renderColumnInEditingMode = (index: number) => { + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + return shallow( +
+ {columns[index].editingRender(crawlRule, onChange, { + isInvalid: false, + isLoading: false, + })} +
+ ); + }; + + describe('policy column', () => { + it('shows the policy of a crawl rule', () => { + expect(renderColumn(0).html()).toContain('Allow'); + }); + + it('can show the policy of a crawl rule as editable', () => { + const column = renderColumnInEditingMode(0); + + const selectField = column.find(EuiSelect); + expect(selectField.props()).toEqual( + expect.objectContaining({ + disabled: false, + isInvalid: false, + options: [ + { text: 'Allow', value: 'allow' }, + { text: 'Disallow', value: 'deny' }, + ], + value: 'allow', + }) + ); + + selectField.simulate('change', { target: { value: 'deny' } }); + expect(onChange).toHaveBeenCalledWith('deny'); + }); + }); + + describe('rule column', () => { + it('shows the rule of a crawl rule', () => { + expect(renderColumn(1).html()).toContain('Begins with'); + }); + + it('can show the rule of a crawl rule as editable', () => { + const column = renderColumnInEditingMode(1); + + const selectField = column.find(EuiSelect); + expect(selectField.props()).toEqual( + expect.objectContaining({ + disabled: false, + isInvalid: false, + options: [ + { text: 'Begins with', value: 'begins' }, + { text: 'Ends with', value: 'ends' }, + { text: 'Contains', value: 'contains' }, + { text: 'Regex', value: 'regex' }, + ], + value: 'begins', + }) + ); + + selectField.simulate('change', { target: { value: 'ends' } }); + expect(onChange).toHaveBeenCalledWith('ends'); + }); + }); + + describe('pattern column', () => { + it('shows the pattern of a crawl rule', () => { + expect(renderColumn(2).html()).toContain('*'); + }); + + it('can show the pattern of a crawl rule as editable', () => { + const column = renderColumnInEditingMode(2); + + const field = column.find(EuiFieldText); + expect(field.props()).toEqual( + expect.objectContaining({ + disabled: false, + isInvalid: false, + value: '*', + }) + ); + + field.simulate('change', { target: { value: 'foo' } }); + expect(onChange).toHaveBeenCalledWith('foo'); + }); + }); + }); + + describe('routes', () => { + it('can calculate an update and delete route correctly', () => { + const wrapper = shallow(); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRule = { + id: '1', + pattern: '*', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.beginsWith, + }; + expect(table.prop('deleteRoute')(crawlRule)).toEqual( + '/internal/enterprise_search/indices/index-name/crawler/domains/6113e1407a2f2e6f42489794/crawl_rules/1' + ); + expect(table.prop('updateRoute')(crawlRule)).toEqual( + '/internal/enterprise_search/indices/index-name/crawler/domains/6113e1407a2f2e6f42489794/crawl_rules/1' + ); + }); + }); + + it('shows a custom description if one is provided', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + expect(table.prop('description')).toEqual('I am a description'); + }); + + it('shows a default crawl rule as uneditable if one is provided', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + expect(table.prop('uneditableItems')).toEqual([crawlRules[0]]); + }); + + describe('when a crawl rule is added', () => { + it('should update the crawl rules for the current domain, and clear flash messages', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRulesThatWasAdded = { + id: '2', + pattern: '*', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }; + const updatedCrawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + { id: '2', pattern: '*', policy: CrawlerPolicies.deny, rule: CrawlerRules.endsWith }, + ]; + table.prop('onAdd')(crawlRulesThatWasAdded, updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a crawl rule is updated', () => { + it('should update the crawl rules for the current domain, and clear flash messages', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRulesThatWasUpdated = { + id: '2', + pattern: '*', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }; + const updatedCrawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + { + id: '2', + pattern: 'newPattern', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }, + ]; + table.prop('onUpdate')(crawlRulesThatWasUpdated, updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a crawl rule is deleted', () => { + it('should update the crawl rules for the current domain, clear flash messages, and show a success', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRulesThatWasDeleted = { + id: '2', + pattern: '*', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }; + const updatedCrawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + ]; + table.prop('onDelete')(crawlRulesThatWasDeleted, updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + }); + + describe('when a crawl rule is reordered', () => { + it('should update the crawl rules for the current domain and clear flash messages', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const updatedCrawlRules = [ + { id: '2', pattern: '*', policy: CrawlerPolicies.deny, rule: CrawlerRules.endsWith }, + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + ]; + table.prop('onReorder')!(updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.tsx new file mode 100644 index 00000000000000..1a2e545dd4fe4b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/extraction_rules_table.tsx @@ -0,0 +1,257 @@ +/* + * 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, useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButtonEmpty, + EuiCode, + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedRelative } from '@kbn/i18n-react'; + +import { ExtractionRule } from '../../../../../../../../common/types/extraction_rules'; +import { CANCEL_BUTTON_LABEL } from '../../../../../../shared/constants'; + +import { ContentFieldsPanel } from './content_fields_panel'; +import { ExtractionRulesLogic } from './extraction_rules_logic'; + +export const ExtractionRulesTable: React.FC = () => { + const { + deleteFieldRule, + editExtractionRule, + hideDeleteFieldModal, + openEditRuleFlyout, + showDeleteFieldModal, + showDeleteModal, + } = useActions(ExtractionRulesLogic); + + const { deleteFieldModalVisible, extractionRules } = useValues(ExtractionRulesLogic); + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); + + useEffect(() => { + setItemIdToExpandedRowMap({}); + }, [extractionRules]); + + const toggleExpandedItem = (item: ExtractionRule) => { + if (itemIdToExpandedRowMap[item.id]) { + // omit item from rowmap + const { [item.id]: _, ...rest } = itemIdToExpandedRowMap; + setItemIdToExpandedRowMap(rest); + } else { + const rules = item.rules.map((val, index) => ({ ...val, id: `${index}`, index })); + const newItem = ( + + { + editExtractionRule(item); + const rule = rules.find(({ id: ruleId }) => id === ruleId); + if (rule) { + openEditRuleFlyout({ + fieldRule: rule, + fieldRuleIndex: rule.index, + isNewRule: false, + }); + } + }} + editNewField={() => { + editExtractionRule(item); + openEditRuleFlyout({ isNewRule: true }); + }} + removeField={(id) => { + const rule = rules.find(({ id: ruleId }) => id === ruleId); + if (rule) { + showDeleteFieldModal({ extractionRuleId: item.id, fieldRuleIndex: rule.index }); + } + }} + /> + + ); + setItemIdToExpandedRowMap({ ...itemIdToExpandedRowMap, [item.id]: newItem }); + } + }; + + const columns: Array> = [ + { + field: 'description', + name: i18n.translate( + 'xpack.enterpriseSearch.crawler.extractionRulesTable.descriptionTableLabel', + { + defaultMessage: 'Description', + } + ), + textOnly: true, + }, + { + field: 'url_filters', + name: i18n.translate('xpack.enterpriseSearch.crawler.extractionRulesTable.urlsLabel', { + defaultMessage: 'URLs', + }), + render: (filters: ExtractionRule['url_filters']) => ( + + {filters.length > 0 ? ( + filters.map(({ pattern }, index) => ( + + {pattern} + + )) + ) : ( + + {'/*'} + + )} + + ), + }, + { + name: i18n.translate('xpack.enterpriseSearch.crawler.extractionRulesTable.rulesLabel', { + defaultMessage: 'Field rules', + }), + render: (rule: ExtractionRule) => ( + toggleExpandedItem(rule)}> + {rule.rules.length} + + ), + textOnly: true, + }, + { + field: 'updated_at', + name: i18n.translate('xpack.enterpriseSearch.crawler.extractionRulesTable.lastUpdatedLabel', { + defaultMessage: 'Last updated', + }), + render: (lastUpdated: string) => , + textOnly: true, + }, + { + field: 'edited_by', + name: i18n.translate('xpack.enterpriseSearch.crawler.extractionRulesTable.editedByLabel', { + defaultMessage: 'Edited by', + }), + render: (editedBy: string) => editedBy, + textOnly: true, + }, + { + actions: [ + { + description: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.editRule.title', + { + defaultMessage: 'Edit this extraction rule', + } + ), + icon: 'pencil', + isPrimary: false, + name: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.editRule.caption', + { + defaultMessage: 'Edit this extraction rule', + } + ), + onClick: (extractionRule) => editExtractionRule(extractionRule), + type: 'icon', + }, + { + color: 'danger', + description: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.deleteRule.title', + { + defaultMessage: 'Delete this extraction rule', + } + ), + icon: 'trash', + isPrimary: false, + name: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.deleteRule.caption', + { + defaultMessage: 'Delete extraction rule', + } + ), + onClick: (extractionRule) => showDeleteModal(extractionRule), + type: 'icon', + }, + { + color: 'primary', + description: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.expandRule.title', + { + defaultMessage: 'Expand this extraction rule', + } + ), + icon: (item) => (!!itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'), + isPrimary: true, + name: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.expandRule.caption', + { + defaultMessage: 'Expand rule', + } + ), + onClick: (extractionRule) => toggleExpandedItem(extractionRule), + type: 'icon', + }, + ], + name: i18n.translate('xpack.enterpriseSearch.content.crawler.extractionRules.actions.label', { + defaultMessage: 'Actions', + }), + }, + ]; + + return ( + <> + {deleteFieldModalVisible && ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.deleteFieldModal.description', + { + defaultMessage: 'This action cannot be undone.', + } + )} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/field_rules_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/field_rules_table.tsx new file mode 100644 index 00000000000000..c8f6b3a882a969 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/extraction_rules/field_rules_table.tsx @@ -0,0 +1,151 @@ +/* + * 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 { EuiBasicTable, EuiBasicTableColumn, EuiCode, EuiFlexGroup, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { + ContentFrom, + ExtractionRuleFieldRule, + FieldType, + MultipleObjectsHandling, +} from '../../../../../../../../common/types/extraction_rules'; + +type FieldRuleWithId = ExtractionRuleFieldRule & { id: string }; + +export interface FieldRulesTableProps { + editRule: (id: string) => void; + fieldRules: FieldRuleWithId[]; + removeRule: (id: string) => void; +} + +export const FieldRulesTable: React.FC = ({ + editRule, + fieldRules, + removeRule, +}) => { + const columns: Array> = [ + { + field: 'field_name', + name: i18n.translate( + 'xpack.enterpriseSearch.crawler.extractionRules.fieldRulesTable.fieldNameLabel', + { + defaultMessage: 'Field name', + } + ), + textOnly: true, + }, + { + name: i18n.translate('xpack.enterpriseSearch.crawler.extractionRulesTable.sourceLabel', { + defaultMessage: 'Source', + }), + render: (rule: FieldRuleWithId) => ( + + + {rule.source_type === FieldType.HTML + ? i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.HTMLLabel', { + defaultMessage: 'HTML: ', + }) + : i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.UrlLabel', { + defaultMessage: 'URL: ', + })} + + {rule.selector} + + ), + }, + { + name: i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.contentLabel', { + defaultMessage: 'Content', + }), + render: ({ + content_from: content, + multiple_objects_handling: multipleObjectsHandling, + }: FieldRuleWithId) => ( + + + {content.value_type === ContentFrom.EXTRACTED + ? i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.extractedLabel', { + defaultMessage: 'Extracted as: ', + }) + : i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.fixedLabel', { + defaultMessage: 'Fixed value: ', + })} + + + {content.value_type === ContentFrom.FIXED + ? content.value + : multipleObjectsHandling === MultipleObjectsHandling.ARRAY + ? i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.arrayLabel', { + defaultMessage: 'array', + }) + : i18n.translate('xpack.enterpriseSearch.crawler.fieldRulesTable.stringLabel', { + defaultMessage: 'string', + })} + + + ), + }, + { + actions: [ + { + description: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.fieldRulesTable.editRule.title', + { + defaultMessage: 'Edit this content field rule', + } + ), + icon: 'pencil', + isPrimary: false, + name: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.fieldRulesTable.editRule.caption', + { + defaultMessage: 'Edit this content field rule', + } + ), + onClick: ({ id }) => editRule(id), + type: 'icon', + }, + { + color: 'danger', + description: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.deleteRule.title', + { + defaultMessage: 'Delete this extraction rule', + } + ), + icon: 'trash', + isPrimary: false, + name: i18n.translate( + 'xpack.enterpriseSearch.content.crawler.extractionRules.actions.deleteRule.caption', + { + defaultMessage: 'Delete extraction rule', + } + ), + onClick: ({ id }) => removeRule(id), + type: 'icon', + }, + ], + name: i18n.translate('xpack.enterpriseSearch.content.crawler.extractionRules.actions.label', { + defaultMessage: 'Actions', + }), + }, + ]; + + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.test.tsx index 1cae5f5cbe0960..e556c32b6eded0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.test.tsx @@ -35,6 +35,7 @@ describe('SitemapsTable', () => { url: 'https://www.elastic.co', crawlRules: [], entryPoints: [], + extractionRules: [], sitemaps, deduplicationEnabled: true, deduplicationFields: ['title'], diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx index c486cb2eeaed71..22b89a2fd3c596 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx @@ -112,9 +112,7 @@ export const SitemapsTable: React.FC = ({ domain, indexName, updateSitemaps(newSitemaps as Sitemap[]); clearFlashMessages(); }} - title={i18n.translate('xpack.enterpriseSearch.crawler.sitemapsTable.title', { - defaultMessage: 'Sitemaps', - })} + title="" disableReordering /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx index 350ef6ba686722..37250d99e789a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx @@ -37,6 +37,7 @@ const domains: CrawlerDomain[] = [ createdOn: '2020-01-01T00:00:00-12:00', deduplicationEnabled: false, deduplicationFields: ['title'], + extractionRules: [], availableDeduplicationFields: ['title', 'description'], auth: null, }, @@ -46,6 +47,7 @@ const domains: CrawlerDomain[] = [ url: 'empty.site', crawlRules: [], entryPoints: [], + extractionRules: [], sitemaps: [], createdOn: '1970-01-01T00:00:00-12:00', deduplicationEnabled: false, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 602d8c48d520ef..c1ce03f33b5879 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -6,6 +6,7 @@ */ import { RouteDependencies } from '../../plugin'; +import { registerCrawlerExtractionRulesRoutes } from '../enterprise_search/crawler/crawler_extraction_rules'; import { registerSearchRelevanceSuggestionsRoutes } from './adaptive_relevance'; import { registerAnalyticsRoutes } from './analytics'; @@ -50,6 +51,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerCrawlerRoutes(dependencies); registerCrawlerEntryPointRoutes(dependencies); registerCrawlerCrawlRulesRoutes(dependencies); + registerCrawlerExtractionRulesRoutes(dependencies); registerCrawlerSitemapRoutes(dependencies); registerSearchRelevanceSuggestionsRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler_extraction_rules.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler_extraction_rules.ts new file mode 100644 index 00000000000000..a5b9d38e70d9d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler_extraction_rules.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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../plugin'; + +const extractionRuleSchema = schema.object({ + extraction_rule: schema.object({ + description: schema.string(), + rules: schema.arrayOf( + schema.object({ + content_from: schema.object({ + value: schema.nullable(schema.string()), + value_type: schema.string(), + }), + field_name: schema.string(), + multiple_objects_handling: schema.string(), + selector: schema.string(), + source_type: schema.string(), + }) + ), + url_filters: schema.arrayOf( + schema.object({ filter: schema.string(), pattern: schema.string() }) + ), + }), +}); + +export function registerCrawlerExtractionRulesRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/internal/enterprise_search/indices/{indexName}/crawler/domains/{domainId}/extraction_rules', + validate: { + body: extractionRuleSchema, + params: schema.object({ + domainId: schema.string(), + indexName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + params: { + respond_with: 'index', + }, + path: '/api/ent/v1/internal/indices/:indexName/crawler2/domains/:domainId/extraction_rules', + }) + ); + + router.put( + { + path: '/internal/enterprise_search/indices/{indexName}/crawler/domains/{domainId}/extraction_rules/{crawlRuleId}', + validate: { + body: extractionRuleSchema, + params: schema.object({ + crawlRuleId: schema.string(), + domainId: schema.string(), + indexName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + params: { + respond_with: 'index', + }, + path: '/api/ent/v1/internal/indices/:indexName/crawler2/domains/:domainId/extraction_rules/:crawlRuleId', + }) + ); + + router.delete( + { + path: '/internal/enterprise_search/indices/{indexName}/crawler/domains/{domainId}/extraction_rules/{crawlRuleId}', + validate: { + params: schema.object({ + crawlRuleId: schema.string(), + domainId: schema.string(), + indexName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + params: { + respond_with: 'index', + }, + path: '/api/ent/v1/internal/indices/:indexName/crawler2/domains/:domainId/extraction_rules/:crawlRuleId', + }) + ); + + router.get( + { + path: '/internal/enterprise_search/indices/{indexName}/crawler/domains/{domainId}/extraction_rules/{crawlRuleId}', + validate: { + params: schema.object({ + crawlRuleId: schema.string(), + domainId: schema.string(), + indexName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + params: { + respond_with: 'index', + }, + path: '/api/ent/v1/internal/indices/:indexName/crawler2/domains/:domainId/extraction_rules/:crawlRuleId', + }) + ); +} From a373ac73368d9e9173072369c4093f1b09cf64a6 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Wed, 1 Feb 2023 11:14:41 +0100 Subject: [PATCH 30/59] [AO] Fix slo failed jest test (#150008) ## Summary Update jest slo snapshot --- .../historical_summary_client.test.ts.snap | 144 +++++++++--------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap index a8950a810a9b79..a05fded6706b78 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap @@ -4,10 +4,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.004019, + "consumed": 0.004449, "initial": 0.05, "isEstimated": true, - "remaining": 0.995981, + "remaining": 0.995551, }, "sliValue": 0.97, "status": "HEALTHY", @@ -18,10 +18,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.023374, + "consumed": 0.025879, "initial": 0.05, "isEstimated": true, - "remaining": 0.976626, + "remaining": 0.974121, }, "sliValue": 0.97, "status": "HEALTHY", @@ -32,10 +32,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.042725, + "consumed": 0.047306, "initial": 0.05, "isEstimated": true, - "remaining": 0.957275, + "remaining": 0.952694, }, "sliValue": 0.97, "status": "HEALTHY", @@ -46,10 +46,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.06208, + "consumed": 0.068729, "initial": 0.05, "isEstimated": true, - "remaining": 0.93792, + "remaining": 0.931271, }, "sliValue": 0.97, "status": "HEALTHY", @@ -60,10 +60,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.081433, + "consumed": 0.090171, "initial": 0.05, "isEstimated": true, - "remaining": 0.918567, + "remaining": 0.909829, }, "sliValue": 0.97, "status": "HEALTHY", @@ -74,10 +74,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.100784, + "consumed": 0.111593, "initial": 0.05, "isEstimated": true, - "remaining": 0.899216, + "remaining": 0.888407, }, "sliValue": 0.97, "status": "HEALTHY", @@ -88,10 +88,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.120137, + "consumed": 0.133038, "initial": 0.05, "isEstimated": true, - "remaining": 0.879863, + "remaining": 0.866962, }, "sliValue": 0.97, "status": "HEALTHY", @@ -102,10 +102,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.139494, + "consumed": 0.15444, "initial": 0.05, "isEstimated": true, - "remaining": 0.860506, + "remaining": 0.84556, }, "sliValue": 0.97, "status": "HEALTHY", @@ -116,10 +116,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.15887, + "consumed": 0.175896, "initial": 0.05, "isEstimated": true, - "remaining": 0.84113, + "remaining": 0.824104, }, "sliValue": 0.97, "status": "HEALTHY", @@ -130,10 +130,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.1782, + "consumed": 0.197304, "initial": 0.05, "isEstimated": true, - "remaining": 0.8218, + "remaining": 0.802696, }, "sliValue": 0.97, "status": "HEALTHY", @@ -144,10 +144,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.197546, + "consumed": 0.21876, "initial": 0.05, "isEstimated": true, - "remaining": 0.802454, + "remaining": 0.78124, }, "sliValue": 0.97, "status": "HEALTHY", @@ -158,10 +158,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.216933, + "consumed": 0.24016, "initial": 0.05, "isEstimated": true, - "remaining": 0.783067, + "remaining": 0.75984, }, "sliValue": 0.97, "status": "HEALTHY", @@ -172,10 +172,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.236292, + "consumed": 0.261569, "initial": 0.05, "isEstimated": true, - "remaining": 0.763708, + "remaining": 0.738431, }, "sliValue": 0.97, "status": "HEALTHY", @@ -186,10 +186,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.25563, + "consumed": 0.283019, "initial": 0.05, "isEstimated": true, - "remaining": 0.74437, + "remaining": 0.716981, }, "sliValue": 0.97, "status": "HEALTHY", @@ -200,10 +200,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.274977, + "consumed": 0.304465, "initial": 0.05, "isEstimated": true, - "remaining": 0.725023, + "remaining": 0.695535, }, "sliValue": 0.97, "status": "HEALTHY", @@ -214,10 +214,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.294298, + "consumed": 0.325866, "initial": 0.05, "isEstimated": true, - "remaining": 0.705702, + "remaining": 0.674134, }, "sliValue": 0.97, "status": "HEALTHY", @@ -228,10 +228,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.313653, + "consumed": 0.347293, "initial": 0.05, "isEstimated": true, - "remaining": 0.686347, + "remaining": 0.652707, }, "sliValue": 0.97, "status": "HEALTHY", @@ -242,10 +242,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns th Object { "date": Any, "errorBudget": Object { - "consumed": 0.333025, + "consumed": 0.368727, "initial": 0.05, "isEstimated": true, - "remaining": 0.666975, + "remaining": 0.631273, }, "sliValue": 0.97, "status": "HEALTHY", @@ -256,10 +256,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.001344, + "consumed": 0.001488, "initial": 0.05, "isEstimated": false, - "remaining": 0.998656, + "remaining": 0.998512, }, "sliValue": 0.97, "status": "HEALTHY", @@ -270,10 +270,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.002688, + "consumed": 0.002976, "initial": 0.05, "isEstimated": false, - "remaining": 0.997312, + "remaining": 0.997024, }, "sliValue": 0.97, "status": "HEALTHY", @@ -284,10 +284,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.004032, + "consumed": 0.004464, "initial": 0.05, "isEstimated": false, - "remaining": 0.995968, + "remaining": 0.995536, }, "sliValue": 0.97, "status": "HEALTHY", @@ -298,10 +298,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.005376, + "consumed": 0.005952, "initial": 0.05, "isEstimated": false, - "remaining": 0.994624, + "remaining": 0.994048, }, "sliValue": 0.97, "status": "HEALTHY", @@ -312,10 +312,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.00672, + "consumed": 0.00744, "initial": 0.05, "isEstimated": false, - "remaining": 0.99328, + "remaining": 0.99256, }, "sliValue": 0.97, "status": "HEALTHY", @@ -326,10 +326,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.008065, + "consumed": 0.008929, "initial": 0.05, "isEstimated": false, - "remaining": 0.991935, + "remaining": 0.991071, }, "sliValue": 0.97, "status": "HEALTHY", @@ -340,10 +340,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.009409, + "consumed": 0.010417, "initial": 0.05, "isEstimated": false, - "remaining": 0.990591, + "remaining": 0.989583, }, "sliValue": 0.97, "status": "HEALTHY", @@ -354,10 +354,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.010753, + "consumed": 0.011905, "initial": 0.05, "isEstimated": false, - "remaining": 0.989247, + "remaining": 0.988095, }, "sliValue": 0.97, "status": "HEALTHY", @@ -368,10 +368,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.012097, + "consumed": 0.013393, "initial": 0.05, "isEstimated": false, - "remaining": 0.987903, + "remaining": 0.986607, }, "sliValue": 0.97, "status": "HEALTHY", @@ -382,10 +382,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.013441, + "consumed": 0.014881, "initial": 0.05, "isEstimated": false, - "remaining": 0.986559, + "remaining": 0.985119, }, "sliValue": 0.97, "status": "HEALTHY", @@ -396,10 +396,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.014785, + "consumed": 0.016369, "initial": 0.05, "isEstimated": false, - "remaining": 0.985215, + "remaining": 0.983631, }, "sliValue": 0.97, "status": "HEALTHY", @@ -410,10 +410,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.016129, + "consumed": 0.017857, "initial": 0.05, "isEstimated": false, - "remaining": 0.983871, + "remaining": 0.982143, }, "sliValue": 0.97, "status": "HEALTHY", @@ -424,10 +424,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.017473, + "consumed": 0.019345, "initial": 0.05, "isEstimated": false, - "remaining": 0.982527, + "remaining": 0.980655, }, "sliValue": 0.97, "status": "HEALTHY", @@ -438,10 +438,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.018817, + "consumed": 0.020833, "initial": 0.05, "isEstimated": false, - "remaining": 0.981183, + "remaining": 0.979167, }, "sliValue": 0.97, "status": "HEALTHY", @@ -452,10 +452,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.020161, + "consumed": 0.022321, "initial": 0.05, "isEstimated": false, - "remaining": 0.979839, + "remaining": 0.977679, }, "sliValue": 0.97, "status": "HEALTHY", @@ -466,10 +466,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.021505, + "consumed": 0.02381, "initial": 0.05, "isEstimated": false, - "remaining": 0.978495, + "remaining": 0.97619, }, "sliValue": 0.97, "status": "HEALTHY", @@ -480,10 +480,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.022849, + "consumed": 0.025298, "initial": 0.05, "isEstimated": false, - "remaining": 0.977151, + "remaining": 0.974702, }, "sliValue": 0.97, "status": "HEALTHY", @@ -494,10 +494,10 @@ exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the Object { "date": Any, "errorBudget": Object { - "consumed": 0.024194, + "consumed": 0.026786, "initial": 0.05, "isEstimated": false, - "remaining": 0.975806, + "remaining": 0.973214, }, "sliValue": 0.97, "status": "HEALTHY", From f296abb6c93de1b98c3dab2dc61c7eef66c691ba Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 1 Feb 2023 11:57:22 +0100 Subject: [PATCH 31/59] [@kbn/handlebars] Support custom decorator return value (#149392) Fixes #149327 --- packages/kbn-handlebars/index.test.ts | 189 +++++++++++++++++- packages/kbn-handlebars/index.ts | 179 +++++++++-------- .../kbn-handlebars/src/__jest__/test_bench.ts | 10 + 3 files changed, 296 insertions(+), 82 deletions(-) diff --git a/packages/kbn-handlebars/index.test.ts b/packages/kbn-handlebars/index.test.ts index 6159a65dbcb8fd..7a3a2a5772c144 100644 --- a/packages/kbn-handlebars/index.test.ts +++ b/packages/kbn-handlebars/index.test.ts @@ -307,7 +307,37 @@ describe('blocks', () => { .toCompileTo(''); }); - it('should pass expected options to root decorator', () => { + it('should pass expected options to root decorator with no args', () => { + expectTemplate('{{*decorator}}') + .withDecorator('decorator', function (fn, props, container, options) { + expect(options).toMatchInlineSnapshot(` + Object { + "args": Array [], + "data": Object { + "root": Object { + "foo": "bar", + }, + }, + "hash": Object {}, + "loc": Object { + "end": Object { + "column": 14, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "name": "decorator", + } + `); + }) + .withInput({ foo: 'bar' }) + .toCompileTo(''); + }); + + it('should pass expected options to root decorator with one arg', () => { expectTemplate('{{*decorator foo}}') .withDecorator('decorator', function (fn, props, container, options) { expect(options).toMatchInlineSnapshot(` @@ -339,6 +369,163 @@ describe('blocks', () => { .toCompileTo(''); }); + describe('return values', () => { + for (const [desc, template, result] of [ + ['non-block', '{{*decorator}}cont{{*decorator}}ent', 'content'], + ['block', '{{#*decorator}}con{{/decorator}}tent', 'tent'], + ]) { + describe(desc, () => { + const falsy = [undefined, null, false, 0, '']; + const truthy = [true, 42, 'foo', {}]; + + // Falsy return values from decorators are simply ignored and the + // execution falls back to default behavior which is to render the + // other parts of the template. + for (const value of falsy) { + it(`falsy value (type ${typeof value}): ${JSON.stringify(value)}`, () => { + expectTemplate(template) + .withDecorator('decorator', () => value) + .toCompileTo(result); + }); + } + + // Truthy return values from decorators are expected to be functions + // and the program will attempt to call them. We expect an error to + // be thrown in this case. + for (const value of truthy) { + it(`non-falsy value (type ${typeof value}): ${JSON.stringify(value)}`, () => { + expectTemplate(template) + .withDecorator('decorator', () => value) + .toThrow('is not a function'); + }); + } + + // If the decorator return value is a custom function, its return + // value will be the final content of the template. + for (const value of [...falsy, ...truthy]) { + it(`function returning ${typeof value}: ${JSON.stringify(value)}`, () => { + expectTemplate(template) + .withDecorator('decorator', () => () => value) + .toCompileTo(value as string); + }); + } + }); + } + }); + + describe('custom return function should be called with expected arguments and its return value should be rendered in the template', () => { + it('root decorator', () => { + expectTemplate('{{*decorator}}world') + .withInput({ me: 'my' }) + .withDecorator( + 'decorator', + (fn): Handlebars.TemplateDelegate => + (context, options) => { + expect(context).toMatchInlineSnapshot(` + Object { + "me": "my", + } + `); + expect(options).toMatchInlineSnapshot(` + Object { + "decorators": Object { + "decorator": [Function], + }, + "helpers": Object {}, + } + `); + return `hello ${context.me} ${fn()}!`; + } + ) + .toCompileTo('hello my world!'); + }); + + it('decorator nested inside of array-helper', () => { + expectTemplate('{{#arr}}{{*decorator}}world{{/arr}}') + .withInput({ arr: ['my'] }) + .withDecorator( + 'decorator', + (fn): Handlebars.TemplateDelegate => + (context, options) => { + expect(context).toMatchInlineSnapshot(`"my"`); + expect(options).toMatchInlineSnapshot(` + Object { + "blockParams": Array [ + "my", + 0, + ], + "data": Object { + "_parent": Object { + "root": Object { + "arr": Array [ + "my", + ], + }, + }, + "first": true, + "index": 0, + "key": 0, + "last": true, + "root": Object { + "arr": Array [ + "my", + ], + }, + }, + } + `); + return `hello ${context} ${fn()}!`; + } + ) + .toCompileTo('hello my world!'); + }); + + it('decorator nested inside of custom helper', () => { + expectTemplate('{{#helper}}{{*decorator}}world{{/helper}}') + .withHelper('helper', function (options: Handlebars.HelperOptions) { + return options.fn('my', { foo: 'bar' } as any); + }) + .withDecorator( + 'decorator', + (fn): Handlebars.TemplateDelegate => + (context, options) => { + expect(context).toMatchInlineSnapshot(`"my"`); + expect(options).toMatchInlineSnapshot(` + Object { + "foo": "bar", + } + `); + return `hello ${context} ${fn()}!`; + } + ) + .toCompileTo('hello my world!'); + }); + }); + + it('should call multiple decorators in the same program body in the expected order and get the expected output', () => { + let decoratorCall = 0; + let progCall = 0; + expectTemplate('{{*decorator}}con{{*decorator}}tent') + .beforeRender(() => { + // ensure the counters are reset between EVAL/AST render calls + decoratorCall = 0; + progCall = 0; + }) + .withInput({ + decoratorCall: 0, + progCall: 0, + }) + .withDecorator('decorator', (fn) => { + const decoratorCallOrder = ++decoratorCall; + const ret: Handlebars.TemplateDelegate = () => { + const progCallOrder = ++progCall; + return `(decorator: ${decoratorCallOrder}, prog: ${progCallOrder}, fn: "${fn()}")`; + }; + return ret; + }) + .toCompileTo('(decorator: 2, prog: 1, fn: "(decorator: 1, prog: 2, fn: "content")")'); + }); + describe('registration', () => { beforeEach(() => { global.kbnHandlebarsEnv = Handlebars.create(); diff --git a/packages/kbn-handlebars/index.ts b/packages/kbn-handlebars/index.ts index da48f5e1475f7a..2c63e014a33876 100644 --- a/packages/kbn-handlebars/index.ts +++ b/packages/kbn-handlebars/index.ts @@ -64,6 +64,13 @@ type ProcessableNodeWithPathPartsOrLiteral = ProcessableNode & { path: hbs.AST.PathExpression | hbs.AST.Literal; }; +interface Helper { + fn?: Handlebars.HelperDelegate; + context: any[]; + params: any[]; + options: AmbiguousHelperOptions; +} + export type NonBlockHelperOptions = Omit; export type AmbiguousHelperOptions = Handlebars.HelperOptions | NonBlockHelperOptions; @@ -290,7 +297,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { render(context: any, options: ExtendedRuntimeOptions = {}): string { this.contexts = [context]; this.output = []; - this.runtimeOptions = options; + this.runtimeOptions = Object.assign({}, options); this.container.helpers = Object.assign(this.initialHelpers, options.helpers); this.container.decorators = Object.assign( this.initialDecorators, @@ -312,9 +319,46 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { this.ast = Handlebars.parse(this.template!); } - this.accept(this.ast); + // The `defaultMain` function contains the default behavior: + // + // Generate a "program" function based on the root `Program` in the AST and + // call it. This will start the processing of all the child nodes in the + // AST. + const defaultMain: Handlebars.TemplateDelegate = (_context) => { + const prog = this.generateProgramFunction(this.ast!); + return prog(_context, this.runtimeOptions); + }; - return this.output.join(''); + // Run any decorators that might exist on the root: + // + // The `defaultMain` function is passed in, and if there are no root + // decorators, or if the decorators chooses to do so, the same function is + // returned from `processDecorators` and the default behavior is retained. + // + // Alternatively any of the root decorators might call the `defaultMain` + // function themselves, process its return value, and return a completely + // different `main` function. + const main = this.processDecorators(this.ast, defaultMain); + this.processedRootDecorators = true; + + // Call the `main` function and add the result to the final output. + const result = main(this.context, options); + + if (main === defaultMain) { + this.output.push(result); + return this.output.join(''); + } else { + // We normally expect the return value of `main` to be a string. However, + // if a decorator is used to override the `defaultMain` function, the + // return value can be any type. To match the upstream handlebars project + // behavior, we want the result of rendering the template to be the + // literal value returned by the decorator. + // + // Since the output array in this case always will be empty, we just + // return that single value instead of attempting to join all the array + // elements as strings. + return result; + } } // ********************************************** // @@ -323,11 +367,6 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { Program(program: hbs.AST.Program) { this.blockParamNames.unshift(program.blockParams); - - // Run any decorators that might exist on the root - this.processDecorators(program, this.generateProgramFunction(program)); - this.processedRootDecorators = true; - super.Program(program); this.blockParamNames.shift(); } @@ -340,14 +379,14 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { this.processStatementOrExpression(block); } - // This space intentionally left blank: We want to override the Visitor class implementation - // of this method, but since we handle decorators separately before traversing the nodes, we - // just want to make this a no-op. + // This space is intentionally left blank: We want to override the Visitor + // class implementation of this method, but since we handle decorators + // separately before traversing the nodes, we just want to make this a no-op. DecoratorBlock(decorator: hbs.AST.DecoratorBlock) {} - // This space intentionally left blank: We want to override the Visitor class implementation - // of this method, but since we handle decorators separately before traversing the nodes, we - // just want to make this a no-op. + // This space is intentionally left blank: We want to override the Visitor + // class implementation of this method, but since we handle decorators + // separately before traversing the nodes, we just want to make this a no-op. Decorator(decorator: hbs.AST.Decorator) {} SubExpression(sexpr: hbs.AST.SubExpression) { @@ -405,20 +444,23 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { */ private processDecorators(program: hbs.AST.Program, prog: Handlebars.TemplateDelegate) { if (!this.processedDecoratorsForProgram.has(program)) { + this.processedDecoratorsForProgram.add(program); + const props = {}; for (const node of program.body) { if (isDecorator(node)) { - this.processDecorator(node, prog); + prog = this.processDecorator(node, prog, props); } } - this.processedDecoratorsForProgram.add(program); } + + return prog; } private processDecorator( decorator: hbs.AST.DecoratorBlock | hbs.AST.Decorator, - prog: Handlebars.TemplateDelegate + prog: Handlebars.TemplateDelegate, + props: Record ) { - const props = {}; const options = this.setupDecoratorOptions(decorator); const result = this.container.lookupProperty( @@ -426,7 +468,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { options.name )(prog, props, this.container, options); - Object.assign(result || prog, props); + return Object.assign(result || prog, props); } private processStatementOrExpression(node: ProcessableNodeWithPathPartsOrLiteral) { @@ -590,77 +632,51 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { } private processAmbiguousNode(node: ProcessableNodeWithPathParts) { - const invokeResult = this.invokeAmbiguous(node); - - if (isBlock(node)) { - const result = this.ambiguousBlockValue(node, invokeResult); - if (result != null) { - this.output.push(result); - } - } else { - if ( - (node as hbs.AST.MustacheStatement).escaped === false || - this.compileOptions.noEscape === true || - typeof invokeResult !== 'string' - ) { - this.output.push(invokeResult); - } else { - this.output.push(Handlebars.escapeExpression(invokeResult)); - } - } - } - - // This operation is used when an expression like `{{foo}}` - // is provided, but we don't know at compile-time whether it - // is a helper or a path. - // - // This operation emits more code than the other options, - // and can be avoided by passing the `knownHelpers` and - // `knownHelpersOnly` flags at compile-time. - private invokeAmbiguous(node: ProcessableNodeWithPathParts) { const name = node.path.parts[0]; const helper = this.setupHelper(node, name); + let { fn: helperFn } = helper; - const loc = helper.fn ? node.loc : node.path.loc; - helper.fn = helper.fn ?? this.resolveNodes(node.path)[0]; + const loc = helperFn ? node.loc : node.path.loc; + helperFn = helperFn ?? this.resolveNodes(node.path)[0]; - if (helper.fn === undefined) { + if (helperFn === undefined) { if (this.compileOptions.strict) { - helper.fn = this.container.strict(helper.context, name, loc); + helperFn = this.container.strict(helper.context, name, loc); } else { - helper.fn = + helperFn = helper.context != null ? this.container.lookupProperty(helper.context, name) : helper.context; - if (helper.fn == null) helper.fn = this.container.hooks.helperMissing; + if (helperFn == null) helperFn = this.container.hooks.helperMissing; } } - return typeof helper.fn === 'function' - ? helper.fn.call(helper.context, ...helper.params, helper.options) - : helper.fn; - } - - private ambiguousBlockValue(block: hbs.AST.BlockStatement, value: any) { - const name = block.path.parts[0]; - const helper = this.setupHelper(block, name); + const helperResult = + typeof helperFn === 'function' + ? helperFn.call(helper.context, ...helper.params, helper.options) + : helperFn; - if (!helper.fn) { - value = this.container.hooks.blockHelperMissing!.call(this.context, value, helper.options); + if (isBlock(node)) { + const result = helper.fn + ? helperResult + : this.container.hooks.blockHelperMissing!.call(this.context, helperResult, helper.options); + if (result != null) { + this.output.push(result); + } + } else { + if ( + (node as hbs.AST.MustacheStatement).escaped === false || + this.compileOptions.noEscape === true || + typeof helperResult !== 'string' + ) { + this.output.push(helperResult); + } else { + this.output.push(Handlebars.escapeExpression(helperResult)); + } } - - return value; } - private setupHelper( - node: ProcessableNode, - helperName: string - ): { - fn?: Handlebars.HelperDelegate; - context: any[]; - params: any[]; - options: AmbiguousHelperOptions; - } { + private setupHelper(node: ProcessableNode, helperName: string): Helper { return { fn: this.container.lookupProperty(this.container.helpers, helperName), context: this.context, @@ -683,6 +699,8 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { } else { options.args = this.resolveNodes(decorator.params); } + } else { + options.args = []; } return options; @@ -701,13 +719,12 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { }; if (isBlock(node)) { - // TODO: Is there a way in TypeScript to infer that `options` is `Handlebars.HelperOptions` inside this if-statement. If not, is there a way to just cast once? - (options as Handlebars.HelperOptions).fn = this.generateProgramFunction(node.program); - if (node.program) - this.processDecorators(node.program, (options as Handlebars.HelperOptions).fn); - (options as Handlebars.HelperOptions).inverse = this.generateProgramFunction(node.inverse); - if (node.inverse) - this.processDecorators(node.inverse, (options as Handlebars.HelperOptions).inverse); + (options as Handlebars.HelperOptions).fn = node.program + ? this.processDecorators(node.program, this.generateProgramFunction(node.program)) + : noop; + (options as Handlebars.HelperOptions).inverse = node.inverse + ? this.processDecorators(node.inverse, this.generateProgramFunction(node.inverse)) + : noop; } return options; diff --git a/packages/kbn-handlebars/src/__jest__/test_bench.ts b/packages/kbn-handlebars/src/__jest__/test_bench.ts index 4aaac2b52bd736..ffbf8b1fe84f52 100644 --- a/packages/kbn-handlebars/src/__jest__/test_bench.ts +++ b/packages/kbn-handlebars/src/__jest__/test_bench.ts @@ -38,6 +38,7 @@ export function forEachCompileFunctionName( class HandlebarsTestBench { private template: string; private options: TestOptions; + private beforeRenderFn: Function = () => {}; private compileOptions?: ExtendedCompileOptions; private runtimeOptions?: ExtendedRuntimeOptions; private helpers: { [name: string]: Handlebars.HelperDelegate | undefined } = {}; @@ -49,6 +50,11 @@ class HandlebarsTestBench { this.options = options; } + beforeRender(fn: Function) { + this.beforeRenderFn = fn; + return this; + } + withCompileOptions(compileOptions?: ExtendedCompileOptions) { this.compileOptions = compileOptions; return this; @@ -147,6 +153,8 @@ class HandlebarsTestBench { this.runtimeOptions ); + this.beforeRenderFn(); + return renderEval(this.input, runtimeOptions); } @@ -161,6 +169,8 @@ class HandlebarsTestBench { this.runtimeOptions ); + this.beforeRenderFn(); + return renderAST(this.input, runtimeOptions); } From abfe96ff8976418844a7f2c56ceb7c0d65feaee2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 1 Feb 2023 12:19:22 +0100 Subject: [PATCH 32/59] [Synthetics] Step metrics (#149481) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Fixes https://github.com/elastic/kibana/issues/145392 --- .../common/components/thershold_indicator.tsx | 6 +- .../monitor_test_result/result_details.tsx | 22 +-- .../hooks/use_network_timings.ts | 19 +-- .../hooks/use_network_timings_prev.ts | 21 +-- .../hooks/use_step_filters.ts | 24 ---- .../hooks/use_step_metrics.ts | 12 +- .../hooks/use_step_prev_metrics.ts | 125 ++++++++++++------ .../step_details_page/step_metrics/labels.ts | 5 + .../step_metrics/step_metrics.tsx | 2 +- 9 files changed, 105 insertions(+), 131 deletions(-) delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_filters.ts diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx index b1d82f16f3c358..fc77ef52aee8af 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/thershold_indicator.tsx @@ -38,10 +38,10 @@ export const ThresholdIndicator = ({ loading: boolean; current: number; previous?: number | null; - previousFormatted: string; - currentFormatted: string; - asStat?: boolean; + previousFormatted?: string | number; + currentFormatted: string | number; setHasAnyDelta?: (hasDelta: boolean) => void; + asStat?: boolean; }) => { if (loading) { return ; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx index 2021c0da1423e6..e0835dd7824142 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx @@ -10,7 +10,6 @@ import { EuiDescriptionList, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useStepMetrics } from '../../step_details_page/hooks/use_step_metrics'; import { JourneyStepScreenshotContainer } from '../screenshot/journey_step_screenshot_container'; -import { formatBytes } from '../../step_details_page/hooks/use_object_metrics'; import { ThresholdIndicator } from '../components/thershold_indicator'; import { useNetworkTimings } from '../../step_details_page/hooks/use_network_timings'; import { useNetworkTimingsPrevious24Hours } from '../../step_details_page/hooks/use_network_timings_prev'; @@ -62,16 +61,12 @@ export const ResultDetails = ({ }; export const TimingDetails = ({ step }: { step: JourneyStep }) => { - const { timingsWithLabels, transferSize } = useNetworkTimings( + const { timingsWithLabels } = useNetworkTimings( step.monitor.check_group, step.synthetics.step?.index ); - const { - timingsWithLabels: prevTimingsWithLabels, - loading, - transferSizePrev, - } = useNetworkTimingsPrevious24Hours( + const { timingsWithLabels: prevTimingsWithLabels, loading } = useNetworkTimingsPrevious24Hours( step.synthetics.step?.index, step['@timestamp'], step.monitor.check_group @@ -94,19 +89,6 @@ export const TimingDetails = ({ step }: { step: JourneyStep }) => { }; }); - items.push({ - title: transferSize.label, - description: ( - - ), - }); - return ( { dns.push(bucket.dns.value ?? 0); @@ -194,7 +183,6 @@ export const useNetworkTimingsPrevious24Hours = ( wait.push(bucket.wait.value ?? 0); blocked.push(bucket.blocked.value ?? 0); ssl.push(bucket.ssl.value ?? 0); - transferSize.push(bucket.transferSize.value ?? 0); }); const timings = { @@ -205,21 +193,16 @@ export const useNetworkTimingsPrevious24Hours = ( wait: median(wait), blocked: median(blocked), ssl: median(ssl), - transferSize: median(transferSize), }; return { loading, timings, - transferSizePrev: { - value: timings.transferSize, - label: CONTENT_SIZE_LABEL, - }, timingsWithLabels: getTimingWithLabels(timings), }; }; -const median = (arr: number[]): number => { +export const median = (arr: number[]): number => { if (!arr.length) return 0; const s = [...arr].sort((a, b) => a - b); const mid = Math.floor(s.length / 2); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_filters.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_filters.ts deleted file mode 100644 index 6828cdf69633d1..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_filters.ts +++ /dev/null @@ -1,24 +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 { useParams } from 'react-router-dom'; - -export const useStepFilters = (prevCheckGroupId?: string) => { - const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); - return [ - { - term: { - 'monitor.check_group': prevCheckGroupId ?? checkGroupId, - }, - }, - { - term: { - 'synthetics.step.index': Number(stepIndex), - }, - }, - ]; -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts index 0f2342e34496b6..7c7d1ecb898283 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_metrics.ts @@ -10,7 +10,13 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { formatBytes } from './use_object_metrics'; import { formatMillisecond } from '../step_metrics/step_metrics'; -import { CLS_HELP_LABEL, DCL_TOOLTIP, FCP_TOOLTIP, LCP_HELP_LABEL } from '../step_metrics/labels'; +import { + CLS_HELP_LABEL, + DCL_TOOLTIP, + FCP_TOOLTIP, + LCP_HELP_LABEL, + TRANSFER_SIZE_HELP, +} from '../step_metrics/labels'; import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { JourneyStep } from '../../../../../../common/runtime_types'; @@ -173,7 +179,7 @@ export const useStepMetrics = (step?: JourneyStep) => { value: metrics?.cls.value, label: CLS_LABEL, helpText: CLS_HELP_LABEL, - formatted: formatMillisecond((metrics?.cls.value ?? 0) / 1000), + formatted: metrics?.cls.value ?? 0, }, { value: metrics?.dcl.value, @@ -184,7 +190,7 @@ export const useStepMetrics = (step?: JourneyStep) => { { value: transferDataVal, label: TRANSFER_SIZE, - helpText: '', + helpText: TRANSFER_SIZE_HELP, formatted: formatBytes(transferDataVal ?? 0), }, ], diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts index db5475380f5ebf..d29c142c030830 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_prev_metrics.ts @@ -9,7 +9,6 @@ import { useParams } from 'react-router-dom'; import { useEsSearch } from '@kbn/observability-plugin/public'; import { formatBytes } from './use_object_metrics'; import { formatMillisecond } from '../step_metrics/step_metrics'; -import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; import { CLS_LABEL, DCL_LABEL, @@ -19,6 +18,8 @@ import { TRANSFER_SIZE, } from './use_step_metrics'; import { JourneyStep } from '../../../../../../common/runtime_types'; +import { median } from './use_network_timings_prev'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; export const MONITOR_DURATION_US = 'monitor.duration.us'; export const SYNTHETICS_CLS = 'browser.experience.cls'; @@ -75,29 +76,37 @@ export const useStepPrevMetrics = (step?: JourneyStep) => { }, }, aggs: { - fcp: { - avg: { - field: SYNTHETICS_FCP, - }, - }, - lcp: { - avg: { - field: SYNTHETICS_LCP, - }, - }, - cls: { - avg: { - field: SYNTHETICS_CLS, - }, - }, - dcl: { - avg: { - field: SYNTHETICS_DCL, + testRuns: { + terms: { + field: 'monitor.check_group', + size: 10000, }, - }, - totalDuration: { - avg: { - field: SYNTHETICS_STEP_DURATION, + aggs: { + fcp: { + sum: { + field: SYNTHETICS_FCP, + }, + }, + lcp: { + sum: { + field: SYNTHETICS_LCP, + }, + }, + cls: { + sum: { + field: SYNTHETICS_CLS, + }, + }, + dcl: { + sum: { + field: SYNTHETICS_DCL, + }, + }, + stepDuration: { + sum: { + field: SYNTHETICS_STEP_DURATION, + }, + }, }, }, }, @@ -106,7 +115,6 @@ export const useStepPrevMetrics = (step?: JourneyStep) => { [monitorId, checkGroupId, stepIndex], { name: 'previousStepMetrics' } ); - const { data: transferData } = useEsSearch( { index: SYNTHETICS_INDEX_PATTERN, @@ -150,14 +158,17 @@ export const useStepPrevMetrics = (step?: JourneyStep) => { }, }, aggs: { - transferSize: { - avg: { - field: 'synthetics.payload.transfer_size', + testRuns: { + terms: { + field: 'monitor.check_group', + size: 10000, }, - }, - resourceSize: { - avg: { - field: 'synthetics.payload.resource_size', + aggs: { + transferSize: { + sum: { + field: 'synthetics.payload.transfer_size', + }, + }, }, }, }, @@ -170,40 +181,66 @@ export const useStepPrevMetrics = (step?: JourneyStep) => { ); const metrics = data?.aggregations; - const transferDataVal = transferData?.aggregations?.transferSize?.value ?? 0; + + const transferSize: number[] = []; + transferData?.aggregations?.testRuns.buckets.forEach((bucket) => { + transferSize.push(bucket.transferSize.value ?? 0); + }); + + const medianTransferSize = median(transferSize); + + const lcp: number[] = []; + const fcp: number[] = []; + const cls: number[] = []; + const dcl: number[] = []; + const stepDuration: number[] = []; + + metrics?.testRuns.buckets.forEach((bucket) => { + lcp.push(bucket.lcp.value ?? 0); + fcp.push(bucket.fcp.value ?? 0); + cls.push(bucket.cls.value ?? 0); + dcl.push(bucket.dcl.value ?? 0); + stepDuration.push(bucket.stepDuration.value ?? 0); + }); + + const medianLcp = median(lcp); + const medianFcp = median(fcp); + const medianCls = median(cls); + const medianDcl = median(dcl); + const medianStepDuration = median(stepDuration); return { loading, metrics: [ { label: STEP_DURATION_LABEL, - value: metrics?.totalDuration.value, - formatted: formatMillisecond((metrics?.totalDuration.value ?? 0) / 1000), + value: medianStepDuration, + formatted: formatMillisecond(medianStepDuration / 1000), }, { - value: metrics?.lcp.value, + value: medianLcp, label: LCP_LABEL, - formatted: formatMillisecond((metrics?.lcp.value ?? 0) / 1000), + formatted: formatMillisecond(medianLcp / 1000), }, { - value: metrics?.fcp.value, + value: medianFcp, label: FCP_LABEL, - formatted: formatMillisecond((metrics?.fcp.value ?? 0) / 1000), + formatted: formatMillisecond(medianFcp / 1000), }, { - value: metrics?.cls.value, + value: medianCls, label: CLS_LABEL, - formatted: formatMillisecond((metrics?.cls.value ?? 0) / 1000), + formatted: medianCls, }, { - value: metrics?.dcl.value, + value: medianDcl, label: DCL_LABEL, - formatted: formatMillisecond((metrics?.dcl.value ?? 0) / 1000), + formatted: formatMillisecond(medianDcl / 1000), }, { - value: transferDataVal, + value: medianTransferSize, label: TRANSFER_SIZE, - formatted: formatBytes(transferDataVal ?? 0), + formatted: formatBytes(medianTransferSize), }, ], }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/labels.ts index f8ab8547736345..ee2820306e7544 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/labels.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/labels.ts @@ -27,6 +27,11 @@ export const DCL_TOOLTIP = i18n.translate('xpack.synthetics.coreVitals.dclToolti 'Triggered when the browser completes parsing the document. Helpful when there are multiple listeners, or logic is executed: domContentLoadedEventEnd - domContentLoadedEventStart.', }); +export const TRANSFER_SIZE_HELP = i18n.translate('xpack.synthetics.fieldLabels.transferSize', { + defaultMessage: + 'The transferSize property represents the size of the fetched resource. The size includes the response header fields plus the response payload body', +}); + export const LCP_LABEL = i18n.translate('xpack.synthetics.fieldLabels.lcp', { defaultMessage: 'Largest contentful paint (LCP)', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx index bb508e9bea2f0c..fdcb874e0b48da 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx @@ -57,7 +57,7 @@ export const StepMetrics = () => { previous={prevVal?.value ?? 0} helpText={helpText} currentFormatted={formatted} - previousFormatted={prevVal?.formatted!} + previousFormatted={prevVal?.formatted} />
); From 6f3b29df5d4dcaa6a4f87d06c9f8407e7c32ec50 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 1 Feb 2023 12:24:45 +0100 Subject: [PATCH 33/59] [Discover] Add a way to quickly expand time range from "No results" screen (#147195) Related to issue https://github.com/elastic/kibana/issues/12608 A part of Spacetime project https://github.com/elastic/kibana/pull/146729 but only for "No results" UI, excluding the time picker changes. ## Summary This PR extends the "No results matches your search criteria. Expand your time range..." message to allow users quickly expand the time range by clicking on a link. Screenshot 2022-12-07 at 14 38 45 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../components/layout/discover_layout.tsx | 14 +- .../components/no_results/no_results.test.tsx | 128 +++++++---- .../main/components/no_results/no_results.tsx | 57 ++--- .../assets/no_results_illustration.scss | 0 .../assets/no_results_illustration.tsx | 4 +- .../no_results_suggestion_default.tsx | 16 +- .../no_results_suggestion_when_filters.tsx | 47 ++-- .../no_results_suggestion_when_query.tsx | 208 ++++++++++++++++-- .../no_results_suggestion_when_time_range.tsx | 30 +-- .../no_results_suggestions.tsx | 150 +++++++++++-- .../syntax_suggestions_popover.tsx | 102 +++++++++ .../use_fetch_occurances_range.ts | 153 +++++++++++++ .../apps/discover/group1/_discover.ts | 9 + test/functional/page_objects/discover_page.ts | 7 + .../translations/translations/fr-FR.json | 7 - .../translations/translations/ja-JP.json | 7 - .../translations/translations/zh-CN.json | 7 - 17 files changed, 738 insertions(+), 208 deletions(-) rename src/plugins/discover/public/application/main/components/no_results/{ => no_results_suggestions}/assets/no_results_illustration.scss (100%) rename src/plugins/discover/public/application/main/components/no_results/{ => no_results_suggestions}/assets/no_results_illustration.tsx (99%) create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/syntax_suggestions_popover.tsx create mode 100644 src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/use_fetch_occurances_range.ts diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index ab8c5da67d04a3..2eec55daf742d7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -19,7 +19,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; -import { isOfQueryType } from '@kbn/es-query'; import classNames from 'classnames'; import { generateFilters } from '@kbn/data-plugin/public'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; @@ -42,7 +41,6 @@ import { DataMainMsg, RecordRawType } from '../../services/discover_data_state_c import { useColumns } from '../../../../hooks/use_data_grid_columns'; import { FetchStatus } from '../../../types'; import { useDataState } from '../../hooks/use_data_state'; -import { hasActiveFilter } from './utils'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout'; import { DiscoverHistogramLayout } from './discover_histogram_layout'; @@ -84,10 +82,9 @@ export function DiscoverLayout({ inspector, } = useDiscoverServices(); const { main$ } = stateContainer.dataState.data$; - const [query, savedQuery, filters, columns, sort] = useAppStateSelector((state) => [ + const [query, savedQuery, columns, sort] = useAppStateSelector((state) => [ state.query, state.savedQuery, - state.filters, state.columns, state.sort, ]); @@ -208,13 +205,16 @@ export function DiscoverLayout({ const mainDisplay = useMemo(() => { if (resultState === 'none') { + const globalQueryState = data.query.getState(); + return ( ); @@ -257,7 +257,6 @@ export function DiscoverLayout({ dataState.error, dataView, expandedDoc, - filters, inspectorAdapters, isPlainRecord, isTimeBased, @@ -265,7 +264,6 @@ export function DiscoverLayout({ onAddFilter, onDisableFilters, onFieldEdited, - query, resetSavedSearch, resultState, savedSearch, diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx index 6ead00f8cce069..98b5d6f46fcc80 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.test.tsx @@ -7,47 +7,83 @@ */ import React from 'react'; +import { ReactWrapper } from 'enzyme'; +import * as RxApi from 'rxjs'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; - -import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { + stubDataView, + stubDataViewWithoutTimeField, +} from '@kbn/data-views-plugin/common/data_view.stub'; +import { type Filter } from '@kbn/es-query'; +import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results'; +import { createDiscoverServicesMock } from '../../../../__mocks__/services'; -beforeEach(() => { - jest.clearAllMocks(); -}); - -function mountAndFindSubjects(props: Omit) { - const services = { - docLinks: { - links: { - query: { - luceneQuerySyntax: 'documentation-link', - }, +jest.spyOn(RxApi, 'lastValueFrom').mockImplementation(async () => ({ + rawResponse: { + aggregations: { + earliest_timestamp: { + value_as_string: '2020-09-01T08:30:00.000Z', + }, + latest_timestamp: { + value_as_string: '2022-09-01T08:30:00.000Z', }, }, - }; - const component = mountWithIntl( - - {}} {...props} /> - - ); + }, +})); + +async function mountAndFindSubjects( + props: Omit +) { + const services = createDiscoverServicesMock(); + + let component: ReactWrapper; + + await act(async () => { + component = await mountWithIntl( + + {}} + {...props} + /> + + ); + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + await act(async () => { + await component!.update(); + }); + return { - mainMsg: findTestSubject(component, 'discoverNoResults').exists(), - errorMsg: findTestSubject(component, 'discoverNoResultsError').exists(), - adjustTimeRange: findTestSubject(component, 'discoverNoResultsTimefilter').exists(), - adjustSearch: findTestSubject(component, 'discoverNoResultsAdjustSearch').exists(), - adjustFilters: findTestSubject(component, 'discoverNoResultsAdjustFilters').exists(), - checkIndices: findTestSubject(component, 'discoverNoResultsCheckIndices').exists(), - disableFiltersButton: findTestSubject(component, 'discoverNoResultsDisableFilters').exists(), + mainMsg: findTestSubject(component!, 'discoverNoResults').exists(), + errorMsg: findTestSubject(component!, 'discoverNoResultsError').exists(), + adjustTimeRange: findTestSubject(component!, 'discoverNoResultsTimefilter').exists(), + adjustSearch: findTestSubject(component!, 'discoverNoResultsAdjustSearch').exists(), + adjustFilters: findTestSubject(component!, 'discoverNoResultsAdjustFilters').exists(), + checkIndices: findTestSubject(component!, 'discoverNoResultsCheckIndices').exists(), + disableFiltersButton: findTestSubject(component!, 'discoverNoResultsDisableFilters').exists(), + viewMatchesButton: findTestSubject(component!, 'discoverNoResultsViewAllMatches').exists(), }; } describe('DiscoverNoResults', () => { + beforeEach(() => { + (RxApi.lastValueFrom as jest.Mock).mockClear(); + }); + describe('props', () => { describe('no props', () => { - test('renders default feedback', () => { - const result = mountAndFindSubjects({}); + test('renders default feedback', async () => { + const result = await mountAndFindSubjects({ + dataView: stubDataViewWithoutTimeField, + query: undefined, + filters: undefined, + }); expect(result).toMatchInlineSnapshot(` Object { "adjustFilters": false, @@ -57,14 +93,17 @@ describe('DiscoverNoResults', () => { "disableFiltersButton": false, "errorMsg": false, "mainMsg": true, + "viewMatchesButton": false, } `); }); }); describe('timeFieldName', () => { - test('renders time range feedback', () => { - const result = mountAndFindSubjects({ - isTimeBased: true, + test('renders time range feedback', async () => { + const result = await mountAndFindSubjects({ + dataView: stubDataView, + query: { language: 'lucene', query: '' }, + filters: [], }); expect(result).toMatchInlineSnapshot(` Object { @@ -75,30 +114,42 @@ describe('DiscoverNoResults', () => { "disableFiltersButton": false, "errorMsg": false, "mainMsg": true, + "viewMatchesButton": true, } `); + expect(RxApi.lastValueFrom).toHaveBeenCalledTimes(1); }); }); describe('filter/query', () => { - test('shows "adjust search" message when having query', () => { - const result = mountAndFindSubjects({ hasQuery: true }); + test('shows "adjust search" message when having query', async () => { + const result = await mountAndFindSubjects({ + dataView: stubDataView, + query: { language: 'lucene', query: '*' }, + filters: undefined, + }); expect(result).toHaveProperty('adjustSearch', true); }); - test('shows "adjust filters" message when having filters', () => { - const result = mountAndFindSubjects({ hasFilters: true }); + test('shows "adjust filters" message when having filters', async () => { + const result = await mountAndFindSubjects({ + dataView: stubDataView, + query: { language: 'lucene', query: '' }, + filters: [{} as Filter], + }); expect(result).toHaveProperty('adjustFilters', true); expect(result).toHaveProperty('disableFiltersButton', true); }); }); describe('error message', () => { - test('renders error message', () => { + test('renders error message', async () => { const error = new Error('Fatal error'); - const result = mountAndFindSubjects({ - isTimeBased: true, + const result = await mountAndFindSubjects({ + dataView: stubDataView, error, + query: { language: 'lucene', query: '' }, + filters: [{} as Filter], }); expect(result).toMatchInlineSnapshot(` Object { @@ -109,6 +160,7 @@ describe('DiscoverNoResults', () => { "disableFiltersButton": false, "errorMsg": true, "mainMsg": false, + "viewMatchesButton": false, } `); }); diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx index 4e6f4425f56e0e..c24423693a6224 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -8,60 +8,41 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { NoResultsSuggestions } from './no_results_suggestions'; import './_no_results.scss'; -import { NoResultsIllustration } from './assets/no_results_illustration'; export interface DiscoverNoResultsProps { isTimeBased?: boolean; + query: Query | AggregateQuery | undefined; + filters: Filter[] | undefined; error?: Error; - data?: DataPublicPluginStart; - hasQuery?: boolean; - hasFilters?: boolean; + data: DataPublicPluginStart; + dataView: DataView; onDisableFilters: () => void; } export function DiscoverNoResults({ isTimeBased, + query, + filters, error, data, - hasFilters, - hasQuery, + dataView, onDisableFilters, }: DiscoverNoResultsProps) { const callOut = !error ? ( - - -

- -

-
- - - - - - - - - + + ) : ( diff --git a/src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.scss b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/assets/no_results_illustration.scss similarity index 100% rename from src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.scss rename to src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/assets/no_results_illustration.scss diff --git a/src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/assets/no_results_illustration.tsx similarity index 99% rename from src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.tsx rename to src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/assets/no_results_illustration.tsx index b96fad88f1b443..10fc01537688a9 100644 --- a/src/plugins/discover/public/application/main/components/no_results/assets/no_results_illustration.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/assets/no_results_illustration.tsx @@ -12,8 +12,8 @@ import React from 'react'; export const NoResultsIllustration = () => ( diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx index b232b4138ea69b..b90ca64c23e645 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_default.tsx @@ -8,17 +8,15 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiDescriptionList, EuiDescriptionListDescription } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; export function NoResultsSuggestionDefault() { return ( - - - - - + + + ); } diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx index b153f6046b1041..4112161aa5f294 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_filters.tsx @@ -8,12 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiDescriptionList, - EuiDescriptionListTitle, - EuiLink, - EuiDescriptionListDescription, -} from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; export interface NoResultsSuggestionWhenFiltersProps { onDisableFilters: () => void; @@ -23,29 +18,21 @@ export function NoResultsSuggestionWhenFilters({ onDisableFilters, }: NoResultsSuggestionWhenFiltersProps) { return ( - - - - - - - - - ), - }} - /> - - + + + + + ), + }} + /> + ); } diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx index 166b2a7f742cdc..d6ecb53a8025f6 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_query.tsx @@ -7,25 +7,199 @@ */ import React from 'react'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLink } from '@elastic/eui'; +import { SyntaxExamples, SyntaxSuggestionsPopover } from './syntax_suggestions_popover'; +import { type DiscoverServices } from '../../../../../build_services'; +import { useDiscoverServices } from '../../../../../hooks/use_discover_services'; -export function NoResultsSuggestionWhenQuery() { - return ( - - - - - +const getExamples = ( + querySyntax: string | undefined, + docLinks: DiscoverServices['docLinks'] +): SyntaxExamples | null => { + if (!querySyntax) { + return null; + } + + if (querySyntax === 'lucene') { + return { + title: i18n.translate('discover.noResults.luceneExamples.title', { + defaultMessage: 'Lucene examples', + }), + items: [ + { + label: i18n.translate( + 'discover.noResults.luceneExamples.findRequestsThatContain200Text', + { + defaultMessage: 'Find requests that contain the number 200, in any field', + } + ), + example: '200', + }, + { + label: i18n.translate('discover.noResults.luceneExamples.find200InStatusFieldText', { + defaultMessage: 'Find 200 in the status field', + }), + example: 'status:200', + }, + { + label: i18n.translate('discover.noResults.luceneExamples.findAllStatusCodesText', { + defaultMessage: 'Find all status codes between 400-499', + }), + example: 'status:[400 TO 499]', + }, + { + label: i18n.translate('discover.noResults.luceneExamples.findStatusCodesWithPHPText', { + defaultMessage: 'Find status codes 400-499 with the extension php', + }), + example: 'status:[400 TO 499] AND extension:PHP', + }, + { + label: i18n.translate( + 'discover.noResults.luceneExamples.findStatusCodesWithPhpOrHtmlText', + { + defaultMessage: 'Find status codes 400-499 with the extension php or html', + } + ), + example: 'status:[400 TO 499] AND (extension:php OR extension:html)', + }, + ], + footer: ( + + + ), + }} /> - - - ); + ), + }; + } + + if (querySyntax === 'kuery') { + return { + title: i18n.translate('discover.noResults.kqlExamples.title', { + defaultMessage: 'KQL examples', + }), + items: [ + { + label: i18n.translate('discover.noResults.kqlExamples.filterForExistingFieldsText', { + defaultMessage: 'Filter for documents where a field exists', + }), + example: 'http.request.method: *', + }, + { + label: i18n.translate('discover.noResults.kqlExamples.filterForDocsThatMatchValueText', { + defaultMessage: 'Filter for documents that match a value', + }), + example: 'http.request.method: GET', + }, + { + label: i18n.translate('discover.noResults.kqlExamples.filterForDocsWithinRangeText', { + defaultMessage: 'Filter for documents within a range', + }), + example: 'http.response.bytes > 10000 and http.response.bytes <= 20000', + }, + { + label: i18n.translate('discover.noResults.kqlExamples.filterForDocsWithWildcardsText', { + defaultMessage: 'Filter for documents using wildcards', + }), + example: 'http.response.status_code: 4*', + }, + { + label: i18n.translate('discover.noResults.kqlExamples.negatingQueryText', { + defaultMessage: 'Negating a query', + }), + example: 'NOT http.request.method: GET', + }, + { + label: i18n.translate('discover.noResults.kqlExamples.combineMultipleText', { + defaultMessage: 'Combining multiple queries with AND/OR', + }), + example: 'http.request.method: GET AND http.response.status_code: 400', + }, + { + label: i18n.translate('discover.noResults.kqlExamples.queryMultipleText', { + defaultMessage: 'Querying multiple values for the same field', + }), + example: 'http.request.method: (GET OR POST OR DELETE)', + }, + ], + footer: ( + + + + ), + }} + /> + ), + }; + } + + return null; +}; + +export interface NoResultsSuggestionWhenQueryProps { + querySyntax: string | undefined; } + +export const NoResultsSuggestionWhenQuery: React.FC = ({ + querySyntax, +}) => { + const services = useDiscoverServices(); + const { docLinks } = services; + const examplesMeta = getExamples(querySyntax, docLinks); + + return ( + <> + + + + {examplesMeta ? ( + + ) : ( + + )} + + + {!!examplesMeta && ( + + + + )} + + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx index 434d6025b950ed..41f36b446778e4 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestion_when_time_range.tsx @@ -8,27 +8,15 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, -} from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; -export function NoResultsSuggestionWhenTimeRange() { +export const NoResultsSuggestionWhenTimeRange: React.FC = () => { return ( - - - - - - - - + + + ); -} +}; diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx index 595ca61225ebb1..e9cd75e022db33 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx @@ -6,8 +6,18 @@ * Side Public License, v 1. */ -import React from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; +import { css } from '@emotion/react'; +import { EuiEmptyPrompt, EuiButton, EuiLoadingSpinner, EuiSpacer, useEuiTheme } from '@elastic/eui'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { + isOfQueryType, + isOfAggregateQueryType, + type Query, + type AggregateQuery, + type Filter, +} from '@kbn/es-query'; +import { FormattedMessage } from '@kbn/i18n-react'; import { NoResultsSuggestionDefault } from './no_results_suggestion_default'; import { NoResultsSuggestionWhenFilters, @@ -15,41 +25,133 @@ import { } from './no_results_suggestion_when_filters'; import { NoResultsSuggestionWhenQuery } from './no_results_suggestion_when_query'; import { NoResultsSuggestionWhenTimeRange } from './no_results_suggestion_when_time_range'; +import { hasActiveFilter } from '../../layout/utils'; +import { useDiscoverServices } from '../../../../../hooks/use_discover_services'; +import { useFetchOccurrencesRange } from './use_fetch_occurances_range'; +import { NoResultsIllustration } from './assets/no_results_illustration'; interface NoResultsSuggestionProps { - hasFilters?: boolean; - hasQuery?: boolean; + dataView: DataView; isTimeBased?: boolean; + query: Query | AggregateQuery | undefined; + filters: Filter[] | undefined; onDisableFilters: NoResultsSuggestionWhenFiltersProps['onDisableFilters']; } -export function NoResultsSuggestions({ +export const NoResultsSuggestions: React.FC = ({ + dataView, isTimeBased, - hasFilters, - hasQuery, + query, + filters, onDisableFilters, -}: NoResultsSuggestionProps) { +}) => { + const { euiTheme } = useEuiTheme(); + const services = useDiscoverServices(); + const { data, uiSettings, timefilter } = services; + const hasQuery = + (isOfQueryType(query) && !!query?.query) || (!!query && isOfAggregateQueryType(query)); + const hasFilters = hasActiveFilter(filters); + + const [isExtending, setIsExtending] = useState(false); + const { range: occurrencesRange, refetch } = useFetchOccurrencesRange({ + dataView, + query, + filters, + services: { + data, + uiSettings, + }, + }); + + const extendTimeRange = async () => { + setIsExtending(true); + const range = await refetch(); + if (range?.from && range?.to) { + timefilter.setTime({ + from: range.from, + to: range.to, + }); + } else { + setIsExtending(false); + } + }; + + const canExtendTimeRange = Boolean(occurrencesRange?.from && occurrencesRange.to); const canAdjustSearchCriteria = isTimeBased || hasFilters || hasQuery; - if (canAdjustSearchCriteria) { - return ( - <> - {isTimeBased && } + const body = canAdjustSearchCriteria ? ( + <> + + +
    + {isTimeBased && ( +
  • + +
  • + )} {hasQuery && ( - <> - - - +
  • + +
  • )} {hasFilters && ( - <> - +
  • - +
  • )} - - ); - } +
+ + ) : ( + + ); - return ; -} + return ( + } + title={ +

+ +

+ } + body={body} + actions={ +
+ {typeof occurrencesRange === 'undefined' ? ( + + ) : canExtendTimeRange ? ( + + + + ) : null} +
+ } + /> + ); +}; diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/syntax_suggestions_popover.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/syntax_suggestions_popover.tsx new file mode 100644 index 00000000000000..4e5e3ba3796cdf --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/syntax_suggestions_popover.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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, { useState } from 'react'; +import { css } from '@emotion/react'; +import { + EuiBasicTable, + EuiButtonIcon, + EuiPanel, + EuiPopover, + EuiPopoverTitle, + EuiCode, + EuiText, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface SyntaxExample { + label: string; + example: string; +} + +export interface SyntaxExamples { + title: string; + footer: React.ReactElement; + items: SyntaxExample[]; +} + +export interface SyntaxSuggestionsPopoverProps { + meta: SyntaxExamples; +} + +export const SyntaxSuggestionsPopover: React.FC = ({ meta }) => { + const [isOpen, setIsOpen] = useState(false); + const { title, items, footer } = meta; + + const helpButton = ( + setIsOpen((prev) => !prev)} + iconType="documentation" + aria-label={title} + /> + ); + + const columns = [ + { + field: 'label', + name: i18n.translate('discover.noResults.suggestion.syntaxPopoverDescriptionHeader', { + defaultMessage: 'Description', + }), + width: '200px', + }, + { + field: 'example', + name: i18n.translate('discover.noResults.suggestion.syntaxPopoverExampleHeader', { + defaultMessage: 'Example', + }), + render: (example: string) => {example}, + }, + ]; + + return ( + setIsOpen(false)} + initialFocus="#querySyntaxBasicTableId" + > + {title} + + + id="querySyntaxBasicTableId" + tableCaption={title} + items={items} + compressed={true} + rowHeader="label" + columns={columns} + responsive + /> + + + {footer} + + + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/use_fetch_occurances_range.ts b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/use_fetch_occurances_range.ts new file mode 100644 index 00000000000000..8ae801fc930396 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/use_fetch_occurances_range.ts @@ -0,0 +1,153 @@ +/* + * 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 { useCallback, useEffect, useRef, useState } from 'react'; +import { lastValueFrom } from 'rxjs'; +import type { DataView } from '@kbn/data-plugin/common'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import type { AggregationsSingleMetricAggregateBase } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { buildEsQuery } from '@kbn/es-query'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; + +export interface Params { + dataView?: DataView; + query?: Query | AggregateQuery; + filters?: Filter[]; + services: { + data: DataPublicPluginStart; + uiSettings: IUiSettingsClient; + }; +} + +export interface OccurrencesRange { + from: string; + to: string; +} + +export interface Result { + range: OccurrencesRange | null | undefined; + refetch: () => Promise; +} + +export const useFetchOccurrencesRange = (params: Params): Result => { + const data = params.services.data; + const uiSettings = params.services.uiSettings; + const [range, setRange] = useState(undefined); + const abortControllerRef = useRef(null); + const mountedRef = useRef(true); + + const fetchOccurrences = useCallback( + async (dataView?: DataView, query?: Query | AggregateQuery, filters?: Filter[]) => { + let occurrencesRange = null; + if (!dataView?.timeFieldName || !query || !mountedRef.current) { + return null; + } + + abortControllerRef.current?.abort(); + abortControllerRef.current = new AbortController(); + + try { + const dslQuery = buildEsQuery( + dataView, + query ?? [], + filters ?? [], + getEsQueryConfig(uiSettings) + ); + occurrencesRange = await fetchDocumentsTimeRange({ + data, + dataView, + dslQuery, + abortSignal: abortControllerRef.current?.signal, + }); + } catch (error) { + // + } + + if (mountedRef.current) { + setRange(occurrencesRange); + } + + return occurrencesRange; + }, + [abortControllerRef, setRange, mountedRef, data, uiSettings] + ); + + useEffect(() => { + return () => { + mountedRef.current = false; + abortControllerRef.current?.abort(); + }; + }, [abortControllerRef, mountedRef]); + + useEffect(() => { + fetchOccurrences(params.dataView, params.query, params.filters); + }, [fetchOccurrences, params.query, params.filters, params.dataView]); + + return { + range, + refetch: () => fetchOccurrences(params.dataView, params.query, params.filters), + }; +}; + +async function fetchDocumentsTimeRange({ + data, + dataView, + dslQuery, + abortSignal, +}: { + data: DataPublicPluginStart; + dataView: DataView; + dslQuery?: object; + abortSignal?: AbortSignal; +}): Promise { + if (!dataView?.timeFieldName) { + return null; + } + + const result = await lastValueFrom( + data.search.search( + { + params: { + index: dataView.title, + size: 0, + body: { + query: dslQuery ?? { match_all: {} }, + aggs: { + earliest_timestamp: { + min: { + field: dataView.timeFieldName, + }, + }, + latest_timestamp: { + max: { + field: dataView.timeFieldName, + }, + }, + }, + }, + }, + }, + { + abortSignal, + } + ) + ); + + const earliestTimestamp = ( + result.rawResponse?.aggregations?.earliest_timestamp as AggregationsSingleMetricAggregateBase + )?.value_as_string; + const latestTimestamp = ( + result.rawResponse?.aggregations?.latest_timestamp as AggregationsSingleMetricAggregateBase + )?.value_as_string; + + return earliestTimestamp && latestTimestamp + ? { from: earliestTimestamp, to: latestTimestamp } + : null; +} diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index 1cba5aa4812d8f..c3be7257374707 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -159,6 +159,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const isVisible = await PageObjects.discover.hasNoResultsTimepicker(); expect(isVisible).to.be(true); }); + + it('should show matches when time range is expanded', async () => { + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await retry.try(async function () { + expect(await PageObjects.discover.hasNoResults()).to.be(false); + expect(await PageObjects.discover.getHitCountInt()).to.be.above(0); + }); + }); }); describe('nested query', () => { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 46d2bd94423f9e..8d4438ea91e1ba 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -457,6 +457,13 @@ export class DiscoverPageObject extends FtrService { return await this.testSubjects.exists('discoverNoResultsTimefilter'); } + public async expandTimeRangeAsSuggestedInNoResultsMessage() { + await this.retry.waitFor('the button before pressing it', async () => { + return await this.testSubjects.exists('discoverNoResultsViewAllMatches'); + }); + return await this.testSubjects.click('discoverNoResultsViewAllMatches'); + } + public async getSidebarAriaDescription(): Promise { return await ( await this.testSubjects.find('fieldListGrouped__ariaDescription') diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b7950b56bcca50..9b8635876b4ca9 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2079,7 +2079,6 @@ "discover.gridSampleSize.description": "Vous voyez les {sampleSize} premiers échantillons de documents qui correspondent à votre recherche. Pour modifier cette valeur, accédez à {advancedSettingsLink}.", "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.savedSearchAliasMatchRedirect.objectNoun": "Recherche {savedSearch}", "discover.savedSearchURLConflictCallout.objectNoun": "Recherche {savedSearch}", @@ -2321,15 +2320,9 @@ "discover.localMenu.shareSearchDescription": "Partager la recherche", "discover.localMenu.shareTitle": "Partager", "discover.noMatchRoute.bannerTitleText": "Page introuvable", - "discover.noResults.adjustFilters": "Modifiez les filtres.", - "discover.noResults.adjustSearch": "Modifiez la requête.", - "discover.noResults.expandYourTimeRangeTitle": "Étendre la plage temporelle", "discover.noResults.noDocumentsOrCheckPermissionsDescription": "Assurez-vous de disposer de l'autorisation d'afficher les index et vérifiez qu'ils contiennent des documents.", - "discover.noResults.queryMayNotMatchTitle": "Essayez de rechercher sur une période plus longue.", "discover.noResults.searchExamples.noResultsBecauseOfError": "Une erreur s’est produite lors de la récupération des résultats de recherche.", "discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "Aucun résultat ne correspond à vos critères de recherche.", - "discover.noResults.temporaryDisablingFiltersLinkText": "désactiver temporairement les filtres", - "discover.noResults.trySearchingForDifferentCombination": "Essayez de rechercher une autre combinaison de termes.", "discover.noResultsFound": "Résultat introuvable", "discover.notifications.invalidTimeRangeText": "La plage temporelle spécifiée n'est pas valide (de : \"{from}\" à \"{to}\").", "discover.notifications.invalidTimeRangeTitle": "Plage temporelle non valide", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 772e8ac9eaecf1..5bdecf9a74dc1f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2077,7 +2077,6 @@ "discover.gridSampleSize.description": "検索と一致する最初の{sampleSize}ドキュメントを表示しています。この値を変更するには、{advancedSettingsLink}に移動してください。", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.noMatchRoute.bannerText": "Discoverアプリケーションはこのルート{route}を認識できません", - "discover.noResults.tryRemovingOrDisablingFilters": "削除または{disablingFiltersLink}してください。", "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", "discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch}検索", "discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch}検索", @@ -2319,15 +2318,9 @@ "discover.localMenu.shareSearchDescription": "検索を共有します", "discover.localMenu.shareTitle": "共有", "discover.noMatchRoute.bannerTitleText": "ページが見つかりません", - "discover.noResults.adjustFilters": "フィルターを調整", - "discover.noResults.adjustSearch": "クエリを調整", - "discover.noResults.expandYourTimeRangeTitle": "時間範囲を拡大", "discover.noResults.noDocumentsOrCheckPermissionsDescription": "インデックスと含まれるドキュメントを表示する権限がありません。", - "discover.noResults.queryMayNotMatchTitle": "期間を長くして検索を試してください。", "discover.noResults.searchExamples.noResultsBecauseOfError": "検索結果の取得中にエラーが発生しました", "discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "検索条件と一致する結果がありません。", - "discover.noResults.temporaryDisablingFiltersLinkText": "フィルターを一時的に無効にしています", - "discover.noResults.trySearchingForDifferentCombination": "別の用語の組み合わせを検索してください。", "discover.noResultsFound": "結果が見つかりませんでした", "discover.notifications.invalidTimeRangeText": "指定された時間範囲が無効です。(開始:'{from}'、終了:'{to}')", "discover.notifications.invalidTimeRangeTitle": "無効な時間範囲", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f705f8d765a44a..fe57899b38b34c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2081,7 +2081,6 @@ "discover.gridSampleSize.description": "您正查看与您的搜索相匹配的前 {sampleSize} 个文档。要更改此值,请转到{advancedSettingsLink}。", "discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.noMatchRoute.bannerText": "Discover 应用程序无法识别此路由:{route}", - "discover.noResults.tryRemovingOrDisablingFilters": "尝试删除或{disablingFiltersLink}。", "discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}", "discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch} 搜索", "discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch} 搜索", @@ -2323,15 +2322,9 @@ "discover.localMenu.shareSearchDescription": "共享搜索", "discover.localMenu.shareTitle": "共享", "discover.noMatchRoute.bannerTitleText": "未找到页面", - "discover.noResults.adjustFilters": "调整您的筛选", - "discover.noResults.adjustSearch": "调整您的查询", - "discover.noResults.expandYourTimeRangeTitle": "展开时间范围", "discover.noResults.noDocumentsOrCheckPermissionsDescription": "确保您有权查看索引并且它们包含文档。", - "discover.noResults.queryMayNotMatchTitle": "尝试搜索更长的时间段。", "discover.noResults.searchExamples.noResultsBecauseOfError": "检索搜索结果时遇到问题", "discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "没有任何结果匹配您的搜索条件", - "discover.noResults.temporaryDisablingFiltersLinkText": "正临时禁用筛选", - "discover.noResults.trySearchingForDifferentCombination": "尝试搜索不同的词组合。", "discover.noResultsFound": "找不到结果", "discover.notifications.invalidTimeRangeText": "提供的时间范围无效。(自:“{from}”,至:“{to}”)", "discover.notifications.invalidTimeRangeTitle": "时间范围无效", From 0575f43377d102c67ba192c8149e4e17f968df37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 1 Feb 2023 12:36:40 +0100 Subject: [PATCH 34/59] [APM] Remove `host.name` correlation (#150005) Closes https://github.com/elastic/kibana/issues/148788 --- .../components/app/service_logs/index.test.ts | 6 ++--- .../components/app/service_logs/index.tsx | 22 ++++++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts index dc0c486deeed94..2c3f5b49460fe1 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts @@ -38,7 +38,7 @@ describe('service logs', () => { ); }); - it('filter by host names as fallback', () => { + it('does not filter by host names as fallback', () => { expect( getInfrastructureKQLFilter( { @@ -48,9 +48,7 @@ describe('service logs', () => { }, serviceName ) - ).toEqual( - 'service.name: "opbeans-node" or (not service.name and (host.name: "baz" or host.name: "quz"))' - ); + ).toEqual('service.name: "opbeans-node"'); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index 6616eee3d85d16..02cdf967939823 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -12,11 +12,7 @@ import { useFetcher } from '../../../hooks/use_fetcher'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; -import { - CONTAINER_ID, - HOST_NAME, - SERVICE_NAME, -} from '../../../../common/es_fields/apm'; +import { CONTAINER_ID, SERVICE_NAME } from '../../../../common/es_fields/apm'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -70,16 +66,12 @@ export const getInfrastructureKQLFilter = ( | undefined, serviceName: string ) => { - const containerIds = data?.containerIds ?? []; - const hostNames = data?.hostNames ?? []; + const containerIds: string[] = data?.containerIds ?? []; + const containerIdKql = containerIds + .map((id) => `${CONTAINER_ID}: "${id}"`) + .join(' or '); - const infraAttributes = containerIds.length - ? containerIds.map((id) => `${CONTAINER_ID}: "${id}"`) - : hostNames.map((id) => `${HOST_NAME}: "${id}"`); - - const infraAttributesJoined = infraAttributes.join(' or '); - - return infraAttributes.length - ? `${SERVICE_NAME}: "${serviceName}" or (not ${SERVICE_NAME} and (${infraAttributesJoined}))` + return containerIds.length + ? `${SERVICE_NAME}: "${serviceName}" or (not ${SERVICE_NAME} and (${containerIdKql}))` : `${SERVICE_NAME}: "${serviceName}"`; }; From ef00463019c0ebce609070d25cee6eed59a33909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 1 Feb 2023 13:00:15 +0100 Subject: [PATCH 35/59] [Enterprise Search] Delete engine from Engine Overview (#149124) ## Summary Adds delete functionality on Engine overview. ![Screenshot 2023-01-18 at 15 14 14](https://user-images.githubusercontent.com/1410658/213194949-3f76a769-e634-47ff-8c7e-e2391d427ff4.png) ![Screenshot 2023-01-18 at 15 14 19](https://user-images.githubusercontent.com/1410658/213194955-f0a9c693-a618-423e-94fe-4e1c38c7beea.png) ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../components/engine/engine_indices.tsx | 35 +++++--- .../components/engine/engine_view.tsx | 54 ++++++++----- .../engine/engine_view_header_actions.tsx | 80 +++++++++++++++++++ .../engine/engine_view_logic.test.ts | 22 +++++ .../components/engine/engine_view_logic.ts | 36 ++++++++- .../engines/delete_engine_modal.tsx | 72 ++++++++--------- .../components/engines/engines_list.tsx | 27 +++++-- ...gic.test.ts => engines_list_logic.test.ts} | 0 .../components/engines/engines_list_logic.ts | 8 +- 9 files changed, 257 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx rename x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/{engine_list_logic.test.ts => engines_list_logic.test.ts} (100%) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx index fd6aa9f145e828..0bcdde5e47647d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx @@ -13,6 +13,8 @@ import { EuiBasicTableColumn, EuiButton, EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, EuiIcon, EuiInMemoryTable, EuiText, @@ -26,13 +28,16 @@ import { indexHealthToHealthColor } from '../../../shared/constants/health_color import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; + import { SEARCH_INDEX_PATH, EngineViewTabs } from '../../routes'; import { IngestionMethod } from '../../types'; import { ingestionMethodToText } from '../../utils/indices'; + import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; import { AddIndicesFlyout } from './add_indices_flyout'; import { EngineIndicesLogic } from './engine_indices_logic'; +import { EngineViewHeaderActions } from './engine_view_header_actions'; export const EngineIndices: React.FC = () => { const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } = @@ -172,16 +177,26 @@ export const EngineIndices: React.FC = () => { defaultMessage: 'Indices', }), rightSideItems: [ - - {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { - defaultMessage: 'Add new indices', - })} - , + + + + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', + { + defaultMessage: 'Add new indices', + } + )} + + + + + + , ], }} engineName={engineName} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx index 05b34088b1797d..e6057a2d1b2bd2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx @@ -15,18 +15,25 @@ import { Status } from '../../../../../common/types/api'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH, EngineViewTabs } from '../../routes'; +import { DeleteEngineModal } from '../engines/delete_engine_modal'; import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; import { EngineAPI } from './engine_api/engine_api'; import { EngineError } from './engine_error'; import { EngineIndices } from './engine_indices'; +import { EngineViewHeaderActions } from './engine_view_header_actions'; import { EngineViewLogic } from './engine_view_logic'; import { EngineHeaderDocsAction } from './header_docs_action'; export const EngineView: React.FC = () => { - const { fetchEngine } = useActions(EngineViewLogic); - const { engineName, fetchEngineApiError, fetchEngineApiStatus, isLoadingEngine } = - useValues(EngineViewLogic); + const { fetchEngine, closeDeleteEngineModal } = useActions(EngineViewLogic); + const { + engineName, + fetchEngineApiError, + fetchEngineApiStatus, + isDeleteModalVisible, + isLoadingEngine, + } = useValues(EngineViewLogic); const { tabId = EngineViewTabs.OVERVIEW } = useParams<{ tabId?: string; }>(); @@ -54,23 +61,28 @@ export const EngineView: React.FC = () => { } return ( - - - - ( - - )} - /> - + <> + {isDeleteModalVisible ? ( + + ) : null} + + + + ( + ], + }} + engineName={engineName} + isLoading={isLoadingEngine} + /> + )} + /> + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.tsx new file mode 100644 index 00000000000000..ba563a9c23dbcc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_header_actions.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiPopover, EuiButtonIcon, EuiText, EuiContextMenu, EuiIcon } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { EngineViewLogic } from './engine_view_logic'; + +export const EngineViewHeaderActions: React.FC = () => { + const { engineData } = useValues(EngineViewLogic); + + const { openDeleteEngineModal } = useActions(EngineViewLogic); + + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const toggleActionsPopover = () => setIsActionsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + const closePopover = () => setIsActionsPopoverOpen(false); + return ( + <> + + } + isOpen={isActionsPopoverOpen} + panelPaddingSize="xs" + closePopover={closePopover} + display="block" + > + , + name: ( + + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.headerActions.delete', + { defaultMessage: 'Delete this engine' } + )} + + ), + onClick: () => { + if (engineData) { + openDeleteEngineModal(); + } + }, + size: 's', + }, + ], + }, + ]} + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.test.ts index 18ad97c54e6493..48d85c9f0c6ac7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.test.ts @@ -9,6 +9,11 @@ import { LogicMounter } from '../../../__mocks__/kea_logic'; import { Status } from '../../../../../common/types/api'; +import { KibanaLogic } from '../../../shared/kibana'; +import { DeleteEnginesApiLogicResponse } from '../../api/engines/delete_engines_api_logic'; +import { ENGINES_PATH } from '../../routes'; +import { EnginesListLogic } from '../engines/engines_list_logic'; + import { EngineViewLogic, EngineViewValues } from './engine_view_logic'; const DEFAULT_VALUES: EngineViewValues = { @@ -16,19 +21,36 @@ const DEFAULT_VALUES: EngineViewValues = { engineName: 'my-test-engine', fetchEngineApiError: undefined, fetchEngineApiStatus: Status.IDLE, + isDeleteModalVisible: false, isLoadingEngine: true, }; describe('EngineViewLogic', () => { const { mount } = new LogicMounter(EngineViewLogic); + const { mount: mountEnginesListLogic } = new LogicMounter(EnginesListLogic); beforeEach(() => { jest.clearAllMocks(); jest.useRealTimers(); + mountEnginesListLogic(); mount({ engineName: DEFAULT_VALUES.engineName }, { engineName: DEFAULT_VALUES.engineName }); }); it('has expected default values', () => { expect(EngineViewLogic.values).toEqual(DEFAULT_VALUES); }); + + describe('listeners', () => { + describe('deleteSuccess', () => { + it('should navigate to the engines list when an engine is deleted', () => { + jest.spyOn(EngineViewLogic.actions, 'deleteSuccess'); + jest + .spyOn(KibanaLogic.values, 'navigateToUrl') + .mockImplementationOnce(() => Promise.resolve()); + EnginesListLogic.actions.deleteSuccess({} as DeleteEnginesApiLogicResponse); + + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(ENGINES_PATH); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.ts index 021ac6d9ea626f..1cc3bfc5f4e906 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view_logic.ts @@ -9,15 +9,24 @@ import { kea, MakeLogicType } from 'kea'; import { Status } from '../../../../../common/types/api'; +import { KibanaLogic } from '../../../shared/kibana'; + import { FetchEngineApiLogic, FetchEngineApiLogicActions, } from '../../api/engines/fetch_engine_api_logic'; +import { ENGINES_PATH } from '../../routes'; + +import { EnginesListLogic, EnginesListActions } from '../engines/engines_list_logic'; + import { EngineNameLogic } from './engine_name_logic'; export interface EngineViewActions { + closeDeleteEngineModal(): void; + deleteSuccess: EnginesListActions['deleteSuccess']; fetchEngine: FetchEngineApiLogicActions['makeRequest']; + openDeleteEngineModal(): void; } export interface EngineViewValues { @@ -25,12 +34,18 @@ export interface EngineViewValues { engineName: typeof EngineNameLogic.values.engineName; fetchEngineApiError?: typeof FetchEngineApiLogic.values.error; fetchEngineApiStatus: typeof FetchEngineApiLogic.values.status; + isDeleteModalVisible: boolean; isLoadingEngine: boolean; } export const EngineViewLogic = kea>({ connect: { - actions: [FetchEngineApiLogic, ['makeRequest as fetchEngine']], + actions: [ + FetchEngineApiLogic, + ['makeRequest as fetchEngine'], + EnginesListLogic, + ['deleteSuccess'], + ], values: [ EngineNameLogic, ['engineName'], @@ -38,7 +53,26 @@ export const EngineViewLogic = kea ({ + deleteSuccess: () => { + actions.closeDeleteEngineModal(); + KibanaLogic.values.navigateToUrl(ENGINES_PATH); + }, + }), path: ['enterprise_search', 'content', 'engine_view_logic'], + reducers: () => ({ + isDeleteModalVisible: [ + false, + { + closeDeleteEngineModal: () => false, + openDeleteEngineModal: () => true, + }, + ], + }), selectors: ({ selectors }) => ({ isLoadingEngine: [ () => [selectors.fetchEngineApiStatus, selectors.engineData], diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/delete_engine_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/delete_engine_modal.tsx index 16dfdf50a74707..a8cde672107c2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/delete_engine_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/delete_engine_modal.tsx @@ -15,46 +15,42 @@ import { CANCEL_BUTTON_LABEL } from '../../../shared/constants'; import { EnginesListLogic } from './engines_list_logic'; -export const DeleteEngineModal: React.FC = () => { - const { closeDeleteEngineModal, deleteEngine } = useActions(EnginesListLogic); - const { - deleteModalEngineName: engineName, - isDeleteModalVisible, - isDeleteLoading, - } = useValues(EnginesListLogic); +export interface DeleteEngineModalProps { + engineName: string; + onClose: () => void; +} - if (isDeleteModalVisible) { - return ( - { - deleteEngine({ engineName }); - }} - cancelButtonText={CANCEL_BUTTON_LABEL} - confirmButtonText={i18n.translate( - 'xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title', +export const DeleteEngineModal: React.FC = ({ engineName, onClose }) => { + const { deleteEngine } = useActions(EnginesListLogic); + const { isDeleteLoading } = useValues(EnginesListLogic); + return ( + { + deleteEngine({ engineName }); + }} + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title', + { + defaultMessage: 'Yes, delete this engine ', + } + )} + buttonColor="danger" + isLoading={isDeleteLoading} + > +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description', { - defaultMessage: 'Yes, delete this engine ', + defaultMessage: + 'Deleting your engine is not a reversible action. Your indices will not be affected. ', } )} - buttonColor="danger" - isLoading={isDeleteLoading} - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description', - { - defaultMessage: - 'Deleting your engine is not a reversible action. Your indices will not be affected. ', - } - )} -

-
- ); - } else { - return <>; - } +

+
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index cf97afc5673ec2..7165e915fd9e58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -47,11 +47,26 @@ const CreateButton: React.FC = () => { }; export const EnginesList: React.FC = () => { - const { closeEngineCreate, fetchEngines, onPaginate, openDeleteEngineModal, setSearchQuery } = - useActions(EnginesListLogic); + const { + closeDeleteEngineModal, + closeEngineCreate, + fetchEngines, + onPaginate, + openDeleteEngineModal, + setSearchQuery, + } = useActions(EnginesListLogic); + const { openFetchEngineFlyout } = useActions(EnginesListFlyoutLogic); - const { isLoading, meta, results, createEngineFlyoutOpen, searchQuery } = - useValues(EnginesListLogic); + + const { + createEngineFlyoutOpen, + deleteModalEngineName, + isDeleteModalVisible, + isLoading, + meta, + results, + searchQuery, + } = useValues(EnginesListLogic); const throttledSearchQuery = useThrottle(searchQuery, INPUT_THROTTLE_DELAY_MS); @@ -61,7 +76,9 @@ export const EnginesList: React.FC = () => { return ( <> - + {isDeleteModalVisible ? ( + + ) : null} {createEngineFlyoutOpen && } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engine_list_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.test.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engine_list_logic.test.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.test.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts index a031294c016bce..d83f5b179a862c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list_logic.ts @@ -11,6 +11,7 @@ import { Status } from '../../../../../common/types/api'; import { EnterpriseSearchEngine, + EnterpriseSearchEngineDetails, EnterpriseSearchEnginesResponse, } from '../../../../../common/types/engines'; import { Page } from '../../../../../common/types/pagination'; @@ -33,7 +34,7 @@ interface EuiBasicTableOnChange { page: { index: number }; } -type EnginesListActions = Pick< +export type EnginesListActions = Pick< Actions, 'apiError' | 'apiSuccess' | 'makeRequest' > & { @@ -46,10 +47,13 @@ type EnginesListActions = Pick< fetchEngines(): void; onPaginate(args: EuiBasicTableOnChange): { pageNumber: number }; - openDeleteEngineModal: (engine: EnterpriseSearchEngine) => { engine: EnterpriseSearchEngine }; + openDeleteEngineModal: (engine: EnterpriseSearchEngine | EnterpriseSearchEngineDetails) => { + engine: EnterpriseSearchEngine; + }; openEngineCreate(): void; setSearchQuery(searchQuery: string): { searchQuery: string }; }; + interface EngineListValues { createEngineFlyoutOpen: boolean; data: typeof FetchEnginesAPILogic.values.data; From 538963994344fb9aed9f1de58642b0fa7f9f04f9 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:26:21 +0100 Subject: [PATCH 36/59] [Enterprise Search] Update sitemaps copy (#150010) This updates the copy for the Elastic Web Crawler sitemaps feature. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../crawler/crawler_domain_detail/sitemaps_table.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx index 22b89a2fd3c596..80ae2e886b095f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawler_domain_detail/sitemaps_table.tsx @@ -68,7 +68,8 @@ export const SitemapsTable: React.FC = ({ domain, indexName, description={

{i18n.translate('xpack.enterpriseSearch.crawler.sitemapsTable.description', { - defaultMessage: 'Specify sitemap URLs for the crawler on this domain.', + defaultMessage: + 'Add custom sitemap URLs for this domain. The crawler automatically detects existing sitemaps.', })}

} From b47cbf7b831f022b3c31f21bfe1ea0fc52ae14af Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:36:46 +0100 Subject: [PATCH 37/59] [Security Solution][Endpoint][Response Actions] response action history `execute` action filter behind FF (#150016) > **Note** > **This PR ensures `execute` action filter doesn't show in `8.7`** ## Summary ![Screenshot 2023-02-01 at 11 37 54](https://user-images.githubusercontent.com/1849116/216020161-fda46fee-80b2-49af-b923-49aa2576e501.png) This PR hides the `execute` action in the response action history filter dropdown. Small leftover from elastic/kibana/pull/149589 The [unit tests still use the updated list of commands](https://github.com/elastic/kibana/blob/a373ac73368d9e9173072369c4093f1b09cf64a6/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx#L688) and do not need to change. --- .../endpoint_response_actions_list/components/hooks.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx index cc36974c3fe69b..dbd917107e90c3 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx @@ -242,6 +242,15 @@ export const useActionsLogFilter = ({ return false; } + // TODO: remove this when `execute` is no longer behind FF + // planned for 8.8 + if ( + commandName === 'execute' && + !ExperimentalFeaturesService.get().responseActionExecuteEnabled + ) { + return false; + } + return true; }).map((commandName) => ({ key: commandName, From 7bb6ad17b9ccba60ee142db708d01ebd76017cb6 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 1 Feb 2023 08:02:36 -0500 Subject: [PATCH 38/59] [Fleet] Do not verify package policy unique name if not updated (#149944) --- .../server/services/package_policy.test.ts | 77 +++++++++++++++++++ .../fleet/server/services/package_policy.ts | 6 +- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index ae50ec3f785f37..079c7fe3e05991 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -826,6 +826,83 @@ describe('Package policy service', () => { expect(result.name).toEqual('endpoint-1'); }); + it('should not fail to update if skipUniqueNameVerification: false when the name is not updated but duplicates exists', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + total: 1, + per_page: 1, + page: 1, + saved_objects: [ + { + id: 'existing-package-policy', + type: 'ingest-package-policies', + score: 1, + references: [], + version: '1.0.0', + attributes: { + name: 'endpoint-1', + description: '', + namespace: 'default', + enabled: true, + policy_id: 'policy-id-1', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.9.0', + }, + inputs: [], + }, + }, + ], + }); + savedObjectsClient.get.mockResolvedValue({ + id: 'the-package-policy-id', + type: 'abcd', + references: [], + version: 'test', + attributes: { + name: 'endpoint-1', + }, + }); + savedObjectsClient.update.mockImplementation( + async ( + type: string, + id: string, + attrs: any + ): Promise> => { + savedObjectsClient.get.mockResolvedValue({ + id: 'the-package-policy-id', + type, + references: [], + version: 'test', + attributes: attrs, + }); + return attrs; + } + ); + const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const result = await packagePolicyService.update( + savedObjectsClient, + elasticsearchClient, + 'the-package-policy-id', + { + name: 'endpoint-1', + description: '', + namespace: 'default', + enabled: true, + policy_id: '93c46720-c217-11ea-9906-b5b8a21b268e', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.9.0', + }, + inputs: [], + }, + { skipUniqueNameVerification: false } + ); + expect(result.name).toEqual('endpoint-1'); + }); + it('should throw if the user try to update input vars that are frozen', async () => { const savedObjectsClient = savedObjectsClientMock.create(); const mockPackagePolicy = createPackagePolicyMock(); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index c7152e2d26a3ed..5c4071fec906b1 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -505,7 +505,11 @@ class PackagePolicyClientImpl implements PackagePolicyClient { throw new Error('Package policy not found'); } - if (!options?.skipUniqueNameVerification) { + if ( + packagePolicy.name && + packagePolicy.name !== oldPackagePolicy.name && + !options?.skipUniqueNameVerification + ) { // Check that the name does not exist already but exclude the current package policy const existingPoliciesWithName = await this.list(soClient, { perPage: SO_SEARCH_LIMIT, From 71a2bf91953390c71e092d96bc68f025965fed2b Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Wed, 1 Feb 2023 14:49:34 +0100 Subject: [PATCH 39/59] [ftr] split `alerting_api_integration/spaces_only/config.ts` into small fast configs (#149854) ## Summary Trying to address slow config issue: ``` The following "Functional Tests" configs have durations that exceed the maximum amount of time desired for a single CI job. This is not an error, and if you don't own any of these configs then you can ignore this warning.If you own any of these configs please split them up ASAP and ask Operations if you have questions about how to do that. x-pack/test/alerting_api_integration/spaces_only/config.ts: 41.4 minutes ``` by splitting it into multiple groups. _1 round (splitting main index file with 3 index suites where each one has its own setup/tearDown + alerting suite into 4 groups)_ x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts 7m 1s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts **15m 10s** x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts **21m 40s** x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts 5m 30s x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts 2m 31s x-pack/test/alerting_api_integration/spaces_only/tests/actions/config.ts 4m 22s _2 round (rebalance groups 1-4 to be more time equal)_ x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts 12m 46s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts 8m 46s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts 17m 30s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts 9m 5s Here `Alerting eventLog alerts should generate expected alert events for normal operation` test started to fail, probably there is a dependency on the previous tests. _3 round (rebalance groups 1-4, to keep tests order in group 1 up until `event_log.ts` suite)_ x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts 17m 12s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts 8m 28s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts 16m 15s x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts 6m 21s _4 round (rebalancing groups 3-4 to be more time equal)_ x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts **17m 14s** x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts **8m 37s** x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts **12m 40s** x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts **9m 49s** --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/ftr_configs.yml | 7 ++- .../alerting_api_integration/common/config.ts | 4 +- .../group2/tests/alerting/event_log.ts | 2 +- .../tests/action_task_params/config.ts | 24 +++++++ .../tests/action_task_params/index.ts | 2 +- .../spaces_only/{ => tests/actions}/config.ts | 4 +- .../actions/connector_types/stack/email.ts | 2 +- .../spaces_only/tests/actions/index.ts | 2 +- .../lib => }/create_test_data.ts | 0 .../tests/alerting/{ => group1}/aggregate.ts | 6 +- .../alerting/{ => group1}/aggregate_post.ts | 6 +- .../alerting/{ => group1}/alerts_base.ts | 6 +- .../tests/alerting/group1/config.ts | 24 +++++++ .../tests/alerting/{ => group1}/create.ts | 6 +- .../tests/alerting/{ => group1}/delete.ts | 6 +- .../tests/alerting/{ => group1}/disable.ts | 6 +- .../tests/alerting/{ => group1}/enable.ts | 6 +- .../tests/alerting/{ => group1}/event_log.ts | 6 +- .../tests/alerting/{ => group1}/find.ts | 6 +- .../tests/alerting/{ => group1}/get.ts | 6 +- .../{ => group1}/get_action_error_log.ts | 6 +- .../alerting/{ => group1}/get_alert_state.ts | 6 +- .../{ => group1}/get_alert_summary.ts | 6 +- .../{ => group1}/get_execution_log.ts | 6 +- .../tests/alerting/group1/index.ts | 32 ++++++++++ .../tests/alerting/{ => group1}/rule_types.ts | 6 +- .../{ => group2}/alerts_default_space.ts | 6 +- .../alerting/{ => group2}/alerts_space1.ts | 6 +- .../tests/alerting/group2/config.ts | 24 +++++++ .../alerting/{ => group2}/execution_status.ts | 6 +- .../tests/alerting/group2/index.ts | 31 +++++++++ .../ml_rule_types/anomaly_detection/alert.ts | 6 +- .../ml_rule_types/anomaly_detection/index.ts | 2 +- .../{ => group2}/ml_rule_types/index.ts | 2 +- .../tests/alerting/{ => group2}/monitoring.ts | 6 +- .../{ => group2}/monitoring_collection.ts | 8 +-- .../tests/alerting/{ => group2}/mute_all.ts | 6 +- .../alerting/{ => group2}/mute_instance.ts | 6 +- .../transform_rule_types/index.ts | 2 +- .../transform_health/alert.ts | 6 +- .../transform_health/index.ts | 2 +- .../tests/alerting/{ => group2}/unmute_all.ts | 6 +- .../alerting/{ => group2}/unmute_instance.ts | 6 +- .../tests/alerting/{ => group2}/update.ts | 6 +- .../alerting/{ => group2}/update_api_key.ts | 6 +- .../builtin_alert_types/es_query/common.ts | 8 +-- .../builtin_alert_types/es_query/index.ts | 2 +- .../es_query/query_dsl_only.ts | 8 +-- .../builtin_alert_types/es_query/rule.ts | 8 +-- .../group3/builtin_alert_types/index.ts | 16 +++++ .../index_threshold/alert.ts | 10 +-- .../index_threshold/fields_endpoint.ts | 6 +- .../index_threshold/index.ts | 2 +- .../index_threshold/indices_endpoint.ts | 10 +-- .../time_series_query_endpoint.ts | 8 +-- .../tests/alerting/group3/config.ts | 24 +++++++ .../tests/alerting/group3/index.ts | 19 ++++++ .../alerting/{ => group4}/alerts_as_data.ts | 2 +- .../builtin_alert_types/auto_recover/index.ts | 2 +- .../builtin_alert_types/auto_recover/rule.ts | 8 +-- .../builtin_alert_types/cancellable/index.ts | 2 +- .../builtin_alert_types/cancellable/rule.ts | 8 +-- .../circuit_breaker/alert_limit_services.ts | 6 +- .../circuit_breaker/index.ts | 2 +- .../index_threshold_max_alerts.ts | 8 +-- .../{ => group4}/builtin_alert_types/index.ts | 4 +- .../builtin_alert_types/long_running/index.ts | 2 +- .../builtin_alert_types/long_running/rule.ts | 6 +- .../tests/alerting/{ => group4}/bulk_edit.ts | 6 +- .../{ => group4}/capped_action_type.ts | 6 +- .../check_registered_rule_types.ts | 2 +- .../tests/alerting/group4/config.ts | 24 +++++++ .../tests/alerting/{ => group4}/ephemeral.ts | 6 +- .../alerting/{ => group4}/event_log_alerts.ts | 6 +- .../alerting/{ => group4}/flapping_history.ts | 6 +- .../tests/alerting/group4/index.ts | 36 +++++++++++ .../tests/alerting/{ => group4}/migrations.ts | 6 +- .../alerting/{ => group4}/migrations/8_2_0.ts | 2 +- .../alerting/{ => group4}/migrations/index.ts | 2 +- .../{ => group4}/mustache_templates.ts | 8 +-- .../alerting/{ => group4}/notify_when.ts | 6 +- .../tests/alerting/{ => group4}/run_soon.ts | 4 +- .../{ => group4}/scheduled_task_id.ts | 9 ++- .../tests/alerting/{ => group4}/snooze.ts | 6 +- .../tests/alerting/{ => group4}/unsnooze.ts | 6 +- .../spaces_only/tests/alerting/index.ts | 63 ------------------- .../tests/{index.ts => helpers.ts} | 9 --- 87 files changed, 457 insertions(+), 263 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts rename x-pack/test/alerting_api_integration/spaces_only/{ => tests/actions}/config.ts (80%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{builtin_alert_types/lib => }/create_test_data.ts (100%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/aggregate.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/aggregate_post.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/alerts_base.ts (99%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/create.ts (99%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/delete.ts (95%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/disable.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/enable.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/event_log.ts (99%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/find.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/get.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/get_action_error_log.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/get_alert_state.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/get_alert_summary.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/get_execution_log.ts (99%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group1}/rule_types.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/alerts_default_space.ts (70%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/alerts_space1.ts (70%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/execution_status.ts (98%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/ml_rule_types/anomaly_detection/alert.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/ml_rule_types/anomaly_detection/index.ts (85%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/ml_rule_types/index.ts (86%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/monitoring.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/monitoring_collection.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/mute_all.ts (94%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/mute_instance.ts (94%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/transform_rule_types/index.ts (86%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/transform_rule_types/transform_health/alert.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/transform_rule_types/transform_health/index.ts (85%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/unmute_all.ts (95%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/unmute_instance.ts (95%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/update.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group2}/update_api_key.ts (95%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/es_query/common.ts (93%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/es_query/index.ts (86%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/es_query/query_dsl_only.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/es_query/rule.ts (99%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/index_threshold/alert.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/index_threshold/fields_endpoint.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/index_threshold/index.ts (88%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/index_threshold/indices_endpoint.ts (93%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group3}/builtin_alert_types/index_threshold/time_series_query_endpoint.ts (97%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/index.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/alerts_as_data.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/auto_recover/index.ts (85%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/auto_recover/rule.ts (95%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/cancellable/index.ts (85%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/cancellable/rule.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/circuit_breaker/alert_limit_services.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/circuit_breaker/index.ts (91%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/index.ts (78%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/long_running/index.ts (85%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/builtin_alert_types/long_running/rule.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/bulk_edit.ts (99%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/capped_action_type.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/check_registered_rule_types.ts (97%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/ephemeral.ts (96%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/event_log_alerts.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/flapping_history.ts (98%) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/migrations.ts (99%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/migrations/8_2_0.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/migrations/index.ts (85%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/mustache_templates.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/notify_when.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/run_soon.ts (97%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/scheduled_task_id.ts (95%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/snooze.ts (98%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{ => group4}/unsnooze.ts (94%) delete mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts rename x-pack/test/alerting_api_integration/spaces_only/tests/{index.ts => helpers.ts} (73%) diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 52c47e1eb272ba..0b56007cf9aeb8 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -128,7 +128,12 @@ enabled: - x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts - x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts - - x-pack/test/alerting_api_integration/spaces_only/config.ts + - x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts + - x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts + - x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts + - x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts + - x-pack/test/alerting_api_integration/spaces_only/tests/actions/config.ts + - x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts - x-pack/test/api_integration_basic/config.ts - x-pack/test/api_integration/config_security_basic.ts - x-pack/test/api_integration/config_security_trial.ts diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 3081c6f0c71903..0233568b6e0d2d 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -26,6 +26,7 @@ interface CreateTestConfigOptions { rejectUnauthorized?: boolean; // legacy emailDomainsAllowed?: string[]; testFiles?: string[]; + reportName?: string; useDedicatedTaskRunner: boolean; } @@ -73,6 +74,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) rejectUnauthorized = true, // legacy emailDomainsAllowed = undefined, testFiles = undefined, + reportName = undefined, useDedicatedTaskRunner, } = options; @@ -154,7 +156,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) servers, services, junit: { - reportName: 'X-Pack Alerting API Integration Tests', + reportName: reportName ? reportName : 'X-Pack Alerting API Integration Tests', }, esTestCluster: { ...xPackApiIntegrationTestsConfig.get('esTestCluster'), diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts index dd5f85dac298d6..bf3f60a9a9c551 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { validateEvent } from '../../../../spaces_only/tests/alerting/event_log'; +import { validateEvent } from '../../../../spaces_only/tests/alerting/group1/event_log'; // eslint-disable-next-line import/no-default-export export default function eventLogTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts new file mode 100644 index 00000000000000..b95ab7aec7d4bb --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/config.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../../common/config'; + +export const EmailDomainsAllowed = ['example.org', 'test.com']; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + verificationMode: 'none', + customizeLocalHostSsl: true, + preconfiguredAlertHistoryEsIndex: true, + emailDomainsAllowed: EmailDomainsAllowed, + useDedicatedTaskRunner: true, + testFiles: [require.resolve('.')], + reportName: 'X-Pack Alerting API Integration Tests - Action Task Params', +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/index.ts index 115358c4bce3a7..ab0c837c60938e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/action_task_params/index.ts @@ -6,7 +6,7 @@ */ import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { buildUp, tearDown } from '..'; +import { buildUp, tearDown } from '../helpers'; // eslint-disable-next-line import/no-default-export export default function actionTaskParamsTests({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/config.ts similarity index 80% rename from x-pack/test/alerting_api_integration/spaces_only/config.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/actions/config.ts index 988a6effd56394..3274d91ceb7323 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createTestConfig } from '../common/config'; +import { createTestConfig } from '../../../common/config'; export const EmailDomainsAllowed = ['example.org', 'test.com']; @@ -19,4 +19,6 @@ export default createTestConfig('spaces_only', { preconfiguredAlertHistoryEsIndex: true, emailDomainsAllowed: EmailDomainsAllowed, useDedicatedTaskRunner: true, + testFiles: [require.resolve('.')], + reportName: 'X-Pack Alerting API Integration Tests - Actions', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/email.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/email.ts index eb2ddc33ce444e..9bb924fd5c945e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/email.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/email.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ObjectRemover } from '../../../../../common/lib'; -import { EmailDomainsAllowed } from '../../../../config'; +import { EmailDomainsAllowed } from '../../config'; const EmailDomainAllowed = EmailDomainsAllowed[EmailDomainsAllowed.length - 1]; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index a12ca249e0d981..1e8489c6f901c5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -6,7 +6,7 @@ */ import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { buildUp, tearDown } from '..'; +import { buildUp, tearDown } from '../helpers'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create_test_data.ts similarity index 100% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create_test_data.ts diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate.ts index 602bf75a7ef2b3..52d3907eb88d05 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAggregateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate_post.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate_post.ts index 05d7701a23e545..378b3cb4055613 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/aggregate_post.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAggregateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/alerts_base.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/alerts_base.ts index b1e6ec2b2d458c..868d3f24f50e2f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/alerts_base.ts @@ -13,8 +13,8 @@ import { RecoveredActionGroup } from '@kbn/alerting-plugin/common'; import { TaskRunning, TaskRunningStage } from '@kbn/task-manager-plugin/server/task_running'; import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Space } from '../../../common/types'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Space } from '../../../../common/types'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getUrlPrefix, getTestRuleData, @@ -22,7 +22,7 @@ import { AlertUtils, ensureDatetimeIsWithinRange, TaskManagerUtils, -} from '../../../common/lib'; +} from '../../../../common/lib'; export function alertTests({ getService }: FtrProviderContext, space: Space) { const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts new file mode 100644 index 00000000000000..ef4c38d9171afc --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../../../common/config'; + +export const EmailDomainsAllowed = ['example.org', 'test.com']; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + verificationMode: 'none', + customizeLocalHostSsl: true, + preconfiguredAlertHistoryEsIndex: true, + emailDomainsAllowed: EmailDomainsAllowed, + useDedicatedTaskRunner: true, + testFiles: [require.resolve('.')], + reportName: 'X-Pack Alerting API Integration Tests - Alerting - group1', +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts index 5e07ec90aa7c32..534df486281be6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SavedObject } from '@kbn/core/server'; import { RawRule } from '@kbn/alerting-plugin/server/types'; -import { Spaces } from '../../scenarios'; +import { Spaces } from '../../../scenarios'; import { checkAAD, getUrlPrefix, @@ -16,8 +16,8 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, TaskManagerDoc, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts similarity index 95% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts index 073d76dc859a57..ba235719271202 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts index d4149c9cf2fb85..1ff7aab8f4ca61 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils as RuleUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getEventLog, TaskManagerDoc, -} from '../../../common/lib'; +} from '../../../../common/lib'; import { validateEvent } from './event_log'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts index d8dec2a486298b..f4ad874d3357eb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -15,7 +15,7 @@ import { getTestRuleData, ObjectRemover, TaskManagerDoc, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index 73968703bf9bbe..9a7d90d4adf246 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function eventLogTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts index bcf1a3d09ca50d..30b3643466f42a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { fromKueryExpression } from '@kbn/es-query'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; async function createAlert( objectRemover: ObjectRemover, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts index 6cd28d02024653..dd6b7b9327d27b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const getTestUtils = ( describeType: 'internal' | 'public', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_action_error_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_action_error_log.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_action_error_log.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_action_error_log.ts index 0f2e67b1187fc9..dbb8cee8673b84 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_action_error_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_action_error_log.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createGetActionErrorLogTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts index 61d38b522bb59d..6082e6ff69eb81 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestRuleData } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestRuleData } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createGetAlertStateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts index 1ec570f6c4f719..712616329929ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts @@ -8,15 +8,15 @@ import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { Spaces } from '../../scenarios'; +import { Spaces } from '../../../scenarios'; import { getUrlPrefix, ObjectRemover, getTestRuleData, AlertUtils, getEventLog, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createGetAlertSummaryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_execution_log.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_execution_log.ts index 0d2607ad0602b7..b7d2a7f03b3e68 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_execution_log.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createGetExecutionLogTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts new file mode 100644 index 00000000000000..87ef8228c7303c --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/index.ts @@ -0,0 +1,32 @@ +/* + * 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 '../../../../common/ftr_provider_context'; +import { buildUp, tearDown } from '../../helpers'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerting', () => { + before(async () => await buildUp(getService)); + after(async () => await tearDown(getService)); + + loadTestFile(require.resolve('./aggregate')); + loadTestFile(require.resolve('./aggregate_post')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./disable')); + loadTestFile(require.resolve('./enable')); + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); + loadTestFile(require.resolve('./get_alert_summary')); + loadTestFile(require.resolve('./get_execution_log')); + loadTestFile(require.resolve('./get_action_error_log')); + loadTestFile(require.resolve('./rule_types')); + loadTestFile(require.resolve('./event_log')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts index c3d2f11c580e74..2f2bdde9e74557 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix } from '../../../common/lib/space_test_utils'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix } from '../../../../common/lib/space_test_utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function listAlertTypes({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_default_space.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/alerts_default_space.ts similarity index 70% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_default_space.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/alerts_default_space.ts index 08c269239bd13c..1e62dd82bdaa8c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_default_space.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/alerts_default_space.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { Spaces } from '../../scenarios'; -import { alertTests } from './alerts_base'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { alertTests } from '../group1/alerts_base'; // eslint-disable-next-line import/no-default-export export default function alertSpace1Tests(context: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_space1.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/alerts_space1.ts similarity index 70% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_space1.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/alerts_space1.ts index 36736065a6fc8e..071b3c7d13b4ee 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_space1.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/alerts_space1.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { Spaces } from '../../scenarios'; -import { alertTests } from './alerts_base'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { alertTests } from '../group1/alerts_base'; // eslint-disable-next-line import/no-default-export export default function alertSpace1Tests(context: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts new file mode 100644 index 00000000000000..9aa68299dbdac6 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../../../common/config'; + +export const EmailDomainsAllowed = ['example.org', 'test.com']; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + verificationMode: 'none', + customizeLocalHostSsl: true, + preconfiguredAlertHistoryEsIndex: true, + emailDomainsAllowed: EmailDomainsAllowed, + useDedicatedTaskRunner: true, + testFiles: [require.resolve('.')], + reportName: 'X-Pack Alerting API Integration Tests - Alerting - group2', +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/execution_status.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/execution_status.ts index 02edd1d4f37c80..a21027ea448b9b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/execution_status.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; +import { Spaces } from '../../../scenarios'; import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, ensureDatetimesAreOrdered, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function executionStatusAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts new file mode 100644 index 00000000000000..1ccbb1c8f722d3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/index.ts @@ -0,0 +1,31 @@ +/* + * 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 '../../../../common/ftr_provider_context'; +import { buildUp, tearDown } from '../../helpers'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerting', () => { + before(async () => await buildUp(getService)); + after(async () => await tearDown(getService)); + + loadTestFile(require.resolve('./execution_status')); + loadTestFile(require.resolve('./monitoring_collection')); + loadTestFile(require.resolve('./monitoring')); + loadTestFile(require.resolve('./mute_all')); + loadTestFile(require.resolve('./mute_instance')); + loadTestFile(require.resolve('./unmute_all')); + loadTestFile(require.resolve('./unmute_instance')); + loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_api_key')); + loadTestFile(require.resolve('./alerts_space1')); + loadTestFile(require.resolve('./alerts_default_space')); + loadTestFile(require.resolve('./transform_rule_types')); + loadTestFile(require.resolve('./ml_rule_types')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/anomaly_detection/alert.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/alert.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/anomaly_detection/alert.ts index 51db0e9055fafd..1d69200a277d42 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/anomaly_detection/alert.ts @@ -13,9 +13,9 @@ import { MlAnomalyDetectionAlertParams } from '@kbn/ml-plugin/common/types/alert import { ANOMALY_SCORE_MATCH_GROUP_ID } from '@kbn/ml-plugin/server/lib/alerts/register_anomaly_detection_alert_type'; import { ML_ALERT_TYPES } from '@kbn/ml-plugin/common/constants/alerts'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { Spaces } from '../../../../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; const ACTION_TYPE_ID = '.index'; const ALERT_TYPE_ID = ML_ALERT_TYPES.ANOMALY_DETECTION; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/anomaly_detection/index.ts similarity index 85% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/anomaly_detection/index.ts index f2875c62c67cdf..392068317295df 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/anomaly_detection/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/index.ts similarity index 86% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/index.ts index 1b7a2ea1842ee9..005e5bce7adeaa 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/ml_rule_types/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring.ts index 38506eb54a4bc1..95d2a08a57d336 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function monitoringAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring_collection.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring_collection.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts index c6f240b8bb6bd6..e4e52d6af314ae 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/monitoring_collection.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts @@ -7,16 +7,16 @@ import expect from '@kbn/expect'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../scenarios'; +import { Spaces } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover, createWaitForExecutionCount, getEventLog, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { createEsDocuments } from './builtin_alert_types/lib/create_test_data'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { createEsDocuments } from '../create_test_data'; const NODE_RULES_MONITORING_COLLECTION_URL = `/api/monitoring_collection/node_rules`; const CLUSTER_RULES_MONITORING_COLLECTION_URL = `/api/monitoring_collection/cluster_rules`; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts similarity index 94% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts index 3fe13f8debe25c..9cfbb1e4775261 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts similarity index 94% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts index d32b74fd394475..85dda60babfd00 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteInstanceTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/index.ts similarity index 86% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/index.ts index f743df169d417c..6c50e058d97028 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/alert.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/alert.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/alert.ts index ee432431b7c83d..881f8152bcd2d9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/alert.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { PutTransformsRequestSchema } from '@kbn/transform-plugin/common/api_schemas/transforms'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; -import { Spaces } from '../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; +import { Spaces } from '../../../../../scenarios'; const ACTION_TYPE_ID = '.index'; const ALERT_TYPE_ID = 'transform_health'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts similarity index 85% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts index c324745b858138..adc93332a0f569 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts similarity index 95% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts index 47f61250157a37..a39f576364d042 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUnmuteTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts similarity index 95% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts index 086f40d9febae1..3dda4b0db49b85 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUnmuteInstanceTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts index 21008836bf5eb8..20239f94cfef35 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createUpdateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts similarity index 95% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts index 9fe5c7e112c79d..a57d9dc90fd6d2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, -} from '../../../common/lib'; +} from '../../../../common/lib'; /** * Eventhough security is disabled, this test checks the API behavior. diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/common.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts similarity index 93% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/common.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts index faca906abc586e..9ec5171d74d5cb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/common.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts @@ -6,10 +6,10 @@ */ import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { Spaces } from '../../../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; -import { createEsDocuments, createEsDocumentsWithGroups } from '../lib/create_test_data'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { Spaces } from '../../../../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; +import { createEsDocuments, createEsDocumentsWithGroups } from '../../../create_test_data'; export const RULE_TYPE_ID = '.es-query'; export const CONNECTOR_TYPE_ID = '.index'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts similarity index 86% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts index 98c0737ba670c1..a584753db7c25f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/query_dsl_only.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/query_dsl_only.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts index 6024d07bccec0d..8a558c8e27299b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/query_dsl_only.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/query_dsl_only.ts @@ -9,10 +9,10 @@ import expect from '@kbn/expect'; import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; -import { createDataStream, deleteDataStream } from '../lib/create_test_data'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; +import { createDataStream, deleteDataStream } from '../../../create_test_data'; import { createConnector, CreateRuleParams, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts index 6fd81745f770e3..6a2314bec3a0b0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts @@ -9,9 +9,9 @@ import expect from '@kbn/expect'; import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; import { createConnector, ES_GROUPS_TO_WRITE, @@ -25,7 +25,7 @@ import { RULE_INTERVAL_SECONDS, RULE_TYPE_ID, } from './common'; -import { createDataStream, deleteDataStream } from '../lib/create_test_data'; +import { createDataStream, deleteDataStream } from '../../../create_test_data'; // eslint-disable-next-line import/no-default-export export default function ruleTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index.ts new file mode 100644 index 00000000000000..5489af62f52afa --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index.ts @@ -0,0 +1,16 @@ +/* + * 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 '../../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('builtin alertTypes', () => { + loadTestFile(require.resolve('./index_threshold')); + loadTestFile(require.resolve('./es_query')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/alert.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/alert.ts index 945e9f5cd73807..46f8a0dc955f3e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/alert.ts @@ -9,11 +9,11 @@ import expect from '@kbn/expect'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../common/lib'; -import { createEsDocumentsWithGroups } from '../lib/create_test_data'; -import { createDataStream, deleteDataStream } from '../lib/create_test_data'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../../common/lib'; +import { createEsDocumentsWithGroups } from '../../../create_test_data'; +import { createDataStream, deleteDataStream } from '../../../create_test_data'; const RULE_TYPE_ID = '.index-threshold'; const CONNECTOR_TYPE_ID = '.index'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/fields_endpoint.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/fields_endpoint.ts index 48ba628fd788e7..bedbd386f9ef3e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/fields_endpoint.ts @@ -9,9 +9,9 @@ import expect from '@kbn/expect'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix } from '../../../../../common/lib'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix } from '../../../../../../common/lib'; const API_URI = 'internal/triggers_actions_ui/data/_fields'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/index.ts similarity index 88% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/index.ts index 0030e7a11581df..454daa02b79f3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/indices_endpoint.ts similarity index 93% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/indices_endpoint.ts index 5c6c11d6efd7b2..e6c0e96e678127 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/indices_endpoint.ts @@ -9,11 +9,11 @@ import expect from '@kbn/expect'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix } from '../../../../../common/lib'; -import { createEsDocumentsWithGroups } from '../lib/create_test_data'; -import { createDataStream, deleteDataStream } from '../lib/create_test_data'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix } from '../../../../../../common/lib'; +import { createEsDocumentsWithGroups } from '../../../create_test_data'; +import { createDataStream, deleteDataStream } from '../../../create_test_data'; const API_URI = 'internal/triggers_actions_ui/data/_indices'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/time_series_query_endpoint.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index ec342baf3ad664..24ec06cb9874eb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -10,11 +10,11 @@ import expect from '@kbn/expect'; import { TimeSeriesQuery } from '@kbn/triggers-actions-ui-plugin/server'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix } from '../../../../../common/lib'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix } from '../../../../../../common/lib'; -import { createEsDocumentsWithGroups } from '../lib/create_test_data'; +import { createEsDocumentsWithGroups } from '../../../create_test_data'; const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'internal/triggers_actions_ui/data/_time_series_query'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts new file mode 100644 index 00000000000000..c5fa4109abd3ac --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/config.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../../../common/config'; + +export const EmailDomainsAllowed = ['example.org', 'test.com']; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + verificationMode: 'none', + customizeLocalHostSsl: true, + preconfiguredAlertHistoryEsIndex: true, + emailDomainsAllowed: EmailDomainsAllowed, + useDedicatedTaskRunner: true, + testFiles: [require.resolve('.')], + reportName: 'X-Pack Alerting API Integration Tests - Alerting - group3', +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/index.ts new file mode 100644 index 00000000000000..05fcb4f95d901a --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/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 '../../../../common/ftr_provider_context'; +import { buildUp, tearDown } from '../../helpers'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerting', () => { + before(async () => await buildUp(getService)); + after(async () => await tearDown(getService)); + + loadTestFile(require.resolve('./builtin_alert_types')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_as_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_as_data.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data.ts index 13cb9bcd337f97..49ec03fc6d8d89 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_as_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data.ts @@ -8,7 +8,7 @@ import { alertFieldMap } from '@kbn/alerting-plugin/common/alert_schema'; import { mappingFromFieldMap } from '@kbn/alerting-plugin/common/alert_schema/field_maps/mapping_from_field_map'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertsAsDataTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/auto_recover/index.ts similarity index 85% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/auto_recover/index.ts index 619f6e9b4c93eb..529dfd5f30fad5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/auto_recover/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/auto_recover/rule.ts similarity index 95% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/auto_recover/rule.ts index d46c2d2c60958b..0448472670e3ec 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/auto_recover/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/auto_recover/rule.ts @@ -9,10 +9,10 @@ import expect from '@kbn/expect'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; import { RecoveredActionGroup } from '@kbn/alerting-plugin/common'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover, TaskManagerUtils } from '../../../../../common/lib'; -import { createEsDocuments } from '../lib/create_test_data'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, TaskManagerUtils } from '../../../../../../common/lib'; +import { createEsDocuments } from '../../../create_test_data'; const RULE_INTERVAL_SECONDS = 6; const RULE_INTERVALS_TO_WRITE = 5; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/cancellable/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/index.ts similarity index 85% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/cancellable/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/index.ts index a0b500ef690240..45e495649f7db5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/cancellable/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/cancellable/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/cancellable/rule.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts index f8a84727348fbc..41a12004e256ac 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/cancellable/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts @@ -8,10 +8,10 @@ import expect from '@kbn/expect'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../common/lib'; -import { createEsDocuments } from '../lib/create_test_data'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../../common/lib'; +import { createEsDocuments } from '../../../create_test_data'; const RULE_INTERVAL_SECONDS = 6; const RULE_INTERVALS_TO_WRITE = 5; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/alert_limit_services.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/alert_limit_services.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/alert_limit_services.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/alert_limit_services.ts index 00b607c102a3a3..3d5eb4004ca94f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/alert_limit_services.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/alert_limit_services.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../common/lib'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function maxAlertsRuleTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/index.ts similarity index 91% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/index.ts index 5f5e6f0e75c0cf..4b58c668bc3e9e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingCircuitBreakerTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts index 63a5e5283a10bb..8bab44baee5386 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/circuit_breaker/index_threshold_max_alerts.ts @@ -8,10 +8,10 @@ import expect from '@kbn/expect'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../common/lib'; -import { createEsDocumentsWithGroups } from '../lib/create_test_data'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../../common/lib'; +import { createEsDocumentsWithGroups } from '../../../create_test_data'; const RULE_INTERVAL_SECONDS = 6; const RULE_INTERVALS_TO_WRITE = 1; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/index.ts similarity index 78% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/index.ts index 83c77b504833e3..6db90f566b2308 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/index.ts @@ -5,13 +5,11 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { describe('builtin alertTypes', () => { - loadTestFile(require.resolve('./index_threshold')); - loadTestFile(require.resolve('./es_query')); loadTestFile(require.resolve('./long_running')); loadTestFile(require.resolve('./cancellable')); loadTestFile(require.resolve('./circuit_breaker')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/long_running/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/index.ts similarity index 85% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/long_running/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/index.ts index d997cebc6fd8d0..224c3c99046c05 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/long_running/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/long_running/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/long_running/rule.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts index 580b058d3ab1e9..fb64991d1b2a31 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/long_running/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; -import { Spaces } from '../../../../scenarios'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../common/lib'; +import { Spaces } from '../../../../../scenarios'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { getUrlPrefix, ObjectRemover, getEventLog } from '../../../../../../common/lib'; const RULE_INTERVAL_SECONDS = 3; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_edit.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/bulk_edit.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_edit.ts index 0fcee156b80479..fa039cc3f0533f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/bulk_edit.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_edit.ts @@ -8,15 +8,15 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; import type { SanitizedRule } from '@kbn/alerting-plugin/common'; -import { Spaces } from '../../scenarios'; +import { Spaces } from '../../../scenarios'; import { checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, createWaitForExecutionCount, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const getSnoozeSchedule = () => { return { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/capped_action_type.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/capped_action_type.ts index 374dbddfc17b4f..356812a330b304 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/capped_action_type.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/capped_action_type.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { getEventLog, getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../common/lib'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getEventLog, getTestRuleData, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createCappedActionsTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/check_registered_rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/check_registered_rule_types.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts index 125f5ee4e761d6..042db04fe4e149 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/check_registered_rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createRegisteredRuleTypeTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts new file mode 100644 index 00000000000000..8ceb21f6c9b5f0 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/config.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../../../common/config'; + +export const EmailDomainsAllowed = ['example.org', 'test.com']; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + verificationMode: 'none', + customizeLocalHostSsl: true, + preconfiguredAlertHistoryEsIndex: true, + emailDomainsAllowed: EmailDomainsAllowed, + useDedicatedTaskRunner: true, + testFiles: [require.resolve('.')], + reportName: 'X-Pack Alerting API Integration Tests - Alerting - group4', +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/ephemeral.ts similarity index 96% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/ephemeral.ts index 26c9829c9ef35b..c658e20f8108f3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/ephemeral.ts @@ -10,9 +10,9 @@ import { flatten } from 'lodash'; import { IValidatedEvent } from '@kbn/event-log-plugin/server'; import { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from '@kbn/alerting-plugin/server/config'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createNotifyWhenTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/event_log_alerts.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/event_log_alerts.ts index e5d69507e8c6b1..74b162c04323ee 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/event_log_alerts.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function eventLogAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/flapping_history.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/flapping_history.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/flapping_history.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/flapping_history.ts index ac09451e862fbd..efc4019b58329c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/flapping_history.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/flapping_history.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { get } from 'lodash'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { Spaces } from '../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { Spaces } from '../../../scenarios'; // eslint-disable-next-line import/no-default-export export default function createFlappingHistoryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts new file mode 100644 index 00000000000000..8fb5c46d623b18 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { buildUp, tearDown } from '../../helpers'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerting', () => { + before(async () => await buildUp(getService)); + after(async () => await tearDown(getService)); + + loadTestFile(require.resolve('./builtin_alert_types')); + loadTestFile(require.resolve('./mustache_templates.ts')); + loadTestFile(require.resolve('./notify_when')); + loadTestFile(require.resolve('./ephemeral')); + loadTestFile(require.resolve('./event_log_alerts')); + loadTestFile(require.resolve('./snooze')); + loadTestFile(require.resolve('./bulk_edit')); + loadTestFile(require.resolve('./capped_action_type')); + loadTestFile(require.resolve('./scheduled_task_id')); + loadTestFile(require.resolve('./run_soon')); + loadTestFile(require.resolve('./flapping_history')); + loadTestFile(require.resolve('./check_registered_rule_types')); + loadTestFile(require.resolve('./alerts_as_data')); + // Do not place test files here, due to https://github.com/elastic/kibana/issues/123059 + + // note that this test will destroy existing spaces + loadTestFile(require.resolve('./migrations.ts')); + loadTestFile(require.resolve('./migrations/index.ts')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations.ts similarity index 99% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations.ts index f41887e6e38e6b..35ad717ad80966 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations.ts @@ -9,9 +9,9 @@ import expect from '@kbn/expect'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { RawRule, RawRuleAction } from '@kbn/alerting-plugin/server/types'; import { FILEBEAT_7X_INDICATOR_PATH } from '@kbn/alerting-plugin/server/saved_objects/migrations'; -import { SavedObjectReference } from '@kbn/core-saved-objects-server'; -import { getUrlPrefix } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import type { SavedObjectReference } from '@kbn/core/server'; +import { getUrlPrefix } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations/8_2_0.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations/8_2_0.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations/8_2_0.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations/8_2_0.ts index 6a9117a016cbf8..963f856845c10f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations/8_2_0.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations/8_2_0.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { RawRule } from '@kbn/alerting-plugin/server/types'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations/index.ts similarity index 85% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations/index.ts index 59e7eed1530bf3..9af28e3656a0d5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/migrations/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function migrationTests({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts index 64076aeafd14b2..4663d76a988c28 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts @@ -18,13 +18,13 @@ import { URL, format as formatUrl } from 'url'; import axios from 'axios'; import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getWebhookServer, getSlackServer, -} from '../../../common/plugins/actions_simulators/server/plugin'; +} from '../../../../common/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function executionStatusAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/notify_when.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/notify_when.ts index a7996f19554e80..e32813934e4c25 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/notify_when.ts @@ -8,9 +8,9 @@ import expect from '@kbn/expect'; import { IValidatedEvent } from '@kbn/event-log-plugin/server'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestRuleData, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createNotifyWhenTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/run_soon.ts similarity index 97% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/run_soon.ts index bba958d47d241e..ceaba73b2a2acd 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/run_soon.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const LOADED_RULE_ID = '74f3e6d7-b7bb-477d-ac28-92ee22728e6e'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts similarity index 95% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts index d008421381b14e..d8570564d32d2e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/scheduled_task_id.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts @@ -6,8 +6,13 @@ */ import expect from '@kbn/expect'; -import { getUrlPrefix, TaskManagerDoc, ObjectRemover, getTestRuleData } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + getUrlPrefix, + TaskManagerDoc, + ObjectRemover, + getTestRuleData, +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const MIGRATED_RULE_ID = '74f3e6d7-b7bb-477d-ac28-92ee22728e6e'; const MIGRATED_TASK_ID = '329798f0-b0b0-11ea-9510-fdf248d5f2a4'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts similarity index 98% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts index d4ae41f5470141..87360e2f12a4bd 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/snooze.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { getTestRuleData, ObjectRemover, getEventLog, -} from '../../../common/lib'; +} from '../../../../common/lib'; const NOW = new Date().toISOString(); const SNOOZE_SCHEDULE = { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts similarity index 94% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unsnooze.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts index 79811debe9f466..4c3937d23190ce 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/unsnooze.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, getUrlPrefix, getTestRuleData, ObjectRemover, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createSnoozeRuleTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts deleted file mode 100644 index d7da90cf56df4b..00000000000000 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ /dev/null @@ -1,63 +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 { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { buildUp, tearDown } from '..'; - -// eslint-disable-next-line import/no-default-export -export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { - describe('Alerting', () => { - before(async () => await buildUp(getService)); - after(async () => await tearDown(getService)); - - loadTestFile(require.resolve('./aggregate')); - loadTestFile(require.resolve('./aggregate_post')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./disable')); - loadTestFile(require.resolve('./enable')); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./get_alert_state')); - loadTestFile(require.resolve('./get_alert_summary')); - loadTestFile(require.resolve('./get_execution_log')); - loadTestFile(require.resolve('./get_action_error_log')); - loadTestFile(require.resolve('./rule_types')); - loadTestFile(require.resolve('./event_log')); - loadTestFile(require.resolve('./execution_status')); - loadTestFile(require.resolve('./monitoring_collection')); - loadTestFile(require.resolve('./monitoring')); - loadTestFile(require.resolve('./mute_all')); - loadTestFile(require.resolve('./mute_instance')); - loadTestFile(require.resolve('./unmute_all')); - loadTestFile(require.resolve('./unmute_instance')); - loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./update_api_key')); - loadTestFile(require.resolve('./alerts_space1')); - loadTestFile(require.resolve('./alerts_default_space')); - loadTestFile(require.resolve('./builtin_alert_types')); - loadTestFile(require.resolve('./transform_rule_types')); - loadTestFile(require.resolve('./ml_rule_types')); - loadTestFile(require.resolve('./mustache_templates.ts')); - loadTestFile(require.resolve('./notify_when')); - loadTestFile(require.resolve('./ephemeral')); - loadTestFile(require.resolve('./event_log_alerts')); - loadTestFile(require.resolve('./snooze')); - loadTestFile(require.resolve('./bulk_edit')); - loadTestFile(require.resolve('./capped_action_type')); - loadTestFile(require.resolve('./scheduled_task_id')); - loadTestFile(require.resolve('./run_soon')); - loadTestFile(require.resolve('./flapping_history')); - loadTestFile(require.resolve('./check_registered_rule_types')); - loadTestFile(require.resolve('./alerts_as_data')); - // Do not place test files here, due to https://github.com/elastic/kibana/issues/123059 - - // note that this test will destroy existing spaces - loadTestFile(require.resolve('./migrations.ts')); - loadTestFile(require.resolve('./migrations/index.ts')); - }); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/helpers.ts similarity index 73% rename from x-pack/test/alerting_api_integration/spaces_only/tests/index.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/helpers.ts index 890e6790f84c3d..4baf887ecf9392 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/helpers.ts @@ -8,15 +8,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { Spaces } from '../scenarios'; -// eslint-disable-next-line import/no-default-export -export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('alerting api integration spaces only', function () { - loadTestFile(require.resolve('./actions')); - loadTestFile(require.resolve('./alerting')); - loadTestFile(require.resolve('./action_task_params')); - }); -} - export async function buildUp(getService: FtrProviderContext['getService']) { const spacesService = getService('spaces'); for (const space of Object.values(Spaces)) { From a2c3a3682f4d45a7c1d1151bcebba74cf604abf6 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Wed, 1 Feb 2023 14:52:50 +0100 Subject: [PATCH 40/59] [Infrastructure UI]: Use dateRange as source of truth for Hosts View (#150029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Closes #150027 This PR removes from the URL state the `dateRangeTimestamp` filter and keeps as a unique source of truth the `dateRange`, derivating from this one the expected timestamp conversion on each update or page refresh. ## 🧪 Testing 1. Go To Hosts view 2. Select `Last 1 hour` time range. 3. Verify the only saved param in the URL is the `dateRange` 4. Wait for a couple of minutes 5. Refresh the page You should see the data is updated to the last real hour since the moment the page has been reloaded. You can more specifically verify this by checking what timestamp range is sent with the snapshot request payload. --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../metrics/hosts/hooks/use_hosts_view.ts | 14 ++++--- .../metrics/hosts/hooks/use_unified_search.ts | 7 ++-- .../hooks/use_unified_search_url_state.ts | 40 ++++++------------- 3 files changed, 23 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts index 671b3484356f16..fa259784a3d722 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts @@ -32,32 +32,34 @@ export const INITAL_VALUE = { export const useHostsView = () => { const { sourceId } = useSourceContext(); - const { buildQuery, dateRangeTimestamp } = useUnifiedSearchContext(); + const { buildQuery, getDateRangeAsTimestamp } = useUnifiedSearchContext(); const [hostViewState, setHostViewState] = useState(INITAL_VALUE); const baseRequest = useMemo(() => { const esQuery = buildQuery(); + const { from, to } = getDateRangeAsTimestamp(); + const snapshotRequest: UseSnapshotRequest = { filterQuery: esQuery ? JSON.stringify(esQuery) : null, metrics: [], groupBy: [], nodeType: 'host', sourceId, - currentTime: dateRangeTimestamp.to, + currentTime: to, includeTimeseries: false, sendRequestImmediately: true, timerange: { interval: '1m', - from: dateRangeTimestamp.from, - to: dateRangeTimestamp.to, + from, + to, ignoreLookback: true, }, // The user might want to click on the submit button without changing the filters - // This makes sure all child componets will re-render. + // This makes sure all child components will re-render. requestTs: Date.now(), }; return snapshotRequest; - }, [buildQuery, dateRangeTimestamp.from, dateRangeTimestamp.to, sourceId]); + }, [buildQuery, getDateRangeAsTimestamp, sourceId]); return { baseRequest, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts index c6753345d1705a..625e12df9714df 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts @@ -17,7 +17,7 @@ import { useSyncKibanaTimeFilterTime } from '../../../../hooks/use_kibana_timefi import { useHostsUrlState, INITIAL_DATE_RANGE } from './use_unified_search_url_state'; export const useUnifiedSearch = () => { - const { state, dispatch, getRangeInTimestamp, getTime } = useHostsUrlState(); + const { state, dispatch, getTime, getDateRangeAsTimestamp } = useHostsUrlState(); const { metricsDataView } = useMetricsDataViewContext(); const { services } = useKibana(); const { @@ -78,12 +78,11 @@ export const useUnifiedSearch = () => { query, filters, dateRange: newDateRange, - dateRangeTimestamp: getRangeInTimestamp(newDateRange), panelFilters, }, }); }, - [getTime, dispatch, getRangeInTimestamp] + [getTime, dispatch] ); // This won't prevent onSubmit from being fired twice when `clear filters` is clicked, @@ -131,9 +130,9 @@ export const useUnifiedSearch = () => { buildQuery, clearSavedQuery, controlPanelFilters: state.panelFilters, - dateRangeTimestamp: state.dateRangeTimestamp, onSubmit: debounceOnSubmit, saveQuery, + getDateRangeAsTimestamp, unifiedSearchQuery: state.query, unifiedSearchDateRange: state.dateRange, unifiedSearchFilters: state.filters, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts index e50fa2fa76cc4e..1a19f21626d823 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts @@ -6,7 +6,6 @@ */ import { useCallback, useEffect, useReducer } from 'react'; -import { TimeRange } from '@kbn/es-query'; import DateMath from '@kbn/datemath'; import deepEqual from 'fast-deep-equal'; import * as rt from 'io-ts'; @@ -23,25 +22,19 @@ const DEFAULT_QUERY = { query: '', }; const DEFAULT_FROM_MINUTES_VALUE = 15; -const INITIAL_DATE = new Date(); -export const INITIAL_DATE_RANGE = { from: `now-${DEFAULT_FROM_MINUTES_VALUE}m`, to: 'now' }; -const CALCULATED_DATE_RANGE_TO = INITIAL_DATE.getTime(); const DEFAULT_FROM_IN_MILLISECONDS = DEFAULT_FROM_MINUTES_VALUE * 60000; -const CALCULATED_DATE_RANGE_FROM = new Date( - CALCULATED_DATE_RANGE_TO - DEFAULT_FROM_IN_MILLISECONDS -).getTime(); + +export const INITIAL_DATE_RANGE = { from: `now-${DEFAULT_FROM_MINUTES_VALUE}m`, to: 'now' }; + +const getDefaultFromTimestamp = () => Date.now() - DEFAULT_FROM_IN_MILLISECONDS; +const getDefaultToTimestamp = () => Date.now(); const INITIAL_HOSTS_STATE: HostsState = { query: DEFAULT_QUERY, filters: [], panelFilters: [], // for unified search - dateRange: { ...INITIAL_DATE_RANGE }, - // for useSnapshot - dateRangeTimestamp: { - from: CALCULATED_DATE_RANGE_FROM, - to: CALCULATED_DATE_RANGE_TO, - }, + dateRange: INITIAL_DATE_RANGE, }; type Action = @@ -84,15 +77,12 @@ export const useHostsUrlState = () => { const [state, dispatch] = useReducer(reducer, urlState); - const getRangeInTimestamp = useCallback(({ from, to }: TimeRange) => { - const fromTS = DateMath.parse(from)?.valueOf() ?? CALCULATED_DATE_RANGE_FROM; - const toTS = DateMath.parse(to)?.valueOf() ?? CALCULATED_DATE_RANGE_TO; + const getDateRangeAsTimestamp = useCallback(() => { + const from = DateMath.parse(state.dateRange.from)?.valueOf() ?? getDefaultFromTimestamp(); + const to = DateMath.parse(state.dateRange.to)?.valueOf() ?? getDefaultToTimestamp(); - return { - from: fromTS, - to: toTS, - }; - }, []); + return { from, to }; + }, [state.dateRange]); useEffect(() => { if (!deepEqual(state, urlState)) { @@ -102,7 +92,7 @@ export const useHostsUrlState = () => { return { dispatch, - getRangeInTimestamp, + getDateRangeAsTimestamp, getTime, state, }; @@ -144,17 +134,11 @@ const StringDateRangeRT = rt.type({ to: rt.string, }); -const DateRangeRT = rt.type({ - from: rt.number, - to: rt.number, -}); - const HostsStateRT = rt.type({ filters: HostsFiltersRT, panelFilters: HostsFiltersRT, query: HostsQueryStateRT, dateRange: StringDateRangeRT, - dateRangeTimestamp: DateRangeRT, }); export type HostsState = rt.TypeOf; From cb39822a805a58037c54e77c54b3a6b724ddc1a1 Mon Sep 17 00:00:00 2001 From: Nav <13634519+navarone-feekery@users.noreply.github.com> Date: Wed, 1 Feb 2023 14:57:49 +0100 Subject: [PATCH 41/59] Supprt custom scheduling in Connectors (#149815) --- .../common/types/connectors.ts | 11 +++++++++++ .../__mocks__/search_indices.mock.ts | 18 ++++++++++++++++++ .../__mocks__/view_index.mock.ts | 18 ++++++++++++++++++ .../index_management/setup_indices.test.ts | 3 +++ .../server/index_management/setup_indices.ts | 1 + .../lib/connectors/add_connector.test.ts | 3 +++ .../server/lib/connectors/add_connector.ts | 1 + .../server/lib/connectors/start_sync.test.ts | 2 ++ .../update_connector_scheduling.test.ts | 2 ++ 9 files changed, 59 insertions(+) diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts index e9bc91892f4f62..87ccbad824e1f4 100644 --- a/x-pack/plugins/enterprise_search/common/types/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts @@ -17,6 +17,16 @@ export interface ConnectorScheduling { interval: string; } +export interface CustomScheduling { + configuration_overrides: Record; + enabled: boolean; + interval: string; + last_synced: string | null; + name: string; +} + +export type ConnectorCustomScheduling = Record; + export enum ConnectorStatus { CREATED = 'created', NEEDS_CONFIGURATION = 'needs_configuration', @@ -125,6 +135,7 @@ export type ConnectorFeatures = Partial<{ export interface Connector { api_key_id: string | null; configuration: ConnectorConfiguration; + custom_scheduling: ConnectorCustomScheduling; description: string | null; error: string | null; features: ConnectorFeatures; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts index 2555ef905caff3..955cf219a8019f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts @@ -33,6 +33,15 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ connector: { api_key_id: null, configuration: { foo: { label: 'bar', value: 'barbar' } }, + custom_scheduling: { + foo: { + configuration_overrides: {}, + enabled: false, + interval: '', + last_synced: null, + name: '', + }, + }, description: null, error: null, features: null, @@ -119,6 +128,15 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ connector: { api_key_id: null, configuration: { foo: { label: 'bar', value: 'barbar' } }, + custom_scheduling: { + foo: { + configuration_overrides: {}, + enabled: false, + interval: '', + last_synced: null, + name: '', + }, + }, description: null, error: null, features: null, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts index 73e4a65874c052..78ef10664abcb4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts @@ -43,6 +43,15 @@ export const connectorIndex: ConnectorViewIndex = { connector: { api_key_id: null, configuration: { foo: { label: 'bar', value: 'barbar' } }, + custom_scheduling: { + foo: { + configuration_overrides: {}, + enabled: false, + interval: '', + last_synced: null, + name: '', + }, + }, description: null, error: null, features: null, @@ -133,6 +142,15 @@ export const crawlerIndex: CrawlerViewIndex = { connector: { api_key_id: null, configuration: { foo: { label: 'bar', value: 'barbar' } }, + custom_scheduling: { + foo: { + configuration_overrides: {}, + enabled: false, + interval: '', + last_synced: null, + name: '', + }, + }, description: null, error: null, features: null, diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts index e312bada49e413..6ba4d9484ea870 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts @@ -38,6 +38,9 @@ describe('Setup Indices', () => { configuration: { type: 'object', }, + custom_scheduling: { + type: 'object', + }, description: { type: 'text' }, error: { type: 'keyword' }, features: { diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts index 36757667193a31..10ff75fcb566d9 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts @@ -30,6 +30,7 @@ interface IndexDefinition { const connectorMappingsProperties: Record = { api_key_id: { type: 'keyword' }, configuration: { type: 'object' }, + custom_scheduling: { type: 'object' }, description: { type: 'text' }, error: { type: 'keyword' }, features: { diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts index 14c3532efee91c..e6584c0a8b205d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts @@ -85,6 +85,7 @@ describe('addConnector lib function', () => { document: { api_key_id: null, configuration: {}, + custom_scheduling: {}, description: null, error: null, features: null, @@ -269,6 +270,7 @@ describe('addConnector lib function', () => { document: { api_key_id: null, configuration: {}, + custom_scheduling: {}, description: null, error: null, features: null, @@ -375,6 +377,7 @@ describe('addConnector lib function', () => { document: { api_key_id: null, configuration: {}, + custom_scheduling: {}, description: null, error: null, features: null, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts index e697cf57522f88..507ec3fd0bb4ff 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts @@ -93,6 +93,7 @@ export const addConnector = async ( const document: ConnectorDocument = { api_key_id: null, configuration: {}, + custom_scheduling: {}, description: null, error: null, features: null, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts index 144a9b1dbc8b37..25956b271245ac 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts @@ -35,6 +35,7 @@ describe('addConnector lib function', () => { api_key_id: null, configuration: {}, created_at: null, + custom_scheduling: {}, error: null, index_name: 'index_name', last_seen: null, @@ -59,6 +60,7 @@ describe('addConnector lib function', () => { api_key_id: null, configuration: {}, created_at: null, + custom_scheduling: {}, error: null, index_name: 'index_name', last_seen: null, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts index 68d3f7b17ef587..2fa4c0954b2457 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts @@ -34,6 +34,7 @@ describe('addConnector lib function', () => { api_key_id: null, configuration: {}, created_at: null, + custom_scheduling: {}, error: null, index_name: 'index_name', last_seen: null, @@ -61,6 +62,7 @@ describe('addConnector lib function', () => { api_key_id: null, configuration: {}, created_at: null, + custom_scheduling: {}, error: null, index_name: 'index_name', last_seen: null, From 13853a4a5bc70726ddf9168b13eea0e8013bf360 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 1 Feb 2023 15:00:22 +0100 Subject: [PATCH 42/59] Fix escaping of double quote (#150039) The previous version of `escapeSearchQueryPhrase` didn't escape anything. --- x-pack/plugins/fleet/server/services/saved_object.test.ts | 2 +- x-pack/plugins/fleet/server/services/saved_object.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/saved_object.test.ts b/x-pack/plugins/fleet/server/services/saved_object.test.ts index d2683caf9c7258..4dd99a3db2d2b1 100644 --- a/x-pack/plugins/fleet/server/services/saved_object.test.ts +++ b/x-pack/plugins/fleet/server/services/saved_object.test.ts @@ -18,7 +18,7 @@ describe('Saved object service', () => { it('should escape quotes', () => { const res = escapeSearchQueryPhrase('test1"test2'); - expect(res).toEqual(`"test1\"test2"`); + expect(res).toEqual(`"test1\\"test2"`); }); }); }); diff --git a/x-pack/plugins/fleet/server/services/saved_object.ts b/x-pack/plugins/fleet/server/services/saved_object.ts index 6a45061d893342..2a1f5e216fb2f1 100644 --- a/x-pack/plugins/fleet/server/services/saved_object.ts +++ b/x-pack/plugins/fleet/server/services/saved_object.ts @@ -16,7 +16,7 @@ import type { ListWithKuery } from '../types'; * @param val */ export function escapeSearchQueryPhrase(val: string): string { - return `"${val.replace(/["]/g, '"')}"`; + return `"${val.replace(/["]/g, '\\"')}"`; } // Adds `.attributes` to any kuery strings that are missing it, this comes from From 59366f13175c6306768e37ceb0a487c38e267165 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Wed, 1 Feb 2023 14:02:26 +0000 Subject: [PATCH 43/59] [Fleet] Add `getStatusSummary` query parameter to `GET /api/fleet/agents` API (#149963) ## Summary `getAgentStatus` will return a status breakdown for the given `kuery`. The breakdown is returned in the `statusSummary` response key. This allows us to remove an API call on the agents list page, I also think this kind of facet is a good thing to have for our API. We seem to have a mix of camel case and snake case in our API responses, for this API all the response params are camel case so I kept consistent - integration test added - API docs updated Example request and response: ``` GET kbn:/api/fleet/agents?getStatusSummary=true&showInactive=true&perPage=0 { "list": [], "items": [], "total": 1001, "page": 1, "perPage": 0, "statusSummary": { "online": 1, "error": 0, "inactive": 500, "offline": 0, "updating": 0, "unenrolled": 500, "degraded": 0, "enrolling": 0, "unenrolling": 0 } } ``` --- .../plugins/fleet/common/openapi/bundled.json | 44 +++++++++++- .../plugins/fleet/common/openapi/bundled.yaml | 32 ++++++++- .../schemas/get_agents_response.yaml | 21 ++++++ .../fleet/common/openapi/paths/agents.yaml | 5 ++ .../agent_statuses_to_summary.test.ts | 33 +++++++++ .../services/agent_statuses_to_summary.ts | 21 ++++++ x-pack/plugins/fleet/common/services/index.ts | 1 + .../fleet/common/types/rest_spec/agent.ts | 2 + .../agents/agent_list_page/index.test.tsx | 23 +++--- .../sections/agents/agent_list_page/index.tsx | 71 ++++++++----------- .../fleet/server/routes/agent/handlers.ts | 4 +- .../fleet/server/services/agents/crud.ts | 33 ++++++++- .../fleet/server/services/agents/status.ts | 13 ++-- .../fleet/server/types/rest_spec/agent.ts | 1 + .../fleet_api_integration/apis/agents/list.ts | 20 ++++++ 15 files changed, 252 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/fleet/common/services/agent_statuses_to_summary.test.ts create mode 100644 x-pack/plugins/fleet/common/services/agent_statuses_to_summary.ts diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index bfc7208235c56f..0a62474c890569 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -1372,6 +1372,14 @@ }, { "$ref": "#/components/parameters/with_metrics" + }, + { + "name": "getStatusSummary", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } } ], "security": [ @@ -5399,11 +5407,11 @@ "properties": { "cpu_avg": { "type": "number", - "description": "Average agent CPU usage during the last 5 minute, number between 0-1" + "description": "Average agent CPU usage during the last 5 minutes, number between 0-1" }, "memory_size_byte_avg": { "type": "number", - "description": "Average agent memory consumption during the last 5 minute" + "description": "Average agent memory consumption during the last 5 minutes" } } } @@ -5441,6 +5449,38 @@ }, "perPage": { "type": "number" + }, + "statusSummary": { + "type": "object", + "properties": { + "offline": { + "type": "number" + }, + "error": { + "type": "number" + }, + "online": { + "type": "number" + }, + "inactive": { + "type": "number" + }, + "enrolling": { + "type": "number" + }, + "unenrolling": { + "type": "number" + }, + "unenrolled": { + "type": "number" + }, + "updating": { + "type": "number" + }, + "degraded'": { + "type": "number" + } + } } }, "required": [ diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index bd8d18f2be5b11..7b93038b146bcf 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -857,6 +857,11 @@ paths: - $ref: '#/components/parameters/sort_field' - $ref: '#/components/parameters/sort_order' - $ref: '#/components/parameters/with_metrics' + - name: getStatusSummary + in: query + required: false + schema: + type: boolean security: - basicAuth: [] /agents/bulk_upgrade: @@ -3421,11 +3426,11 @@ components: cpu_avg: type: number description: >- - Average agent CPU usage during the last 5 minute, number between - 0-1 + Average agent CPU usage during the last 5 minutes, number + between 0-1 memory_size_byte_avg: type: number - description: Average agent memory consumption during the last 5 minute + description: Average agent memory consumption during the last 5 minutes required: - type - active @@ -3451,6 +3456,27 @@ components: type: number perPage: type: number + statusSummary: + type: object + properties: + offline: + type: number + error: + type: number + online: + type: number + inactive: + type: number + enrolling: + type: number + unenrolling: + type: number + unenrolled: + type: number + updating: + type: number + degraded': + type: number required: - items - total diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/get_agents_response.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/get_agents_response.yaml index 7a4be64d29cbe3..71416b6d4fe7af 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/get_agents_response.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/get_agents_response.yaml @@ -16,6 +16,27 @@ properties: type: number perPage: type: number + statusSummary: + type: object + properties: + offline: + type: number + error: + type: number + online : + type: number + inactive : + type: number + enrolling: + type: number + unenrolling: + type: number + unenrolled : + type: number + updating : + type: number + degraded': + type: number required: - items - total diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents.yaml index cc1aaab2a679c4..44d5e1c8a97381 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents.yaml @@ -18,5 +18,10 @@ get: - $ref: ../components/parameters/sort_field.yaml - $ref: ../components/parameters/sort_order.yaml - $ref: ../components/parameters/with_metrics.yaml + - name: getStatusSummary + in: query + required: false + schema: + type: boolean security: - basicAuth: [] diff --git a/x-pack/plugins/fleet/common/services/agent_statuses_to_summary.test.ts b/x-pack/plugins/fleet/common/services/agent_statuses_to_summary.test.ts new file mode 100644 index 00000000000000..49144a75b56917 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/agent_statuses_to_summary.test.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 { agentStatusesToSummary } from './agent_statuses_to_summary'; + +describe('agentStatusesToSummary', () => { + it('should return the correct summary', () => { + expect( + agentStatusesToSummary({ + online: 1, + error: 2, + degraded: 3, + inactive: 4, + offline: 5, + updating: 6, + enrolling: 7, + unenrolling: 8, + unenrolled: 9, + }) + ).toEqual({ + healthy: 1, + unhealthy: 5, + inactive: 4, + offline: 5, + updating: 21, + unenrolled: 9, + }); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/agent_statuses_to_summary.ts b/x-pack/plugins/fleet/common/services/agent_statuses_to_summary.ts new file mode 100644 index 00000000000000..0c1f0311851d48 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/agent_statuses_to_summary.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentStatus, SimplifiedAgentStatus } from '../types'; + +export function agentStatusesToSummary( + statuses: Record +): Record { + return { + healthy: statuses.online, + unhealthy: statuses.error + statuses.degraded, + inactive: statuses.inactive, + offline: statuses.offline, + updating: statuses.updating + statuses.enrolling + statuses.unenrolling, + unenrolled: statuses.unenrolled, + }; +} diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 6f630cf117e29f..2b5a8dd04bc917 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -58,3 +58,4 @@ export { } from './package_prerelease'; export { getAllowedOutputTypeForPolicy } from './output_helpers'; +export { agentStatusesToSummary } from './agent_statuses_to_summary'; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 77bea13dda8393..10866da70d93ed 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -14,6 +14,7 @@ import type { CurrentUpgrade, NewAgentAction, AgentDiagnostics, + AgentStatus, } from '../models'; import type { ListResult, ListWithKuery } from './common'; @@ -29,6 +30,7 @@ export interface GetAgentsRequest { export interface GetAgentsResponse extends ListResult { // deprecated in 8.x list?: Agent[]; + statusSummary?: Record; } export interface GetAgentTagsResponse { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.test.tsx index 57aa49019851d6..2bc453a45a8885 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.test.tsx @@ -100,14 +100,18 @@ describe('agent_list_page', () => { data: { items: mapAgents(['agent1', 'agent2', 'agent3', 'agent4', 'agent5']), total: 6, - totalInactive: 0, + statusSummary: { + online: 6, + }, }, }) .mockResolvedValueOnce({ data: { items: mapAgents(['agent1', 'agent2', 'agent3', 'agent4', 'agent6']), total: 6, - totalInactive: 0, + statusSummary: { + online: 6, + }, }, }); jest.useFakeTimers({ legacyFakeTimers: true }); @@ -128,12 +132,8 @@ describe('agent_list_page', () => { return { data: { results: { - online: 6, - error: 0, - offline: 0, - updating: 0, + inactive: 0, }, - totalInactive: 0, }, }; }); @@ -143,9 +143,7 @@ describe('agent_list_page', () => { jest.advanceTimersByTime(65000); }); - // we call the status endpoint twice on page load, - // once for all inactive agents and one for the current kuery - expect(mockedSendGetAgentStatus).toHaveBeenCalledTimes(2); + expect(mockedSendGetAgentStatus).toHaveBeenCalledTimes(1); }); describe('selection change', () => { @@ -153,10 +151,7 @@ describe('agent_list_page', () => { mockedSendGetAgentStatus.mockResolvedValue({ data: { results: { - online: 6, - error: 0, - offline: 0, - updating: 0, + inactive: 0, }, totalInactive: 0, }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 6a11e6460022be..f6808ff2fa3276 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -13,6 +13,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { agentStatusesToSummary } from '../../../../../../common/services'; + import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { usePagination, @@ -274,33 +276,27 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { isLoadingVar.current = true; try { setIsLoading(true); - const [ - agentsResponse, - agentsStatusResponse, - totalInactiveAgentsResponse, - agentTagsResponse, - ] = await Promise.all([ - sendGetAgents({ - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: kuery && kuery !== '' ? kuery : undefined, - sortField: getSortFieldForAPI(sortField), - sortOrder, - showInactive, - showUpgradeable, - withMetrics: displayAgentMetrics, - }), - sendGetAgentStatus({ - kuery: kuery && kuery !== '' ? kuery : undefined, - }), - sendGetAgentStatus({ - kuery: AgentStatusKueryHelper.buildKueryForInactiveAgents(), - }), - sendGetAgentTags({ - kuery: kuery && kuery !== '' ? kuery : undefined, - showInactive, - }), - ]); + const [agentsResponse, totalInactiveAgentsResponse, agentTagsResponse] = + await Promise.all([ + sendGetAgents({ + page: pagination.currentPage, + perPage: pagination.pageSize, + kuery: kuery && kuery !== '' ? kuery : undefined, + sortField: getSortFieldForAPI(sortField), + sortOrder, + showInactive, + showUpgradeable, + getStatusSummary: true, + withMetrics: displayAgentMetrics, + }), + sendGetAgentStatus({ + kuery: AgentStatusKueryHelper.buildKueryForInactiveAgents(), + }), + sendGetAgentTags({ + kuery: kuery && kuery !== '' ? kuery : undefined, + showInactive, + }), + ]); isLoadingVar.current = false; // Return if a newer request has been triggered if (currentRequestRef.current !== currentRequest) { @@ -312,27 +308,22 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { if (!agentsResponse.data) { throw new Error('Invalid GET /agents response'); } - if (agentsStatusResponse.error) { - throw agentsStatusResponse.error; - } - if (!agentsStatusResponse.data || !totalInactiveAgentsResponse.data) { + if (!totalInactiveAgentsResponse.data) { throw new Error('Invalid GET /agents_status response'); } if (agentTagsResponse.error) { - throw agentsStatusResponse.error; + throw agentTagsResponse.error; } if (!agentTagsResponse.data) { throw new Error('Invalid GET /agent/tags response'); } - setAgentsStatus({ - healthy: agentsStatusResponse.data.results.online, - unhealthy: agentsStatusResponse.data.results.error, - offline: agentsStatusResponse.data.results.offline, - updating: agentsStatusResponse.data.results.updating, - inactive: agentsStatusResponse.data.results.inactive, - unenrolled: agentsStatusResponse.data.results.unenrolled, - }); + const statusSummary = agentsResponse.data.statusSummary; + + if (!statusSummary) { + throw new Error('Invalid GET /agents response - no status summary'); + } + setAgentsStatus(agentStatusesToSummary(statusSummary)); const newAllTags = agentTagsResponse.data.items; diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index a44f17547d60b3..24dd0d355411d2 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -186,9 +186,10 @@ export const getAgentsHandler: RequestHandler< kuery: request.query.kuery, sortField: request.query.sortField, sortOrder: request.query.sortOrder, + getStatusSummary: request.query.getStatusSummary, }); - const { total, page, perPage } = agentRes; + const { total, page, perPage, statusSummary } = agentRes; let { agents } = agentRes; // Assign metrics @@ -202,6 +203,7 @@ export const getAgentsHandler: RequestHandler< total, page, perPage, + ...(statusSummary ? { statusSummary } : {}), }; return response.ok({ body }); } catch (error) { diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 3445aa36da42e8..379a656b7d519f 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -13,7 +13,7 @@ import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { appContextService, agentPolicyService } from '..'; -import type { FleetServerAgent } from '../../../common/types'; +import type { AgentStatus, FleetServerAgent } from '../../../common/types'; import { SO_SEARCH_LIMIT } from '../../../common/constants'; import { isAgentUpgradeable } from '../../../common/services'; import { AGENTS_INDEX } from '../../constants'; @@ -194,6 +194,7 @@ export async function getAgentsByKuery( soClient: SavedObjectsClientContract, options: ListWithKuery & { showInactive: boolean; + getStatusSummary?: boolean; sortField?: string; sortOrder?: 'asc' | 'desc'; pitId?: string; @@ -204,6 +205,7 @@ export async function getAgentsByKuery( total: number; page: number; perPage: number; + statusSummary?: Record; }> { const { page = 1, @@ -212,6 +214,7 @@ export async function getAgentsByKuery( sortOrder = options.sortOrder ?? 'desc', kuery, showInactive = false, + getStatusSummary = false, showUpgradeable, searchAfter, pitId, @@ -235,8 +238,24 @@ export async function getAgentsByKuery( const secondarySort: estypes.Sort = isDefaultSort ? [{ 'local_metadata.host.hostname.keyword': { order: 'asc' } }] : []; + + const statusSummary: Record = { + online: 0, + error: 0, + inactive: 0, + offline: 0, + updating: 0, + unenrolled: 0, + degraded: 0, + enrolling: 0, + unenrolling: 0, + }; + const queryAgents = async (from: number, size: number) => - esClient.search({ + esClient.search< + FleetServerAgent, + { status: { buckets: Array<{ key: AgentStatus; doc_count: number }> } } + >({ from, size, track_total_hits: true, @@ -244,7 +263,7 @@ export async function getAgentsByKuery( runtime_mappings: runtimeFields, fields: Object.keys(runtimeFields), sort: [{ [sortField]: { order: sortOrder } }, ...secondarySort], - post_filter: kueryNode ? toElasticsearchQuery(kueryNode) : undefined, + query: kueryNode ? toElasticsearchQuery(kueryNode) : undefined, ...(pitId ? { pit: { @@ -257,6 +276,7 @@ export async function getAgentsByKuery( ignore_unavailable: true, }), ...(pitId && searchAfter ? { search_after: searchAfter, from: 0 } : {}), + ...(getStatusSummary && { aggs: { status: { terms: { field: 'status' } } } }), }); let res; try { @@ -289,11 +309,18 @@ export async function getAgentsByKuery( } } + if (getStatusSummary) { + res.aggregations?.status.buckets.forEach((bucket) => { + statusSummary[bucket.key] = bucket.doc_count; + }); + } + return { agents, total, page, perPage, + ...(getStatusSummary ? { statusSummary } : {}), }; } diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index fecc369f6ad33d..88761c53ee4734 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -16,6 +16,8 @@ import type { QueryDslQueryContainer, } from '@elastic/elasticsearch/lib/api/types'; +import { agentStatusesToSummary } from '../../../common/services'; + import { AGENTS_INDEX } from '../../constants'; import type { AgentStatus } from '../../types'; import { FleetUnauthorizedError } from '../../errors'; @@ -122,15 +124,8 @@ export async function getAgentStatusForAgentPolicy( } }); - const combinedStatuses = { - online: statuses.online, - error: statuses.error + statuses.degraded, - inactive: statuses.inactive, - offline: statuses.offline, - updating: statuses.updating + statuses.enrolling + statuses.unenrolling, - unenrolled: statuses.unenrolled, - }; - + const { healthy: online, unhealthy: error, ...otherStatuses } = agentStatusesToSummary(statuses); + const combinedStatuses = { online, error, ...otherStatuses }; return { ...combinedStatuses, /* @deprecated no agents will have other status */ diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 96227b2c33bfee..92b0f098ae19da 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -22,6 +22,7 @@ export const GetAgentsRequestSchema = { showInactive: schema.boolean({ defaultValue: false }), withMetrics: schema.boolean({ defaultValue: false }), showUpgradeable: schema.boolean({ defaultValue: false }), + getStatusSummary: schema.boolean({ defaultValue: false }), sortField: schema.maybe(schema.string()), sortOrder: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), }, diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 405efd73c2ce7b..35f6a2326f9008 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -197,5 +197,25 @@ export default function ({ getService }: FtrProviderContext) { expect(agent2.metrics?.memory_size_byte_avg).equal(undefined); expect(agent2.metrics?.cpu_avg).equal(undefined); }); + + it('should return a status summary if getStatusSummary provided', async () => { + const { body: apiResponse } = await supertest + .get('/api/fleet/agents?getStatusSummary=true&perPage=0') + .expect(200); + + expect(apiResponse.items).to.eql([]); + + expect(apiResponse.statusSummary).to.eql({ + degraded: 0, + enrolling: 0, + error: 0, + inactive: 0, + offline: 4, + online: 0, + unenrolled: 0, + unenrolling: 0, + updating: 0, + }); + }); }); } From eabf08bbab8525f17e8b85d3142b1c210298dd28 Mon Sep 17 00:00:00 2001 From: Lola Date: Wed, 1 Feb 2023 09:26:53 -0500 Subject: [PATCH 44/59] [Cloud Posture] [Findings] Fix Findings empty state results error (#149716) Issue #144981 ## Summary This PR fixes the resource findings error pop-up when a query filter yields no results. Suppose a filter query produces zero results then the aggregations.key.buckets will return an empty array. In the `assertNonEmptyArray`, we throw an error if the bucket is not an array or if the bucket's array.length is 0. We shouldn't throw an error if there are no results or if buckets is an empty array`[]`. To solve this issue, we need to remove the `arr.length === 0` condition check from `assertNonEmptyArray`. The following changes were also introduced: - Removed `arr.length === 0` check to show an empty state. - Added unit tests for Resource Findings Table for cases empty state or table data - Added a safety check and provided a default value for the first bucket key. - Update translations for Findings page title ## Screenshot Success state image Empty State image --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../resource_findings_container.tsx | 17 ++-- .../resource_findings_table.test.tsx | 77 +++++++++++++++++++ .../resource_findings_table.tsx | 8 +- .../use_resource_findings.ts | 37 ++++----- .../public/pages/findings/test_subjects.ts | 5 ++ .../fixtures/resource_findings_fixture.ts | 64 +++++++++++++++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/test/fixtures/resource_findings_fixture.ts diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx index 05cc930a314f83..79679d3cb33791 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx @@ -135,6 +135,15 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { }), }); }; + function resourceFindingsPageTitleTranslation(resourceName: string | undefined) { + return i18n.translate('xpack.csp.findings.resourceFindings.resourceFindingsPageTitle', { + defaultMessage: '{resourceName} {hyphen} Findings', + values: { + resourceName, + hyphen: resourceFindings.data?.resourceName ? '-' : '', + }, + }); + } return (
@@ -150,13 +159,7 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { } /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx new file mode 100644 index 00000000000000..ea9aba41abe4fb --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen, within } from '@testing-library/react'; +import * as TEST_SUBJECTS from '../../test_subjects'; +import { ResourceFindingsTable, ResourceFindingsTableProps } from './resource_findings_table'; +import { TestProvider } from '../../../../test/test_provider'; + +import { capitalize } from 'lodash'; +import moment from 'moment'; +import { getResourceFindingsTableFixture } from '../../../../test/fixtures/resource_findings_fixture'; + +describe('', () => { + it('should render no findings empty state when status success and data has a length of zero ', async () => { + const resourceFindingsProps: ResourceFindingsTableProps = { + loading: false, + items: [], + pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, + sorting: { + sort: { field: '@timestamp', direction: 'desc' }, + }, + setTableOptions: jest.fn(), + onAddFilter: jest.fn(), + }; + + render( + + + + ); + + expect( + screen.getByTestId(TEST_SUBJECTS.RESOURCES_FINDINGS_TABLE_EMPTY_STATE) + ).toBeInTheDocument(); + }); + + it('should render resource finding table content when data items exists', () => { + const data = Array.from({ length: 10 }, getResourceFindingsTableFixture); + + const props: ResourceFindingsTableProps = { + loading: false, + items: data, + pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, + sorting: { + sort: { field: 'cluster_id', direction: 'desc' }, + }, + setTableOptions: jest.fn(), + onAddFilter: jest.fn(), + }; + + render( + + + + ); + + data.forEach((item, i) => { + const row = screen.getByTestId( + TEST_SUBJECTS.getResourceFindingsTableRowTestId(item.resource.id) + ); + const { evaluation } = item.result; + const evaluationStatusText = capitalize( + item.result.evaluation.slice(0, evaluation.length - 2) + ); + + expect(row).toBeInTheDocument(); + expect(within(row).queryByText(item.rule.name)).toBeInTheDocument(); + expect(within(row).queryByText(evaluationStatusText)).toBeInTheDocument(); + expect(within(row).queryByText(moment(item['@timestamp']).fromNow())).toBeInTheDocument(); + expect(within(row).queryByText(item.rule.section)).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx index 41f18af723b8ec..50a9c19c6b6abb 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx @@ -25,8 +25,9 @@ import { } from '../../layout/findings_layout'; import { FindingsRuleFlyout } from '../../findings_flyout/findings_flyout'; import { getSelectedRowStyle } from '../../utils/utils'; +import * as TEST_SUBJECTS from '../../test_subjects'; -interface Props { +export interface ResourceFindingsTableProps { items: CspFinding[]; loading: boolean; pagination: Pagination; @@ -42,12 +43,13 @@ const ResourceFindingsTableComponent = ({ sorting, setTableOptions, onAddFilter, -}: Props) => { +}: ResourceFindingsTableProps) => { const { euiTheme } = useEuiTheme(); const [selectedFinding, setSelectedFinding] = useState(); const getRowProps = (row: CspFinding) => ({ style: getSelectedRowStyle(euiTheme, row, selectedFinding), + 'data-test-subj': TEST_SUBJECTS.getResourceFindingsTableRowTestId(row.resource.id), }); const columns: [ @@ -69,6 +71,7 @@ const ResourceFindingsTableComponent = ({ return ( >; -export type ResourceFindingsResponseAggs = Record< - 'count' | 'clusterId' | 'resourceSubType' | 'resourceName', - estypes.AggregationsMultiBucketAggregateBase ->; +export interface ResourceFindingsResponseAggs { + count?: estypes.AggregationsMultiBucketAggregateBase; + clusterId?: estypes.AggregationsMultiBucketAggregateBase; + resourceSubType?: estypes.AggregationsMultiBucketAggregateBase; + resourceName?: estypes.AggregationsMultiBucketAggregateBase; +} const getResourceFindingsQuery = ({ query, @@ -92,19 +94,18 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => { keepPreviousData: true, select: ({ rawResponse: { hits, aggregations } }: ResourceFindingsResponse) => { if (!aggregations) throw new Error('expected aggregations to exists'); - - assertNonEmptyArray(aggregations.count.buckets); - assertNonEmptyArray(aggregations.clusterId.buckets); - assertNonEmptyArray(aggregations.resourceSubType.buckets); - assertNonEmptyArray(aggregations.resourceName.buckets); + assertNonBucketsArray(aggregations.count?.buckets); + assertNonBucketsArray(aggregations.clusterId?.buckets); + assertNonBucketsArray(aggregations.resourceSubType?.buckets); + assertNonBucketsArray(aggregations.resourceName?.buckets); return { page: hits.hits.map((hit) => hit._source!), total: number.is(hits.total) ? hits.total : 0, - count: getAggregationCount(aggregations.count.buckets), - clusterId: getFirstBucketKey(aggregations.clusterId.buckets), - resourceSubType: getFirstBucketKey(aggregations.resourceSubType.buckets), - resourceName: getFirstBucketKey(aggregations.resourceName.buckets), + count: getAggregationCount(aggregations.count?.buckets), + clusterId: getFirstBucketKey(aggregations.clusterId?.buckets), + resourceSubType: getFirstBucketKey(aggregations.resourceSubType?.buckets), + resourceName: getFirstBucketKey(aggregations.resourceName?.buckets), }; }, onError: (err: Error) => showErrorToast(toasts, err), @@ -112,11 +113,11 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => { ); }; -function assertNonEmptyArray(arr: unknown): asserts arr is T[] { - if (!Array.isArray(arr) || arr.length === 0) { - throw new Error('expected a non empty array'); +function assertNonBucketsArray(arr: unknown): asserts arr is T[] { + if (!Array.isArray(arr)) { + throw new Error('expected buckets to be an array'); } } -const getFirstBucketKey = (buckets: estypes.AggregationsStringRareTermsBucketKeys[]) => - buckets[0].key; +const getFirstBucketKey = (buckets: estypes.AggregationsStringRareTermsBucketKeys[]): string => + buckets[0]?.key; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/test_subjects.ts index 28c15e49138005..153811865c5c3a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/test_subjects.ts @@ -21,5 +21,10 @@ export const getFindingsTableCellTestId = (columnId: string, rowId: string) => export const FINDINGS_TABLE_CELL_ADD_FILTER = 'findings_table_cell_add_filter'; export const FINDINGS_TABLE_CELL_ADD_NEGATED_FILTER = 'findings_table_cell_add_negated_filter'; +export const RESOURCES_FINDINGS_TABLE_EMPTY_STATE = 'resource_findings_table_empty_state'; +export const RESOURCES_FINDINGS_TABLE = 'resource_findings_table'; +export const getResourceFindingsTableRowTestId = (id: string) => + `resource_findings_table_row_${id}`; + export const DASHBOARD_TABLE_HEADER_SCORE_TEST_ID = 'csp:dashboard-sections-table-header-score'; export const DASHBOARD_TABLE_COLUMN_SCORE_TEST_ID = 'csp:dashboard-sections-table-column-score'; diff --git a/x-pack/plugins/cloud_security_posture/public/test/fixtures/resource_findings_fixture.ts b/x-pack/plugins/cloud_security_posture/public/test/fixtures/resource_findings_fixture.ts new file mode 100644 index 00000000000000..07b1b581b828e8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/test/fixtures/resource_findings_fixture.ts @@ -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 { EcsEvent } from '@kbn/ecs'; +import Chance from 'chance'; +import { CspFinding } from '../../../common/schemas/csp_finding'; + +const chance = new Chance(); + +export const getResourceFindingsTableFixture = (): CspFinding & { id: string } => ({ + cluster_id: chance.guid(), + id: chance.word(), + result: { + expected: { + source: {}, + }, + evaluation: chance.weighted(['passed', 'failed'], [0.5, 0.5]), + evidence: { + filemode: chance.word(), + }, + }, + rule: { + audit: chance.paragraph(), + benchmark: { + name: 'CIS Kubernetes', + version: '1.6.0', + id: 'cis_k8s', + }, + default_value: chance.sentence(), + description: chance.paragraph(), + id: chance.guid(), + impact: chance.word(), + name: chance.string(), + profile_applicability: chance.sentence(), + rationale: chance.paragraph(), + references: chance.paragraph(), + rego_rule_id: 'cis_X_X_X', + remediation: chance.word(), + section: chance.sentence(), + tags: [], + version: '1.0', + }, + agent: { + id: chance.string(), + name: chance.string(), + type: chance.string(), + version: chance.string(), + }, + resource: { + name: chance.string(), + type: chance.string(), + raw: {} as any, + sub_type: chance.string(), + id: chance.string(), + }, + host: {} as any, + ecs: {} as any, + event: {} as EcsEvent, + '@timestamp': new Date().toISOString(), +}); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9b8635876b4ca9..4e4e22cefafec5 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10055,7 +10055,6 @@ "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "Affichage de {pageStart}-{pageEnd} sur {total} {type}", "xpack.csp.findings.findingsTableCell.addFilterButton": "Ajouter un filtre {field}", "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "Ajouter un filtre {field} négatif", - "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - Résultats", "xpack.csp.rules.header.rulesCountLabel": "{count, plural, one { règle} other { règles}}", "xpack.csp.rules.header.totalRulesCount": "Affichage des {rules}", "xpack.csp.rules.rulePageHeader.pageHeaderTitle": "Règles - {integrationName}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5bdecf9a74dc1f..2b089e8c055b81 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10044,7 +10044,6 @@ "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "{total}件中{pageStart}-{pageEnd}件の{type}を表示しています", "xpack.csp.findings.findingsTableCell.addFilterButton": "{field}フィルターを追加", "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "{field}否定フィルターを追加", - "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - 調査結果", "xpack.csp.rules.header.rulesCountLabel": "{count, plural, other {個のルール}}", "xpack.csp.rules.header.totalRulesCount": "{rules}を表示しています", "xpack.csp.rules.rulePageHeader.pageHeaderTitle": "ルール - {integrationName}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fe57899b38b34c..849f629a69eea9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10059,7 +10059,6 @@ "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "正在显示第 {pageStart}-{pageEnd} 个(共 {total} 个){type}", "xpack.csp.findings.findingsTableCell.addFilterButton": "添加 {field} 筛选", "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "添加 {field} 作废筛选", - "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - 结果", "xpack.csp.rules.header.rulesCountLabel": "{count, plural, other { 规则}}", "xpack.csp.rules.header.totalRulesCount": "正在显示 {rules}", "xpack.csp.rules.rulePageHeader.pageHeaderTitle": "规则 - {integrationName}", From a1251a93c2995a9c31cbbb3bbffa20cc7f657305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 1 Feb 2023 15:32:59 +0100 Subject: [PATCH 45/59] [Fleet] Move callbacks from http methods to package policy service (#149272) Closes https://github.com/elastic/kibana/issues/129383 This PR ensures that fleet callbacks are called regardless if operations on a package policy are performed via the api or directly using the package policy service. --- x-pack/plugins/apm/server/plugin.ts | 6 + .../fleet/register_fleet_policy_callbacks.ts | 1 - .../server/plugin.test.ts | 45 ++- .../cloud_security_posture/server/plugin.ts | 13 +- .../routes/package_policy/handlers.test.ts | 196 +------------ .../server/routes/package_policy/handlers.ts | 94 +----- .../server/services/agent_policy.test.ts | 13 - .../fleet/server/services/agent_policy.ts | 29 +- .../server/services/package_policy.test.ts | 109 +++++-- .../fleet/server/services/package_policy.ts | 275 +++++++++++++----- .../server/services/package_policy_service.ts | 44 ++- .../plugins/fleet/server/types/extensions.ts | 31 +- .../fleet_integration.test.ts | 41 ++- .../fleet_integration/fleet_integration.ts | 24 +- 14 files changed, 469 insertions(+), 452 deletions(-) diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index ce5c58bcffe35d..86907fdd570bee 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -225,6 +225,9 @@ export class APMPlugin coreStartPromise: getCoreStart(), plugins: resourcePlugins, config: currentConfig, + }).catch((e) => { + this.logger?.error('Failed to register APM Fleet policy callbacks'); + this.logger?.error(e); }); // This will add an API key to all existing APM package policies @@ -232,6 +235,9 @@ export class APMPlugin coreStartPromise: getCoreStart(), pluginStartPromise: getPluginStart(), logger: this.logger, + }).catch((e) => { + this.logger?.error('Failed to add API keys to APM package policies'); + this.logger?.error(e); }); const taskManager = plugins.taskManager; diff --git a/x-pack/plugins/apm/server/routes/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/routes/fleet/register_fleet_policy_callbacks.ts index 9057ab065cacbb..13815779c60136 100644 --- a/x-pack/plugins/apm/server/routes/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/routes/fleet/register_fleet_policy_callbacks.ts @@ -75,7 +75,6 @@ function onPackagePolicyDelete({ logger: Logger; }): PostPackagePolicyDeleteCallback { return async (packagePolicies) => { - // console.log(`packagePolicyDelete:`, packagePolicies); const promises = packagePolicies.map(async (packagePolicy) => { if (packagePolicy.package?.name !== 'apm') { return packagePolicy; diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts index 17c08cf3006e98..4a3b85b41a1593 100644 --- a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; +import { + coreMock, + elasticsearchServiceMock, + httpServerMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; import { createPackagePolicyServiceMock, createArtifactsClientMock, @@ -43,6 +48,7 @@ import { } from '@kbn/core/server'; import { securityMock } from '@kbn/security-plugin/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import * as onPackagePolicyPostCreateCallback from './fleet_integration/fleet_integration'; const chance = new Chance(); @@ -147,12 +153,18 @@ describe('Cloud Security Posture Plugin', () => { }); it('should initialize when new package is created', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; fleetMock.packageService.asInternalUser.getInstallation.mockImplementationOnce( async (): Promise => { return; } ); + const onPackagePolicyPostCreateCallbackSpy = jest + .spyOn(onPackagePolicyPostCreateCallback, 'onPackagePolicyPostCreateCallback') + .mockResolvedValue(); + const packageMock = createPackagePolicyMock(); packageMock.package!.name = CLOUD_SECURITY_POSTURE_PACKAGE_NAME; @@ -172,19 +184,30 @@ describe('Cloud Security Posture Plugin', () => { await mockPlugins.fleet.fleetSetupCompleted(); // Assert + expect(onPackagePolicyPostCreateCallbackSpy).not.toHaveBeenCalled(); expect(fleetMock.packageService.asInternalUser.getInstallation).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(0); expect(packagePolicyPostCreateCallbacks.length).toBeGreaterThan(0); for (const cb of packagePolicyPostCreateCallbacks) { - await cb(packageMock, contextMock, httpServerMock.createKibanaRequest()); + await cb( + packageMock, + soClient, + esClient, + contextMock, + httpServerMock.createKibanaRequest() + ); } + expect(onPackagePolicyPostCreateCallbackSpy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledTimes(1); }); it('should not initialize when other package is created', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + fleetMock.packageService.asInternalUser.getInstallation.mockImplementationOnce( async (): Promise => { return; @@ -216,7 +239,13 @@ describe('Cloud Security Posture Plugin', () => { expect(packagePolicyPostCreateCallbacks.length).toBeGreaterThan(0); for (const cb of packagePolicyPostCreateCallbacks) { - await cb(packageMock, contextMock, httpServerMock.createKibanaRequest()); + await cb( + packageMock, + soClient, + esClient, + contextMock, + httpServerMock.createKibanaRequest() + ); } expect(spy).toHaveBeenCalledTimes(0); @@ -266,9 +295,14 @@ describe('Cloud Security Posture Plugin', () => { expect(packagePolicyPostCreateCallbacks.length).toBeGreaterThan(0); + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + for (const cb of packagePolicyPostCreateCallbacks) { const updatedPackagePolicy = await cb( packageMock, + soClient, + esClient, contextMock, httpServerMock.createKibanaRequest() ); @@ -284,6 +318,9 @@ describe('Cloud Security Posture Plugin', () => { ])( 'should uninstall resources when package is removed', async (total, items, expectedNumberOfCallsToUninstallResources) => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + fleetMock.packagePolicyService.list.mockImplementationOnce( async (): Promise> => { return { @@ -320,7 +357,7 @@ describe('Cloud Security Posture Plugin', () => { expect(packagePolicyPostDeleteCallbacks.length).toBeGreaterThan(0); for (const cb of packagePolicyPostDeleteCallbacks) { - await cb(deletedPackagePolicyMock); + await cb(deletedPackagePolicyMock, soClient, esClient); } expect(fleetMock.packagePolicyService.list).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(expectedNumberOfCallsToUninstallResources); diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.ts b/x-pack/plugins/cloud_security_posture/server/plugin.ts index 027f92b293d786..11875fab1213ee 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -6,13 +6,12 @@ */ import type { - KibanaRequest, - RequestHandlerContext, PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger, + SavedObjectsClientContract, } from '@kbn/core/server'; import type { DeepReadonly } from 'utility-types'; import type { @@ -107,11 +106,7 @@ export class CspPlugin plugins.fleet.registerExternalCallback( 'packagePolicyCreate', - async ( - packagePolicy: NewPackagePolicy, - _context: RequestHandlerContext, - _request: KibanaRequest - ): Promise => { + async (packagePolicy: NewPackagePolicy): Promise => { const license = await plugins.licensing.refresh(); if (isCspPackage(packagePolicy.package?.name)) { if (!isSubscriptionAllowed(this.isCloudEnabled, license)) { @@ -129,12 +124,10 @@ export class CspPlugin 'packagePolicyPostCreate', async ( packagePolicy: PackagePolicy, - context: RequestHandlerContext, - _: KibanaRequest + soClient: SavedObjectsClientContract ): Promise => { if (isCspPackage(packagePolicy.package?.name)) { await this.initialize(core, plugins.taskManager); - const soClient = (await context.core).savedObjects.client; await onPackagePolicyPostCreateCallback(this.logger, packagePolicy, soClient); return packagePolicy; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 04dce51e529694..92ab414eab2b19 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -14,16 +14,8 @@ import type { FleetAuthzRouter } from '../../services/security'; import { PACKAGE_POLICY_API_ROUTES } from '../../../common/constants'; import { appContextService, packagePolicyService } from '../../services'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; -import type { - PackagePolicyClient, - PostPackagePolicyCreateCallback, - PutPackagePolicyUpdateCallback, - FleetRequestHandlerContext, -} from '../..'; -import type { - CreatePackagePolicyRequestSchema, - UpdatePackagePolicyRequestSchema, -} from '../../types/rest_spec'; +import type { PackagePolicyClient, FleetRequestHandlerContext } from '../..'; +import type { UpdatePackagePolicyRequestSchema } from '../../types/rest_spec'; import type { FleetRequestHandler } from '../../types'; import type { PackagePolicy } from '../../types'; @@ -125,7 +117,6 @@ describe('When calling package policy', () => { let routeConfig: RouteConfig; let context: FleetRequestHandlerContext; let response: ReturnType; - let packagePolicyServiceWithAuthzMock: jest.Mocked; beforeEach(() => { routerMock = httpServiceMock.createRouter() as unknown as jest.Mocked; @@ -135,8 +126,7 @@ describe('When calling package policy', () => { beforeEach(async () => { appContextService.start(createAppContextStartContractMock()); context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; - packagePolicyServiceWithAuthzMock = (await context.fleet).packagePolicyService - .asCurrentUser as jest.Mocked; + (await context.fleet).packagePolicyService.asCurrentUser as jest.Mocked; response = httpServerMock.createResponseFactory(); }); @@ -145,186 +135,6 @@ describe('When calling package policy', () => { appContextService.stop(); }); - describe('create api handler', () => { - const getCreateKibanaRequest = ( - newData?: typeof CreatePackagePolicyRequestSchema.body - ): KibanaRequest => { - return httpServerMock.createKibanaRequest< - undefined, - undefined, - typeof CreatePackagePolicyRequestSchema.body - >({ - path: routeConfig.path, - method: 'post', - body: newData || { - name: 'endpoint-1', - description: '', - policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', - enabled: true, - inputs: [], - namespace: 'default', - package: { name: 'endpoint', title: 'Elastic Endpoint', version: '0.5.0' }, - }, - }); - }; - - // Set the routeConfig and routeHandler to the Create API - beforeEach(() => { - [routeConfig, routeHandler] = routerMock.post.mock.calls.find( - ([{ path }]) => path === PACKAGE_POLICY_API_ROUTES.CREATE_PATTERN - )!; - }); - - describe('and external callbacks are registered', () => { - const callbackCallingOrder: string[] = []; - - // Callback one adds an input that includes a `config` property - const callbackOne: PostPackagePolicyCreateCallback | PutPackagePolicyUpdateCallback = jest.fn( - async (ds) => { - callbackCallingOrder.push('one'); - const newDs = { - ...ds, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - one: { - value: 'inserted by callbackOne', - }, - }, - }, - ], - }; - return newDs; - } - ); - - // Callback two adds an additional `input[0].config` property - const callbackTwo: PostPackagePolicyCreateCallback | PutPackagePolicyUpdateCallback = jest.fn( - async (ds) => { - callbackCallingOrder.push('two'); - const newDs = { - ...ds, - inputs: [ - { - ...ds.inputs[0], - config: { - ...ds.inputs[0].config, - two: { - value: 'inserted by callbackTwo', - }, - }, - }, - ], - }; - return newDs; - } - ); - - beforeEach(() => { - appContextService.addExternalCallback('packagePolicyCreate', callbackOne); - appContextService.addExternalCallback('packagePolicyCreate', callbackTwo); - }); - - afterEach(() => (callbackCallingOrder.length = 0)); - - it('should create with data from callback', async () => { - const request = getCreateKibanaRequest(); - packagePolicyServiceMock.runExternalCallbacks.mockImplementationOnce(() => - Promise.resolve({ - policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', - description: '', - enabled: true, - inputs: [ - { - config: { - one: { - value: 'inserted by callbackOne', - }, - two: { - value: 'inserted by callbackTwo', - }, - }, - enabled: true, - streams: [], - type: 'endpoint', - }, - ], - name: 'endpoint-1', - namespace: 'default', - package: { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - }, - }) - ); - await routeHandler(context, request, response); - expect(response.ok).toHaveBeenCalled(); - - expect(packagePolicyServiceWithAuthzMock.create.mock.calls[0][2]).toEqual({ - policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', - description: '', - enabled: true, - inputs: [ - { - config: { - one: { - value: 'inserted by callbackOne', - }, - two: { - value: 'inserted by callbackTwo', - }, - }, - enabled: true, - streams: [], - type: 'endpoint', - }, - ], - name: 'endpoint-1', - namespace: 'default', - package: { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - }, - }); - }); - }); - - describe('postCreate callback registration', () => { - it('should call to packagePolicyCreate and packagePolicyPostCreate call backs', async () => { - const request = getCreateKibanaRequest(); - await routeHandler(context, request, response); - - expect(response.ok).toHaveBeenCalled(); - expect(packagePolicyService.runExternalCallbacks).toBeCalledTimes(2); - - const firstCB = packagePolicyServiceMock.runExternalCallbacks.mock.calls[0][0]; - const secondCB = packagePolicyServiceMock.runExternalCallbacks.mock.calls[1][0]; - - expect(firstCB).toEqual('packagePolicyCreate'); - expect(secondCB).toEqual('packagePolicyPostCreate'); - }); - - it('should not call packagePolicyPostCreate call back in case of packagePolicy create failed', async () => { - const request = getCreateKibanaRequest(); - - packagePolicyServiceWithAuthzMock.create.mockImplementationOnce(() => { - throw new Error('foo'); - }); - - await routeHandler(context, request, response); - const firstCB = packagePolicyServiceMock.runExternalCallbacks.mock.calls[0][0]; - - expect(firstCB).toEqual('packagePolicyCreate'); - expect(packagePolicyService.runExternalCallbacks).toBeCalledTimes(1); - }); - }); - }); - describe('update api handler', () => { const getUpdateKibanaRequest = ( newData?: typeof UpdatePackagePolicyRequestSchema.body diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 8735cdf08cafd0..51b4056843d25f 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -247,33 +247,21 @@ export const createPackagePolicyHandler: FleetRequestHandler< } as NewPackagePolicy); } - const newData = await packagePolicyService.runExternalCallbacks( - 'packagePolicyCreate', - newPackagePolicy, - context, - request - ); - // Create package policy const packagePolicy = await fleetContext.packagePolicyService.asCurrentUser.create( soClient, esClient, - newData, + newPackagePolicy, { user, force, spaceId, - } - ); - - const enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks( - 'packagePolicyPostCreate', - packagePolicy, + }, context, request ); - const body: CreatePackagePolicyResponse = { item: enrichedPackagePolicy }; + const body: CreatePackagePolicyResponse = { item: packagePolicy }; return response.ok({ body, @@ -368,12 +356,6 @@ export const updatePackagePolicyHandler: FleetRequestHandler< vars: body.vars ?? packagePolicy.vars, } as NewPackagePolicy; } - newData = await packagePolicyService.runExternalCallbacks( - 'packagePolicyUpdate', - newData, - context, - request - ); const updatedPackagePolicy = await packagePolicyService.update( soClient, @@ -400,46 +382,17 @@ export const deletePackagePolicyHandler: RequestHandler< const soClient = coreContext.savedObjects.client; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; - const logger = appContextService.getLogger(); try { - try { - const packagePolicies = await packagePolicyService.getByIDs( - soClient, - request.body.packagePolicyIds, - { ignoreMissing: true } - ); - - if (packagePolicies) { - await packagePolicyService.runExternalCallbacks( - 'packagePolicyDelete', - packagePolicies, - context, - request - ); - } - } catch (error) { - logger.error(`An error occurred executing external callback: ${error}`); - logger.error(error); - } - const body: PostDeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, esClient, request.body.packagePolicyIds, - { user, force: request.body.force, skipUnassignFromAgentPolicies: request.body.force } + { user, force: request.body.force, skipUnassignFromAgentPolicies: request.body.force }, + context, + request ); - try { - await packagePolicyService.runExternalCallbacks( - 'packagePolicyPostDelete', - body, - context, - request - ); - } catch (error) { - logger.error(`An error occurred executing external callback: ${error}`); - logger.error(error); - } + return response.ok({ body, }); @@ -457,30 +410,15 @@ export const deleteOnePackagePolicyHandler: RequestHandler< const soClient = coreContext.savedObjects.client; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; - const logger = appContextService.getLogger(); try { - try { - const packagePolicy = await packagePolicyService.get( - soClient, - request.params.packagePolicyId - ); - await packagePolicyService.runExternalCallbacks( - 'packagePolicyDelete', - packagePolicy ? [packagePolicy] : [], - context, - request - ); - } catch (error) { - logger.error(`An error occurred executing external callback: ${error}`); - logger.error(error); - } - const res = await packagePolicyService.delete( soClient, esClient, [request.params.packagePolicyId], - { user, force: request.query.force, skipUnassignFromAgentPolicies: request.query.force } + { user, force: request.query.force, skipUnassignFromAgentPolicies: request.query.force }, + context, + request ); if ( @@ -493,17 +431,7 @@ export const deleteOnePackagePolicyHandler: RequestHandler< body: res[0].body, }); } - try { - await packagePolicyService.runExternalCallbacks( - 'packagePolicyPostDelete', - res, - context, - request - ); - } catch (error) { - logger.error(`An error occurred executing external callback: ${error}`); - logger.error(error); - } + return response.ok({ body: { id: request.params.packagePolicyId }, }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 014764743ee35e..b223e88102d9cb 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -169,13 +169,6 @@ describe('agent policy', () => { ]); }); - it('should run package policy delete external callbacks', async () => { - await agentPolicyService.delete(soClient, esClient, 'mocked'); - expect(packagePolicyService.runPostDeleteExternalCallbacks).toHaveBeenCalledWith([ - { id: 'package-1' }, - ]); - }); - it('should throw error for agent policy which has managed package poolicy', async () => { mockedPackagePolicyService.findAllForAgentPolicy.mockReturnValue([ { @@ -192,12 +185,6 @@ describe('agent policy', () => { ).message ); } - - await agentPolicyService.delete(soClient, esClient, 'mocked', { force: true }); - - expect(packagePolicyService.runPostDeleteExternalCallbacks).toHaveBeenCalledWith([ - { id: 'package-1' }, - ]); }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index f2e51b9c95bf52..d845d466ea3815 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -51,7 +51,6 @@ import type { FleetServerPolicy, Installation, Output, - PostDeletePackagePoliciesResponse, PackageInfo, } from '../../common/types'; import { @@ -681,25 +680,15 @@ class AgentPolicyService { ); } - await packagePolicyService.runDeleteExternalCallbacks(packagePolicies); - - const deletedPackagePolicies: PostDeletePackagePoliciesResponse = - await packagePolicyService.delete( - soClient, - esClient, - packagePolicies.map((p) => p.id), - { - force: options?.force, - skipUnassignFromAgentPolicies: true, - } - ); - try { - await packagePolicyService.runPostDeleteExternalCallbacks(deletedPackagePolicies); - } catch (error) { - const logger = appContextService.getLogger(); - logger.error(`An error occurred executing external callback: ${error}`); - logger.error(error); - } + await packagePolicyService.delete( + soClient, + esClient, + packagePolicies.map((p) => p.id), + { + force: options?.force, + skipUnassignFromAgentPolicies: true, + } + ); } if (agentPolicy.is_preconfigured && !options?.force) { diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 079c7fe3e05991..343b6129d0a1ae 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -2132,10 +2132,10 @@ describe('Package policy service', () => { { id: 'a', success: true }, { id: 'a', success: true }, ]; - callbackOne = jest.fn(async (deletedPolicies) => { + callbackOne = jest.fn(async (deletedPolicies, soClient, esClient) => { callingOrder.push('one'); }); - callbackTwo = jest.fn(async (deletedPolicies) => { + callbackTwo = jest.fn(async (deletedPolicies, soClient, esClient) => { callingOrder.push('two'); }); appContextService.addExternalCallback('packagePolicyPostDelete', callbackOne); @@ -2147,25 +2147,54 @@ describe('Package policy service', () => { }); it('should execute external callbacks', async () => { - await packagePolicyService.runPostDeleteExternalCallbacks(deletedPackagePolicies); + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - expect(callbackOne).toHaveBeenCalledWith(deletedPackagePolicies); - expect(callbackTwo).toHaveBeenCalledWith(deletedPackagePolicies); + await packagePolicyService.runPostDeleteExternalCallbacks( + deletedPackagePolicies, + soClient, + esClient + ); + + expect(callbackOne).toHaveBeenCalledWith( + deletedPackagePolicies, + expect.any(Object), + expect.any(Object), + undefined, + undefined + ); + expect(callbackTwo).toHaveBeenCalledWith( + deletedPackagePolicies, + expect.any(Object), + expect.any(Object), + undefined, + undefined + ); expect(callingOrder).toEqual(['one', 'two']); }); it("should execute all external callbacks even if one throw's", async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + callbackOne.mockImplementation(async (deletedPolicies) => { callingOrder.push('one'); throw new Error('foo'); }); await expect( - packagePolicyService.runPostDeleteExternalCallbacks(deletedPackagePolicies) + packagePolicyService.runPostDeleteExternalCallbacks( + deletedPackagePolicies, + soClient, + esClient + ) ).rejects.toThrow(FleetError); expect(callingOrder).toEqual(['one', 'two']); }); it('should provide an array of errors encountered by running external callbacks', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + let error: FleetError; const callbackOneError = new Error('foo 1'); const callbackTwoError = new Error('foo 2'); @@ -2180,7 +2209,7 @@ describe('Package policy service', () => { }); await packagePolicyService - .runPostDeleteExternalCallbacks(deletedPackagePolicies) + .runPostDeleteExternalCallbacks(deletedPackagePolicies, soClient, esClient) .catch((e) => { error = e; }); @@ -2203,10 +2232,10 @@ describe('Package policy service', () => { appContextService.start(createAppContextStartContractMock()); callingOrder = []; packagePolicies = [{ id: 'a' }, { id: 'a' }] as DeletePackagePoliciesResponse; - callbackOne = jest.fn(async (deletedPolicies) => { + callbackOne = jest.fn(async (deletedPolicies, soClient, esClient) => { callingOrder.push('one'); }); - callbackTwo = jest.fn(async (deletedPolicies) => { + callbackTwo = jest.fn(async (deletedPolicies, soClient, esClient) => { callingOrder.push('two'); }); appContextService.addExternalCallback('packagePolicyDelete', callbackOne); @@ -2218,25 +2247,31 @@ describe('Package policy service', () => { }); it('should execute external callbacks', async () => { - await packagePolicyService.runDeleteExternalCallbacks(packagePolicies); + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await packagePolicyService.runDeleteExternalCallbacks(packagePolicies, soClient, esClient); - expect(callbackOne).toHaveBeenCalledWith(packagePolicies); - expect(callbackTwo).toHaveBeenCalledWith(packagePolicies); + expect(callbackOne).toHaveBeenCalledWith(packagePolicies, soClient, esClient); + expect(callbackTwo).toHaveBeenCalledWith(packagePolicies, soClient, esClient); expect(callingOrder).toEqual(['one', 'two']); }); it("should execute all external callbacks even if one throw's", async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; callbackOne.mockImplementation(async (deletedPolicies) => { callingOrder.push('one'); throw new Error('foo'); }); await expect( - packagePolicyService.runDeleteExternalCallbacks(packagePolicies) + packagePolicyService.runDeleteExternalCallbacks(packagePolicies, soClient, esClient) ).rejects.toThrow(FleetError); expect(callingOrder).toEqual(['one', 'two']); }); it('should provide an array of errors encountered by running external callbacks', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; let error: FleetError; const callbackOneError = new Error('foo 1'); const callbackTwoError = new Error('foo 2'); @@ -2250,9 +2285,11 @@ describe('Package policy service', () => { throw callbackTwoError; }); - await packagePolicyService.runDeleteExternalCallbacks(packagePolicies).catch((e) => { - error = e; - }); + await packagePolicyService + .runDeleteExternalCallbacks(packagePolicies, soClient, esClient) + .catch((e) => { + error = e; + }); expect(error!.message).toEqual( '2 encountered while executing package delete external callbacks' @@ -2334,6 +2371,9 @@ describe('Package policy service', () => { }); it('should call external callbacks in expected order', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const callbackA: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('a'); return ds; @@ -2350,6 +2390,8 @@ describe('Package policy service', () => { await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', newPackagePolicy, + soClient, + esClient, coreMock.createCustomRequestHandlerContext(context), request ); @@ -2357,12 +2399,17 @@ describe('Package policy service', () => { }); it('should feed package policy returned by last callback', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + appContextService.addExternalCallback('packagePolicyCreate', callbackOne); appContextService.addExternalCallback('packagePolicyCreate', callbackTwo); await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', newPackagePolicy, + soClient, + esClient, coreMock.createCustomRequestHandlerContext(context), request ); @@ -2406,10 +2453,15 @@ describe('Package policy service', () => { }); it('should fail to execute remaining callbacks after a callback exception', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + try { await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', newPackagePolicy, + soClient, + esClient, coreMock.createCustomRequestHandlerContext(context), request ); @@ -2425,10 +2477,14 @@ describe('Package policy service', () => { }); it('should fail to return the package policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; expect( packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', newPackagePolicy, + soClient, + esClient, coreMock.createCustomRequestHandlerContext(context), request ) @@ -2504,6 +2560,9 @@ describe('Package policy service', () => { }); it('should execute PostPackagePolicyPostCreateCallback external callbacks', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const callbackA: PostPackagePolicyPostCreateCallback = jest.fn(async (ds) => { callbackCallingOrder.push('a'); return ds; @@ -2521,12 +2580,26 @@ describe('Package policy service', () => { await packagePolicyService.runExternalCallbacks( 'packagePolicyPostCreate', packagePolicy, + soClient, + esClient, requestContext, request ); - expect(callbackA).toHaveBeenCalledWith(packagePolicy, requestContext, request); - expect(callbackB).toHaveBeenCalledWith(packagePolicy, requestContext, request); + expect(callbackA).toHaveBeenCalledWith( + packagePolicy, + soClient, + esClient, + requestContext, + request + ); + expect(callbackB).toHaveBeenCalledWith( + packagePolicy, + soClient, + esClient, + requestContext, + request + ); expect(callbackCallingOrder).toEqual(['a', 'b']); }); }); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 5c4071fec906b1..c665d9a6f6f97a 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -13,9 +13,9 @@ import { getFlattenedObject } from '@kbn/std'; import type { KibanaRequest, ElasticsearchClient, - RequestHandlerContext, SavedObjectsClientContract, Logger, + RequestHandlerContext, } from '@kbn/core/server'; import { v4 as uuidv4 } from 'uuid'; import { safeLoad } from 'js-yaml'; @@ -133,31 +133,47 @@ class PackagePolicyClientImpl implements PackagePolicyClient { skipUniqueNameVerification?: boolean; overwrite?: boolean; packageInfo?: PackageInfo; - } + }, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise { const logger = appContextService.getLogger(); - const agentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id, true); - if (agentPolicy && packagePolicy.package?.name === FLEET_APM_PACKAGE) { + const enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks( + 'packagePolicyCreate', + packagePolicy, + soClient, + esClient, + context, + request + ); + + const agentPolicy = await agentPolicyService.get( + soClient, + enrichedPackagePolicy.policy_id, + true + ); + + if (agentPolicy && enrichedPackagePolicy.package?.name === FLEET_APM_PACKAGE) { const dataOutput = await getDataOutputForAgentPolicy(soClient, agentPolicy); if (dataOutput.type === outputType.Logstash) { throw new FleetError('You cannot add APM to a policy using a logstash output'); } } - await validateIsNotHostedPolicy(soClient, packagePolicy.policy_id, options?.force); + await validateIsNotHostedPolicy(soClient, enrichedPackagePolicy.policy_id, options?.force); // trailing whitespace causes issues creating API keys - packagePolicy.name = packagePolicy.name.trim(); + enrichedPackagePolicy.name = enrichedPackagePolicy.name.trim(); if (!options?.skipUniqueNameVerification) { const existingPoliciesWithName = await this.list(soClient, { perPage: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: "${packagePolicy.name}"`, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: "${enrichedPackagePolicy.name}"`, }); // Check that the name does not exist already if (existingPoliciesWithName.items.length > 0) { throw new FleetError( - `An integration policy with the name ${packagePolicy.name} already exists. Please rename it or choose a different name.` + `An integration policy with the name ${enrichedPackagePolicy.name} already exists. Please rename it or choose a different name.` ); } } @@ -165,32 +181,36 @@ class PackagePolicyClientImpl implements PackagePolicyClient { let elasticsearchPrivileges: NonNullable['privileges']; // Add ids to stream const packagePolicyId = options?.id || uuidv4(); - let inputs: PackagePolicyInput[] = packagePolicy.inputs.map((input) => + let inputs: PackagePolicyInput[] = enrichedPackagePolicy.inputs.map((input) => assignStreamIdToInput(packagePolicyId, input) ); // Make sure the associated package is installed - if (packagePolicy.package?.name) { + if (enrichedPackagePolicy.package?.name) { if (!options?.skipEnsureInstalled) { await ensureInstalledPackage({ esClient, spaceId: options?.spaceId || DEFAULT_SPACE_ID, savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - pkgVersion: packagePolicy.package.version, + pkgName: enrichedPackagePolicy.package.name, + pkgVersion: enrichedPackagePolicy.package.version, force: options?.force, }); } // Handle component template/mappings updates for experimental features, e.g. synthetic source - await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + await handleExperimentalDatastreamFeatureOptIn({ + soClient, + esClient, + packagePolicy: enrichedPackagePolicy, + }); const pkgInfo = options?.packageInfo ?? (await getPackageInfo({ savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - pkgVersion: packagePolicy.package.version, + pkgName: enrichedPackagePolicy.package.name, + pkgVersion: enrichedPackagePolicy.package.version, prerelease: true, })); @@ -203,9 +223,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ); } } - validatePackagePolicyOrThrow(packagePolicy, pkgInfo); + validatePackagePolicyOrThrow(enrichedPackagePolicy, pkgInfo); - inputs = await _compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs); + inputs = await _compilePackagePolicyInputs(pkgInfo, enrichedPackagePolicy.vars || {}, inputs); elasticsearchPrivileges = pkgInfo.elasticsearch?.privileges; @@ -214,7 +234,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { soClient, esClient, pkgInfo, - packagePolicy, + packagePolicy: enrichedPackagePolicy, force: !!options?.force, logger, }); @@ -225,9 +245,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { const newSo = await soClient.create( SAVED_OBJECT_TYPE, { - ...packagePolicy, - ...(packagePolicy.package - ? { package: omit(packagePolicy.package, 'experimental_data_stream_features') } + ...enrichedPackagePolicy, + ...(enrichedPackagePolicy.package + ? { package: omit(enrichedPackagePolicy.package, 'experimental_data_stream_features') } : {}), inputs, ...(elasticsearchPrivileges && { elasticsearch: { privileges: elasticsearchPrivileges } }), @@ -242,16 +262,18 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ); if (options?.bumpRevision ?? true) { - await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { + await agentPolicyService.bumpRevision(soClient, esClient, enrichedPackagePolicy.policy_id, { user: options?.user, }); } - return { - id: newSo.id, - version: newSo.version, - ...newSo.attributes, - }; + const createdPackagePolicy = { id: newSo.id, version: newSo.version, ...newSo.attributes }; + return packagePolicyService.runExternalCallbacks( + 'packagePolicyPostCreate', + createdPackagePolicy, + soClient, + esClient + ); } public async bulkCreate( @@ -494,7 +516,23 @@ class PackagePolicyClientImpl implements PackagePolicyClient { options?: { user?: AuthenticatedUser; force?: boolean; skipUniqueNameVerification?: boolean }, currentVersion?: string ): Promise { - const packagePolicy = { ...packagePolicyUpdate, name: packagePolicyUpdate.name.trim() }; + let enrichedPackagePolicy: UpdatePackagePolicy; + + try { + enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks( + 'packagePolicyUpdate', + packagePolicyUpdate, + soClient, + esClient + ); + } catch (error) { + const logger = appContextService.getLogger(); + logger.error(`An error occurred executing "packagePolicyUpdate" callback: ${error}`); + logger.error(error); + enrichedPackagePolicy = packagePolicyUpdate; + } + + const packagePolicy = { ...enrichedPackagePolicy, name: enrichedPackagePolicy.name.trim() }; const oldPackagePolicy = await this.get(soClient, id); const { version, ...restOfPackagePolicy } = packagePolicy; @@ -714,15 +752,35 @@ class PackagePolicyClientImpl implements PackagePolicyClient { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, ids: string[], - options?: { user?: AuthenticatedUser; skipUnassignFromAgentPolicies?: boolean; force?: boolean } + options?: { + user?: AuthenticatedUser; + skipUnassignFromAgentPolicies?: boolean; + force?: boolean; + }, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise { const result: PostDeletePackagePoliciesResponse = []; + const logger = appContextService.getLogger(); const packagePolicies = await this.getByIDs(soClient, ids, { ignoreMissing: true }); if (!packagePolicies) { return []; } + try { + await packagePolicyService.runDeleteExternalCallbacks( + packagePolicies, + soClient, + esClient, + context, + request + ); + } catch (error) { + logger.error(`An error occurred executing "packagePolicyDelete" callback: ${error}`); + logger.error(error); + } + const uniqueAgentPolicyIds = [ ...new Set(packagePolicies.map((packagePolicy) => packagePolicy.policy_id)), ]; @@ -823,6 +881,19 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } } + try { + await packagePolicyService.runPostDeleteExternalCallbacks( + result, + soClient, + esClient, + context, + request + ); + } catch (error) { + logger.error(`An error occurred executing "packagePolicyPostDelete" callback: ${error}`); + logger.error(error); + } + return result; } @@ -1235,9 +1306,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ? PostDeletePackagePoliciesResponse : A extends 'packagePolicyPostCreate' ? PackagePolicy - : NewPackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest + : A extends 'packagePolicyCreate' + ? NewPackagePolicy + : never, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise< A extends 'packagePolicyDelete' ? void @@ -1245,7 +1320,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ? void : A extends 'packagePolicyPostCreate' ? PackagePolicy - : NewPackagePolicy + : A extends 'packagePolicyCreate' + ? NewPackagePolicy + : never >; public async runExternalCallbacks( externalCallbackType: ExternalCallback[0], @@ -1254,53 +1331,95 @@ class PackagePolicyClientImpl implements PackagePolicyClient { | NewPackagePolicy | PostDeletePackagePoliciesResponse | DeletePackagePoliciesResponse, - context: RequestHandlerContext, - request: KibanaRequest + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise { - if (externalCallbackType === 'packagePolicyPostDelete') { - return await this.runPostDeleteExternalCallbacks( - packagePolicy as PostDeletePackagePoliciesResponse - ); - } else if (externalCallbackType === 'packagePolicyDelete') { - return await this.runDeleteExternalCallbacks(packagePolicy as DeletePackagePoliciesResponse); - } else { - if (!Array.isArray(packagePolicy)) { - let newData = packagePolicy; - const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); - if (externalCallbacks && externalCallbacks.size > 0) { - let updatedNewData = newData; - for (const callback of externalCallbacks) { - let result; - if (externalCallbackType === 'packagePolicyPostCreate') { - result = await (callback as PostPackagePolicyPostCreateCallback)( - updatedNewData as PackagePolicy, - context, - request - ); - updatedNewData = PackagePolicySchema.validate(result); - } else { - result = await (callback as PostPackagePolicyCreateCallback)( - updatedNewData as NewPackagePolicy, - context, - request - ); - } - if (externalCallbackType === 'packagePolicyCreate') { - updatedNewData = NewPackagePolicySchema.validate(result); - } else if (externalCallbackType === 'packagePolicyUpdate') { - updatedNewData = UpdatePackagePolicySchema.validate(result); + const logger = appContextService.getLogger(); + const numberOfCallbacks = appContextService.getExternalCallbacks(externalCallbackType)?.size; + logger.debug(`Running ${numberOfCallbacks} external callbacks for ${externalCallbackType}`); + try { + if (externalCallbackType === 'packagePolicyPostDelete') { + return await this.runPostDeleteExternalCallbacks( + packagePolicy as PostDeletePackagePoliciesResponse, + soClient, + esClient, + context, + request + ); + } else if (externalCallbackType === 'packagePolicyDelete') { + return await this.runDeleteExternalCallbacks( + packagePolicy as DeletePackagePoliciesResponse, + soClient, + esClient + ); + } else { + if (!Array.isArray(packagePolicy)) { + let newData = packagePolicy; + const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); + if (externalCallbacks && externalCallbacks.size > 0) { + let updatedNewData = newData; + for (const callback of externalCallbacks) { + let result; + if (externalCallbackType === 'packagePolicyPostCreate') { + result = await (callback as PostPackagePolicyPostCreateCallback)( + updatedNewData as PackagePolicy, + soClient, + esClient, + context, + request + ); + updatedNewData = PackagePolicySchema.validate(result); + } else { + result = await (callback as PostPackagePolicyCreateCallback)( + updatedNewData as NewPackagePolicy, + soClient, + esClient, + context, + request + ); + } + + if (externalCallbackType === 'packagePolicyCreate') { + updatedNewData = NewPackagePolicySchema.validate(result); + } else if (externalCallbackType === 'packagePolicyUpdate') { + const omitted = { + ...omit(result, [ + 'id', + 'version', + 'revision', + 'updated_at', + 'updated_by', + 'created_at', + 'created_by', + 'elasticsearch', + ]), + inputs: result.inputs.map((input) => omit(input, ['compiled_input'])), + }; + + updatedNewData = UpdatePackagePolicySchema.validate(omitted); + } } - } - newData = updatedNewData; + newData = updatedNewData; + } + return newData; } - return newData; } + } catch (error) { + logger.error(`Error running external callbacks for ${externalCallbackType}:`); + logger.error(error); + throw error; } } public async runPostDeleteExternalCallbacks( - deletedPackagePolicies: PostDeletePackagePoliciesResponse + deletedPackagePolicies: PostDeletePackagePoliciesResponse, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise { const externalCallbacks = appContextService.getExternalCallbacks('packagePolicyPostDelete'); const errorsThrown: Error[] = []; @@ -1310,7 +1429,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { // Failures from an external callback should not prevent other external callbacks from being // executed. Errors (if any) will be collected and `throw`n after processing the entire set try { - await callback(deletedPackagePolicies); + await callback(deletedPackagePolicies, soClient, esClient, context, request); } catch (error) { errorsThrown.push(error); } @@ -1326,7 +1445,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } public async runDeleteExternalCallbacks( - deletedPackagePolices: DeletePackagePoliciesResponse + deletedPackagePolices: DeletePackagePoliciesResponse, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient ): Promise { const externalCallbacks = appContextService.getExternalCallbacks('packagePolicyDelete'); const errorsThrown: Error[] = []; @@ -1336,7 +1457,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { // Failures from an external callback should not prevent other external callbacks from being // executed. Errors (if any) will be collected and `throw`n after processing the entire set try { - await callback(deletedPackagePolices); + await callback(deletedPackagePolices, soClient, esClient); } catch (error) { errorsThrown.push(error); } @@ -1402,7 +1523,9 @@ class PackagePolicyClientWithAuthz extends PackagePolicyClientImpl { skipUniqueNameVerification?: boolean; overwrite?: boolean; packageInfo?: PackageInfo; - } + }, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise { await this.#runPreflight({ fleetAuthz: { @@ -1410,7 +1533,7 @@ class PackagePolicyClientWithAuthz extends PackagePolicyClientImpl { }, }); - return super.create(soClient, esClient, packagePolicy, options); + return super.create(soClient, esClient, packagePolicy, options, context, request); } } diff --git a/x-pack/plugins/fleet/server/services/package_policy_service.ts b/x-pack/plugins/fleet/server/services/package_policy_service.ts index 790622a6ae6b4c..5ca2a107c02813 100644 --- a/x-pack/plugins/fleet/server/services/package_policy_service.ts +++ b/x-pack/plugins/fleet/server/services/package_policy_service.ts @@ -5,12 +5,8 @@ * 2.0. */ -import type { KibanaRequest, Logger } from '@kbn/core/server'; -import type { - ElasticsearchClient, - RequestHandlerContext, - SavedObjectsClientContract, -} from '@kbn/core/server'; +import type { KibanaRequest, Logger, RequestHandlerContext } from '@kbn/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type { AuthenticatedUser } from '@kbn/security-plugin/server'; import type { @@ -50,7 +46,9 @@ export interface PackagePolicyClient { skipUniqueNameVerification?: boolean; overwrite?: boolean; packageInfo?: PackageInfo; - } + }, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise; bulkCreate( @@ -108,7 +106,13 @@ export interface PackagePolicyClient { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, ids: string[], - options?: { user?: AuthenticatedUser; skipUnassignFromAgentPolicies?: boolean; force?: boolean } + options?: { + user?: AuthenticatedUser; + skipUnassignFromAgentPolicies?: boolean; + force?: boolean; + }, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise; upgrade( @@ -146,9 +150,13 @@ export interface PackagePolicyClient { ? PostDeletePackagePoliciesResponse : A extends 'packagePolicyPostCreate' ? PackagePolicy + : A extends 'packagePolicyUpdate' + ? UpdatePackagePolicy : NewPackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise< A extends 'packagePolicyDelete' ? void @@ -156,13 +164,25 @@ export interface PackagePolicyClient { ? void : A extends 'packagePolicyPostCreate' ? PackagePolicy + : A extends 'packagePolicyUpdate' + ? UpdatePackagePolicy : NewPackagePolicy >; - runDeleteExternalCallbacks(deletedPackagePolicies: DeletePackagePoliciesResponse): Promise; + runDeleteExternalCallbacks( + deletedPackagePolicies: DeletePackagePoliciesResponse, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest + ): Promise; runPostDeleteExternalCallbacks( - deletedPackagePolicies: PostDeletePackagePoliciesResponse + deletedPackagePolicies: PostDeletePackagePoliciesResponse, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ): Promise; getUpgradePackagePolicyInfo( diff --git a/x-pack/plugins/fleet/server/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts index 6d3ebc32b523f3..ca5a0d84c958e4 100644 --- a/x-pack/plugins/fleet/server/types/extensions.ts +++ b/x-pack/plugins/fleet/server/types/extensions.ts @@ -6,6 +6,7 @@ */ import type { KibanaRequest, RequestHandlerContext } from '@kbn/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type { DeepReadonly } from 'utility-types'; @@ -18,29 +19,43 @@ import type { } from '../../common/types'; export type PostPackagePolicyDeleteCallback = ( - packagePolicies: DeletePackagePoliciesResponse + packagePolicies: DeletePackagePoliciesResponse, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ) => Promise; export type PostPackagePolicyPostDeleteCallback = ( - deletedPackagePolicies: DeepReadonly + deletedPackagePolicies: DeepReadonly, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ) => Promise; export type PostPackagePolicyCreateCallback = ( newPackagePolicy: NewPackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ) => Promise; export type PostPackagePolicyPostCreateCallback = ( packagePolicy: PackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ) => Promise; export type PutPackagePolicyUpdateCallback = ( updatePackagePolicy: UpdatePackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + context?: RequestHandlerContext, + request?: KibanaRequest ) => Promise; export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback]; 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 e8bccd2aeacf72..f17c9595a67864 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 @@ -7,7 +7,12 @@ import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { + elasticsearchServiceMock, + httpServerMock, + loggingSystemMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; import { createNewPackagePolicyMock, deletePackagePolicyMock, @@ -84,6 +89,9 @@ describe('ingest_integration tests ', () => { }); describe('package policy init callback (atifacts manifest initialisation tests)', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const createNewEndpointPolicyInput = (manifest: ManifestSchema) => ({ type: 'endpoint', enabled: true, @@ -106,7 +114,13 @@ describe('ingest_integration tests ', () => { exceptionListClient ); - return callback(createNewPackagePolicyMock(), requestContextMock.convertContext(ctx), req); + return callback( + createNewPackagePolicyMock(), + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); }; const TEST_POLICY_ID_1 = 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'; @@ -258,6 +272,8 @@ describe('ingest_integration tests ', () => { }); describe('package policy post create callback', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyPostCreateCallback(logger, exceptionListClient); const policyConfig = generator.generatePolicyPackagePolicy() as PackagePolicy; @@ -275,6 +291,8 @@ describe('ingest_integration tests ', () => { }; const postCreatedPolicyConfig = await callback( policyConfig, + soClient, + esClient, requestContextMock.convertContext(ctx), req ); @@ -312,6 +330,8 @@ describe('ingest_integration tests ', () => { }; const postCreatedPolicyConfig = await callback( policyConfig, + soClient, + esClient, requestContextMock.convertContext(ctx), req ); @@ -326,6 +346,9 @@ describe('ingest_integration tests ', () => { }); }); describe('package policy update callback (when the license is below platinum)', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + beforeEach(() => { licenseEmitter.next(Gold); // set license level to gold }); @@ -341,7 +364,7 @@ describe('ingest_integration tests ', () => { const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; await expect(() => - callback(policyConfig, requestContextMock.convertContext(ctx), req) + callback(policyConfig, soClient, esClient, requestContextMock.convertContext(ctx), req) ).rejects.toThrow('Requires Platinum license'); }); it('updates successfully if no paid features are turned on in the policy', async () => { @@ -358,6 +381,8 @@ describe('ingest_integration tests ', () => { policyConfig.inputs[0]!.config!.policy.value = mockPolicy; const updatedPolicyConfig = await callback( policyConfig, + soClient, + esClient, requestContextMock.convertContext(ctx), req ); @@ -366,6 +391,9 @@ describe('ingest_integration tests ', () => { }); describe('package policy update callback (when the license is at least platinum)', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + beforeEach(() => { licenseEmitter.next(Platinum); // set license level to platinum }); @@ -383,6 +411,8 @@ describe('ingest_integration tests ', () => { policyConfig.inputs[0]!.config!.policy.value = mockPolicy; const updatedPolicyConfig = await callback( policyConfig, + soClient, + esClient, requestContextMock.convertContext(ctx), req ); @@ -391,9 +421,12 @@ describe('ingest_integration tests ', () => { }); describe('package policy delete callback', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const invokeDeleteCallback = async (): Promise => { const callback = getPackagePolicyDeleteCallback(exceptionListClient); - await callback(deletePackagePolicyMock()); + await callback(deletePackagePolicyMock(), soClient, esClient); }; let removedPolicies: PostDeletePackagePoliciesResponse; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index a2fe485921e6d8..a941b8f8aa4cab 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { KibanaRequest, Logger, RequestHandlerContext } from '@kbn/core/server'; +import type { Logger } from '@kbn/core/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { PluginStartContract as AlertsStartContract } from '@kbn/alerting-plugin/server'; import type { @@ -55,10 +55,18 @@ export const getPackagePolicyCreateCallback = ( exceptionsClient: ExceptionListClient | undefined ): PostPackagePolicyCreateCallback => { return async ( - newPackagePolicy: NewPackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest + newPackagePolicy, + soClient, + esClient, + context, + request ): Promise => { + // callback is called outside request context + if (!context || !request) { + logger.debug('PackagePolicyCreateCallback called outside request context. Skipping...'); + return newPackagePolicy; + } + // We only care about Endpoint package policies if (!isEndpointPackagePolicy(newPackagePolicy)) { return newPackagePolicy; @@ -140,11 +148,7 @@ export const getPackagePolicyUpdateCallback = ( featureUsageService: FeatureUsageService, endpointMetadataService: EndpointMetadataService ): PutPackagePolicyUpdateCallback => { - return async ( - newPackagePolicy: NewPackagePolicy - // context: RequestHandlerContext, - // request: KibanaRequest - ): Promise => { + return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { return newPackagePolicy; } @@ -174,7 +178,7 @@ export const getPackagePolicyPostCreateCallback = ( return packagePolicy; } - const integrationConfig = packagePolicy?.inputs[0].config?.integration_config; + const integrationConfig = packagePolicy?.inputs[0]?.config?.integration_config; if (integrationConfig && integrationConfig?.value?.eventFilters !== undefined) { createEventFilters( From 0d6c113ab1b98574bd780586ead12ea76b417138 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 1 Feb 2023 07:36:59 -0700 Subject: [PATCH 46/59] Add context.originalAlertState to the Metric Threshold and Inventory Threshold recovery context (#147928) ## Summary This PR adds the `ALERT_ACTION_GROUP` to the Alerts-As-Data documents for both the Metric Threshold and Inventory Threshold rules. It then uses that value from the alert document in the recovery context to set `context.originalAlertState`. This also adds `context.originalStateWasALERT`, `context.originalStateWasWARNING`, and `context.originalStateWasNO_DATA` (Metric Threshold Only) to allow for conditional Mustache templates. I also fixed the types for `getAlertByAlertUuid()` to be more accurate. #### Metric Threshold Example ``` {{#context.originalAlertStateWasALERT}} This is a recovery for an ALERT {{/context.originalAlertStateWasALERT}} {{#context.originalAlertStateWasWARNING}} This is a recovery for a WARNING {{/context.originalAlertStateWasWARNING}} {{#context.originalAlertStateWasNO_DATA}} This is a recovery for NO_DATA {{/context.originalAlertStateWasNO_DATA}} ``` #### Inventory Threshold Example ``` {{#context.originalAlertStateWasALERT}} This is a recovery for an ALERT {{/context.originalAlertStateWasALERT}} {{#context.originalAlertStateWasWARNING}} This is a recovery for a WARNING {{/context.originalAlertStateWasWARNING}} ``` Fixes #145418 ### How to test 1. Start Kibana and ingest some data (Metricbeat or whatever) 2. Create a rule (one for each), for the Metric Threshold rule you will need to group by something like `host.name` 3. Set the conditions to something you can trigger, I used `NO_DATA` 4. Add a server log action for the recovery action group with `{{context}}`, alternatively you can use the examples above to see the Mustache logic work 5. Save the rules 6. Stop ingesting data and allow the rule to trigger a `NO DATA` alert 7. Start ingesting data so that it recovers 8. Observe the log message with `originalAlertState` as `NO DATA` for Metric Threshold and `ALERT` for Inventory Threshold. --- .../server/lib/alerting/common/messages.ts | 16 +++++ .../infra/server/lib/alerting/common/utils.ts | 2 +- .../inventory_metric_threshold_executor.ts | 40 ++++++++++--- ...er_inventory_metric_threshold_rule_type.ts | 11 ++++ .../metric_threshold_executor.ts | 59 +++++++++++++++---- .../register_metric_threshold_rule_type.ts | 15 +++++ .../server/utils/get_original_action_group.ts | 15 +++++ .../server/utils/create_lifecycle_executor.ts | 2 +- .../utils/lifecycle_alert_services.mock.ts | 2 +- 9 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/infra/server/utils/get_original_action_group.ts diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts index d35651d66c95ac..5e8d8e4e1ee95e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts @@ -271,3 +271,19 @@ export const tagsActionVariableDescription = i18n.translate( defaultMessage: 'List of tags associated with the entity where this alert triggered.', } ); + +export const originalAlertStateActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.originalAlertStateActionVariableDescription', + { + defaultMessage: + 'The state of the alert before it recovered. This is only available in the recovery context', + } +); + +export const originalAlertStateWasActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.originalAlertStateWasWARNINGActionVariableDescription', + { + defaultMessage: + 'Boolean value of the state of the alert before it recovered. This can be used for template conditions. This is only available in the recovery context', + } +); diff --git a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts index 970d3779a2e259..b6c0b5578c6a0b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts @@ -237,7 +237,7 @@ export const flattenAdditionalContext = ( }; export const getContextForRecoveredAlerts = ( - alertHits: AdditionalContext | undefined | null + alertHits: AdditionalContext[] | undefined | null ): AdditionalContext => { const alertHitsSource = alertHits && alertHits.length > 0 ? unflattenObject(alertHits[0]._source) : undefined; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index b6e0c5dd578e55..ffd0a7e5633393 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_ACTION_GROUP } from '@kbn/rule-data-utils'; import { first, get } from 'lodash'; import { ActionGroup, @@ -15,6 +15,7 @@ import { AlertInstanceState as AlertState, } from '@kbn/alerting-plugin/common'; import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; +import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; @@ -45,6 +46,11 @@ type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; +export const FIRED_ACTIONS_ID = 'metrics.inventory_threshold.fired'; +export const WARNING_ACTIONS_ID = 'metrics.inventory_threshold.warning'; + +type InventoryThrehsoldActionGroup = typeof FIRED_ACTIONS_ID | typeof WARNING_ACTIONS_ID; + export type InventoryMetricThresholdRuleTypeState = RuleTypeState; // no specific state used export type InventoryMetricThresholdAlertState = AlertState; // no specific state used export type InventoryMetricThresholdAlertContext = AlertContext; // no specific instance context used @@ -57,6 +63,7 @@ type InventoryMetricThresholdAlert = Alert< type InventoryMetricThresholdAlertFactory = ( id: string, reason: string, + actionGroup: InventoryThrehsoldActionGroup, additionalContext?: AdditionalContext | null, threshold?: number | undefined, value?: number | undefined @@ -90,11 +97,17 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = getAlertUuid, getAlertByAlertUuid, } = services; - const alertFactory: InventoryMetricThresholdAlertFactory = (id, reason, additionalContext) => + const alertFactory: InventoryMetricThresholdAlertFactory = ( + id, + reason, + actionGroup, + additionalContext + ) => alertWithLifecycle({ id, fields: { [ALERT_REASON]: reason, + [ALERT_ACTION_GROUP]: actionGroup, ...flattenAdditionalContext(additionalContext), }, }); @@ -107,7 +120,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = logger.error(e.message); const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able const reason = buildInvalidQueryAlertReason(params.filterQueryText); - const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason); + const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason, actionGroupId); const indexedStartedDate = getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString(); const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY); @@ -212,11 +225,11 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.WARNING ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; + nextState === AlertStates.WARNING ? WARNING_ACTIONS_ID : FIRED_ACTIONS_ID; const additionalContext = results && results.length > 0 ? results[0][group].context : null; - const alert = alertFactory(group, reason, additionalContext); + const alert = alertFactory(group, reason, actionGroupId, additionalContext); const indexedStartedDate = getAlertStartedDate(group) ?? startedAt.toISOString(); const alertUuid = getAlertUuid(group); @@ -255,6 +268,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = const alertUuid = getAlertUuid(recoveredAlertId); const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined; const additionalContext = getContextForRecoveredAlerts(alertHits); + const originalActionGroup = getOriginalActionGroup(alertHits); alert.setContext({ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid), @@ -270,6 +284,9 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = timestamp: indexedStartedDate, spaceId, }), + originalAlertState: translateActionGroupToAlertState(originalActionGroup), + originalAlertStateWasALERT: originalActionGroup === FIRED_ACTIONS_ID, + originalAlertStateWasWARNING: originalActionGroup === WARNING_ACTIONS_ID, ...additionalContext, }); } @@ -335,14 +352,12 @@ const mapToConditionsLookup = ( {} ); -export const FIRED_ACTIONS_ID = 'metrics.inventory_threshold.fired'; export const FIRED_ACTIONS: ActionGroup = { id: FIRED_ACTIONS_ID, name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { defaultMessage: 'Alert', }), }; -export const WARNING_ACTIONS_ID = 'metrics.inventory_threshold.warning'; export const WARNING_ACTIONS = { id: WARNING_ACTIONS_ID, name: i18n.translate('xpack.infra.metrics.alerting.threshold.warning', { @@ -350,6 +365,17 @@ export const WARNING_ACTIONS = { }), }; +const translateActionGroupToAlertState = ( + actionGroupId: string | undefined +): string | undefined => { + if (actionGroupId === FIRED_ACTIONS.id) { + return stateToAlertMessage[AlertStates.ALERT]; + } + if (actionGroupId === WARNING_ACTIONS.id) { + return stateToAlertMessage[AlertStates.WARNING]; + } +}; + const formatMetric = (metric: SnapshotMetricType, value: number) => { const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count); if (isNaN(value)) { diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts index a5ba2c32ada6e4..b0b8a13562febd 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts @@ -33,6 +33,8 @@ import { labelsActionVariableDescription, metricActionVariableDescription, orchestratorActionVariableDescription, + originalAlertStateActionVariableDescription, + originalAlertStateWasActionVariableDescription, reasonActionVariableDescription, tagsActionVariableDescription, thresholdActionVariableDescription, @@ -124,6 +126,15 @@ export async function registerMetricInventoryThresholdRuleType( { name: 'orchestrator', description: orchestratorActionVariableDescription }, { name: 'labels', description: labelsActionVariableDescription }, { name: 'tags', description: tagsActionVariableDescription }, + { name: 'originalAlertState', description: originalAlertStateActionVariableDescription }, + { + name: 'originalAlertStateWasALERT', + description: originalAlertStateWasActionVariableDescription, + }, + { + name: 'originalAlertStateWasWARNING', + description: originalAlertStateWasActionVariableDescription, + }, ], }, getSummarizedAlerts: libs.metricsRules.createGetSummarizedAlerts(), diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 07af8bbcafe22e..cd0b0f48f36d49 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_ACTION_GROUP, ALERT_REASON } from '@kbn/rule-data-utils'; import { isEqual } from 'lodash'; import { ActionGroupIdsOf, @@ -16,6 +16,7 @@ import { } from '@kbn/alerting-plugin/common'; import { Alert, RuleTypeState } from '@kbn/alerting-plugin/server'; import { TimeUnitChar } from '@kbn/observability-plugin/common/utils/formatters/duration'; +import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, Comparator } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { InfraBackendLibs } from '../../infra_types'; @@ -53,8 +54,18 @@ export type MetricThresholdRuleTypeState = RuleTypeState & { export type MetricThresholdAlertState = AlertState; // no specific instace state used export type MetricThresholdAlertContext = AlertContext; // no specific instace state used +export const FIRED_ACTIONS_ID = 'metrics.threshold.fired'; +export const WARNING_ACTIONS_ID = 'metrics.threshold.warning'; +export const NO_DATA_ACTIONS_ID = 'metrics.threshold.nodata'; + +type MetricThresholdActionGroup = + | typeof FIRED_ACTIONS_ID + | typeof WARNING_ACTIONS_ID + | typeof NO_DATA_ACTIONS_ID + | typeof RecoveredActionGroup.id; + type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< - typeof FIRED_ACTIONS | typeof WARNING_ACTIONS + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS | typeof NO_DATA_ACTIONS >; type MetricThresholdAlert = Alert< @@ -66,6 +77,7 @@ type MetricThresholdAlert = Alert< type MetricThresholdAlertFactory = ( id: string, reason: string, + actionGroup: MetricThresholdActionGroup, additionalContext?: AdditionalContext | null, threshold?: number | undefined, value?: number | undefined @@ -101,11 +113,17 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const { alertWithLifecycle, savedObjectsClient, getAlertUuid, getAlertByAlertUuid } = services; - const alertFactory: MetricThresholdAlertFactory = (id, reason, additionalContext) => + const alertFactory: MetricThresholdAlertFactory = ( + id, + reason, + actionGroup, + additionalContext + ) => alertWithLifecycle({ id, fields: { [ALERT_REASON]: reason, + [ALERT_ACTION_GROUP]: actionGroup, ...flattenAdditionalContext(additionalContext), }, }); @@ -127,9 +145,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => } catch (e) { logger.error(e.message); const timestamp = startedAt.toISOString(); - const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able + const actionGroupId = FIRED_ACTIONS_ID; // Change this to an Error action group when able const reason = buildInvalidQueryAlertReason(params.filterQueryText); - const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason); + const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason, actionGroupId); const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY); alert.scheduleActions(actionGroupId, { @@ -258,14 +276,14 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => if (reason) { const timestamp = startedAt.toISOString(); - const actionGroupId = + const actionGroupId: MetricThresholdActionGroup = nextState === AlertStates.OK ? RecoveredActionGroup.id : nextState === AlertStates.NO_DATA - ? NO_DATA_ACTIONS.id + ? NO_DATA_ACTIONS_ID : nextState === AlertStates.WARNING - ? WARNING_ACTIONS.id - : FIRED_ACTIONS.id; + ? WARNING_ACTIONS_ID + : FIRED_ACTIONS_ID; const additionalContext = hasAdditionalContext(params.groupBy, validGroupByForContext) ? alertResults && alertResults.length > 0 @@ -273,7 +291,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => : null : null; - const alert = alertFactory(`${group}`, reason, additionalContext); + const alert = alertFactory(`${group}`, reason, actionGroupId, additionalContext); const alertUuid = getAlertUuid(group); scheduledActionsCount++; @@ -313,6 +331,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const alertHits = alertUuid ? await getAlertByAlertUuid(alertUuid) : undefined; const additionalContext = getContextForRecoveredAlerts(alertHits); + const originalActionGroup = getOriginalActionGroup(alertHits); alert.setContext({ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid), @@ -323,6 +342,12 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => timestamp: startedAt.toISOString(), threshold: mapToConditionsLookup(criteria, (c) => c.threshold), viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId), + + originalAlertState: translateActionGroupToAlertState(originalActionGroup), + originalAlertStateWasALERT: originalActionGroup === FIRED_ACTIONS.id, + originalAlertStateWasWARNING: originalActionGroup === WARNING_ACTIONS.id, + // eslint-disable-next-line @typescript-eslint/naming-convention + originalAlertStateWasNO_DATA: originalActionGroup === NO_DATA_ACTIONS.id, ...additionalContext, }); } @@ -360,6 +385,20 @@ export const NO_DATA_ACTIONS = { }), }; +const translateActionGroupToAlertState = ( + actionGroupId: string | undefined +): string | undefined => { + if (actionGroupId === FIRED_ACTIONS.id) { + return stateToAlertMessage[AlertStates.ALERT]; + } + if (actionGroupId === WARNING_ACTIONS.id) { + return stateToAlertMessage[AlertStates.WARNING]; + } + if (actionGroupId === NO_DATA_ACTIONS.id) { + return stateToAlertMessage[AlertStates.NO_DATA]; + } +}; + const mapToConditionsLookup = ( list: any[], mapFn: (value: any, index: number, array: any[]) => unknown diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts index 55e2379bcf19a6..82f8d1c441f732 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts @@ -23,6 +23,8 @@ import { labelsActionVariableDescription, metricActionVariableDescription, orchestratorActionVariableDescription, + originalAlertStateActionVariableDescription, + originalAlertStateWasActionVariableDescription, reasonActionVariableDescription, tagsActionVariableDescription, thresholdActionVariableDescription, @@ -124,6 +126,19 @@ export async function registerMetricThresholdRuleType( { name: 'orchestrator', description: orchestratorActionVariableDescription }, { name: 'labels', description: labelsActionVariableDescription }, { name: 'tags', description: tagsActionVariableDescription }, + { name: 'originalAlertState', description: originalAlertStateActionVariableDescription }, + { + name: 'originalAlertStateWasALERT', + description: originalAlertStateWasActionVariableDescription, + }, + { + name: 'originalAlertStateWasWARNING', + description: originalAlertStateWasActionVariableDescription, + }, + { + name: 'originalAlertStateWasNO_DATA', + description: originalAlertStateWasActionVariableDescription, + }, ], }, producer: 'infrastructure', diff --git a/x-pack/plugins/infra/server/utils/get_original_action_group.ts b/x-pack/plugins/infra/server/utils/get_original_action_group.ts new file mode 100644 index 00000000000000..8054935a03e10a --- /dev/null +++ b/x-pack/plugins/infra/server/utils/get_original_action_group.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 { ALERT_ACTION_GROUP } from '@kbn/rule-data-utils'; + +export const getOriginalActionGroup = ( + alertHits: Array<{ [id: string]: any }> | null | undefined +) => { + const source = alertHits && alertHits.length > 0 ? alertHits[0]._source : undefined; + return source?.[ALERT_ACTION_GROUP]; +}; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 7e16fcdd8cdf32..e3678e1455527f 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -75,7 +75,7 @@ export interface LifecycleAlertServices< alertWithLifecycle: LifecycleAlertService; getAlertStartedDate: (alertInstanceId: string) => string | null; getAlertUuid: (alertInstanceId: string) => string; - getAlertByAlertUuid: (alertUuid: string) => { [x: string]: any } | null; + getAlertByAlertUuid: (alertUuid: string) => Promise | null>; } export type LifecycleRuleExecutor< diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts index 721110db4d6af4..9324bcfd76cb48 100644 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts @@ -37,5 +37,5 @@ export const createLifecycleAlertServicesMock = < alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), getAlertStartedDate: jest.fn((id: string) => null), getAlertUuid: jest.fn((id: string) => 'mock-alert-uuid'), - getAlertByAlertUuid: jest.fn((id: string) => null), + getAlertByAlertUuid: jest.fn((id: string) => Promise.resolve(null)), }); From 9b85acb49e821f16b886c608787b285d813198b6 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:57:05 +0100 Subject: [PATCH 47/59] [Security Solution][Endpoint] Fix and unskip flaky test (#149841) > **Note** > **Merge after elastic/kibana/pull/149839** ## Summary Fixes flaky test elastic/kibana/issues/145204 flaky test runners - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1817 x 50 (failed on a single [unrelated ](https://github.com/ashokaditya/kibana/blob/92cb000a2f5116fc7408f52794cea06ad40de4bb/x-pack/test/security_solution_endpoint/apps/endpoint/artifact_entries_list.ts#L75)flaky test) - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1826 x 150 (failed on a single run for an [unrelated](https://github.com/ashokaditya/kibana/blob/92cb000a2f5116fc7408f52794cea06ad40de4bb/x-pack/test/security_solution_endpoint/apps/endpoint/artifact_entries_list.ts#L87) flaky test) - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1834 x 200 (successful on all runs) - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1847 x 100 (successful on all runs) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../management/pages/integration_tests/index.test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx index 95aed4e29ea3aa..d0cbd71e187c52 100644 --- a/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/integration_tests/index.test.tsx @@ -13,7 +13,6 @@ import type { AppContextTestRender } from '../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; import { useUserPrivileges } from '../../../common/components/user_privileges'; import { endpointPageHttpMock } from '../endpoint_hosts/mocks'; -import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__'; jest.mock('../../../common/components/user_privileges'); @@ -30,7 +29,7 @@ describe('when in the Administration tab', () => { }); afterEach(() => { - useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); + useUserPrivilegesMock.mockReset(); }); describe('when the user has no permissions', () => { @@ -97,8 +96,7 @@ describe('when in the Administration tab', () => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/145204 - describe.skip('when the user has permissions', () => { + describe('when the user has permissions', () => { it('should display the Management view if user has privileges', async () => { useUserPrivilegesMock.mockReturnValue({ endpointPrivileges: { loading: false, canReadEndpointList: true, canAccessFleet: true }, From 6e70bdb3479a34ec8c7f6f5ba93c3f607f5912ca Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 1 Feb 2023 07:59:50 -0700 Subject: [PATCH 48/59] Custom equation editor for Metric Threshold Rule (#148732) ## Summary This PR closes #145444 by adding a custom equation editor to the Metric Threshold rule. I also added support for custom metrics to the Metric Explorer API which powers the preview chart on the rule editor. Eventually we could do a follow up PR to the Metrics Explorer UI to expose this new functionality; which is outside the scope of this PR. ### Notable changes with this PR I changed the reason message for Metric Threshold rules which do not have a group by. The original message would say something like `system.cpu.user.pct is 82% in the last 1 min for all hosts. Alert when > 81%.` I removed the `for all hosts` portion because the Metric Threshold rule is not limited to just the concept of hosts, our users rely on this rule as their "Swiss Army Knife" rule for all types of data. I also had to change the format of the `currentPeriod` bucket for the Metric Threshold aggregation to support the "document count with KQL filter" use case. One of the requirements of a `filter` aggregation is that it must be a child of a multi-bucket aggregation. This is why I converted it from a 'filter' aggregation to a `filters` aggregation with an `all` key for the time range query. I added basic validation for the equations with a regular expression that just limits the characters to the allowable: `A-Z, +, -, /, *, (, ), ?, !, &, :, |, >, <, =`. I feel like for now this is good enough. If we want to expose some of the Painless `Math.*` libraries then we can follow up in a later release with a PegJS parser which would do some syntax validation as well. ### Rule with custom equation image ### Rule with custom ratio equation image ### Reason message with custom label ![image](https://user-images.githubusercontent.com/41702/211936062-4b696f0c-dfec-4e48-b89c-b0462fb5f7f0.png) --------- Co-authored-by: Carlos Crespo Co-authored-by: Maryam Saeidi --- .../infra/common/alerting/metrics/types.ts | 32 ++- .../infra/common/http_api/metrics_explorer.ts | 34 +++ .../custom_equation_editor.stories.tsx | 106 +++++++++ .../custom_equation_editor.tsx | 214 ++++++++++++++++++ .../components/custom_equation/index.tsx | 8 + .../custom_equation/metric_row_controls.tsx | 31 +++ .../custom_equation/metric_row_with_agg.tsx | 137 +++++++++++ .../custom_equation/metric_row_with_count.tsx | 100 ++++++++ .../components/custom_equation/types.ts | 33 +++ .../components/expression_chart.tsx | 18 +- .../components/expression_row.tsx | 84 +++++-- .../components/validation.test.ts | 33 +++ .../components/validation.tsx | 62 ++++- .../hooks/use_metrics_explorer_chart_data.ts | 41 +++- .../alerting/metric_threshold/i18n_strings.ts | 40 ++++ .../public/alerting/metric_threshold/types.ts | 9 +- .../lib/create_inventory_metric_formatter.ts | 4 +- .../components/aggregation.tsx | 11 +- .../helpers/create_formatter_for_metric.ts | 4 + .../hooks/use_metrics_explorer_data.ts | 9 +- .../server/lib/alerting/common/messages.ts | 11 +- .../lib/create_bucket_selector.ts | 12 +- .../lib/create_rate_aggregation.ts | 4 +- .../metric_threshold/lib/evaluate_rule.ts | 11 +- .../alerting/metric_threshold/lib/get_data.ts | 16 +- .../metric_threshold/lib/metric_query.ts | 9 +- .../metric_threshold/lib/wrap_in_period.ts | 14 +- .../metric_threshold_executor.test.ts | 25 +- .../register_metric_threshold_rule_type.ts | 34 ++- .../lib/create_custom_metrics_aggregations.ts | 79 +++++++ .../plugins/infra/server/lib/metrics/index.ts | 56 ++--- .../plugins/infra/server/lib/metrics/types.ts | 1 + .../convert_metric_to_metrics_api_metric.ts | 16 ++ .../apis/metrics_ui/metric_threshold_alert.ts | 70 +++++- .../apis/metrics_ui/metrics_explorer.ts | 46 ++++ 35 files changed, 1303 insertions(+), 111 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.stories.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/index.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_controls.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_agg.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_count.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/types.ts create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.test.ts create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/i18n_strings.ts create mode 100644 x-pack/plugins/infra/server/lib/create_custom_metrics_aggregations.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 60c01beacd158a..4b72b6389b49f9 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -33,6 +33,7 @@ export enum Aggregators { CARDINALITY = 'cardinality', P95 = 'p95', P99 = 'p99', + CUSTOM = 'custom', } export enum AlertStates { @@ -100,14 +101,43 @@ interface BaseMetricExpressionParams { export interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { aggType: Exclude; metric: string; + customMetrics: never; + equation: never; + label: never; } export interface CountMetricExpressionParams extends BaseMetricExpressionParams { aggType: Aggregators.COUNT; metric: never; + customMetrics: never; + equation: never; + label: never; } -export type MetricExpressionParams = NonCountMetricExpressionParams | CountMetricExpressionParams; +export type CustomMetricAggTypes = Exclude< + Aggregators, + Aggregators.CUSTOM | Aggregators.RATE | Aggregators.P95 | Aggregators.P99 +>; + +export interface MetricExpressionCustomMetric { + name: string; + aggType: CustomMetricAggTypes; + field?: string; + filter?: string; +} + +export interface CustomMetricExpressionParams extends BaseMetricExpressionParams { + aggType: Aggregators.CUSTOM; + metric: never; + customMetrics: MetricExpressionCustomMetric[]; + equation?: string; + label?: string; +} + +export type MetricExpressionParams = + | NonCountMetricExpressionParams + | CountMetricExpressionParams + | CustomMetricExpressionParams; export const QUERY_INVALID: unique symbol = Symbol('QUERY_INVALID'); diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts index de00d521126e36..d735e398e6661d 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { xor } from 'lodash'; export const METRIC_EXPLORER_AGGREGATIONS = [ 'avg', @@ -17,8 +18,11 @@ export const METRIC_EXPLORER_AGGREGATIONS = [ 'sum', 'p95', 'p99', + 'custom', ] as const; +export const OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS = ['custom', 'rate', 'p95', 'p99']; + type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number]; const metricsExplorerAggregationKeys = METRIC_EXPLORER_AGGREGATIONS.reduce< @@ -27,12 +31,42 @@ const metricsExplorerAggregationKeys = METRIC_EXPLORER_AGGREGATIONS.reduce< export const metricsExplorerAggregationRT = rt.keyof(metricsExplorerAggregationKeys); +export type MetricExplorerCustomMetricAggregations = Exclude< + MetricsExplorerAggregation, + 'custom' | 'rate' | 'p95' | 'p99' +>; +const metricsExplorerCustomMetricAggregationKeys = xor( + METRIC_EXPLORER_AGGREGATIONS, + OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS +).reduce>( + (acc, agg) => ({ ...acc, [agg]: null }), + {} as Record +); +export const metricsExplorerCustomMetricAggregationRT = rt.keyof( + metricsExplorerCustomMetricAggregationKeys +); + export const metricsExplorerMetricRequiredFieldsRT = rt.type({ aggregation: metricsExplorerAggregationRT, }); +export const metricsExplorerCustomMetricRT = rt.intersection([ + rt.type({ + name: rt.string, + aggregation: metricsExplorerCustomMetricAggregationRT, + }), + rt.partial({ + field: rt.string, + filter: rt.string, + }), +]); + +export type MetricsExplorerCustomMetric = rt.TypeOf; + export const metricsExplorerMetricOptionalFieldsRT = rt.partial({ field: rt.union([rt.string, rt.undefined]), + custom_metrics: rt.array(metricsExplorerCustomMetricRT), + equation: rt.string, }); export const metricsExplorerMetricRT = rt.intersection([ diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.stories.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.stories.tsx new file mode 100644 index 00000000000000..ce30172a74f153 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.stories.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, Story } from '@storybook/react/types-6-0'; +import React, { useCallback, useEffect, useState } from 'react'; +import { TimeUnitChar } from '@kbn/observability-plugin/common'; +import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; +import { + Aggregators, + Comparator, + MetricExpressionParams, +} from '../../../../../common/alerting/metrics'; +import { decorateWithGlobalStorybookThemeProviders } from '../../../../test_utils/use_global_storybook_theme'; +import { CustomEquationEditor, CustomEquationEditorProps } from './custom_equation_editor'; +import { aggregationType } from '../expression_row'; +import { MetricExpression } from '../../types'; +import { validateMetricThreshold } from '../validation'; + +export default { + title: 'infra/alerting/CustomEquationEditor', + decorators: [ + (wrappedStory) =>
{wrappedStory()}
, + decorateWithGlobalStorybookThemeProviders, + ], + parameters: { + layout: 'padded', + }, + argTypes: { + onChange: { action: 'changed' }, + }, +} as Meta; + +const CustomEquationEditorTemplate: Story = (args) => { + const [expression, setExpression] = useState(args.expression); + const [errors, setErrors] = useState(args.errors); + + const handleExpressionChange = useCallback( + (exp: MetricExpression) => { + setExpression(exp); + args.onChange(exp); + return exp; + }, + [args] + ); + + useEffect(() => { + const validationObject = validateMetricThreshold({ + criteria: [expression as MetricExpressionParams], + }); + setErrors(validationObject.errors[0]); + }, [expression]); + + return ( + + ); +}; + +export const CustomEquationEditorDefault = CustomEquationEditorTemplate.bind({}); +export const CustomEquationEditorWithEquationErrors = CustomEquationEditorTemplate.bind({}); +export const CustomEquationEditorWithFieldError = CustomEquationEditorTemplate.bind({}); + +const BASE_ARGS = { + expression: { + aggType: Aggregators.CUSTOM, + timeSize: 1, + timeUnit: 'm' as TimeUnitChar, + threshold: [1], + comparator: Comparator.GT, + }, + fields: [ + { name: 'system.cpu.user.pct', normalizedType: 'number' }, + { name: 'system.cpu.system.pct', normalizedType: 'number' }, + { name: 'system.cpu.cores', normalizedType: 'number' }, + ], + aggregationTypes: aggregationType, +}; + +CustomEquationEditorDefault.args = { + ...BASE_ARGS, + errors: {}, +}; + +CustomEquationEditorWithEquationErrors.args = { + ...BASE_ARGS, + expression: { + ...BASE_ARGS.expression, + equation: 'Math.round(A / B)', + customMetrics: [ + { name: 'A', aggType: Aggregators.AVERAGE, field: 'system.cpu.user.pct' }, + { name: 'B', aggType: Aggregators.MAX, field: 'system.cpu.cores' }, + ], + }, + errors: { + equation: + 'The equation field only supports the following characters: A-Z, +, -, /, *, (, ), ?, !, &, :, |, >, <, =', + }, +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.tsx new file mode 100644 index 00000000000000..866e818688533d --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/custom_equation_editor.tsx @@ -0,0 +1,214 @@ +/* + * 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 { + EuiFieldText, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; +import React, { useState, useCallback, useMemo } from 'react'; +import { omit, range, first, xor, debounce } from 'lodash'; +import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS } from '../../../../../common/http_api'; +import { + Aggregators, + CustomMetricAggTypes, + MetricExpressionCustomMetric, +} from '../../../../../common/alerting/metrics'; +import { MetricExpression } from '../../types'; +import { CustomMetrics, AggregationTypes, NormalizedFields } from './types'; +import { MetricRowWithAgg } from './metric_row_with_agg'; +import { MetricRowWithCount } from './metric_row_with_count'; +import { + CUSTOM_EQUATION, + EQUATION_HELP_MESSAGE, + LABEL_HELP_MESSAGE, + LABEL_LABEL, +} from '../../i18n_strings'; + +export interface CustomEquationEditorProps { + onChange: (expression: MetricExpression) => void; + expression: MetricExpression; + fields: NormalizedFields; + aggregationTypes: AggregationTypes; + errors: IErrorObject; +} + +const NEW_METRIC = { name: 'A', aggType: Aggregators.AVERAGE as CustomMetricAggTypes }; +const MAX_VARIABLES = 26; +const CHAR_CODE_FOR_A = 65; +const CHAR_CODE_FOR_Z = CHAR_CODE_FOR_A + MAX_VARIABLES; +const VAR_NAMES = range(CHAR_CODE_FOR_A, CHAR_CODE_FOR_Z).map((c) => String.fromCharCode(c)); + +export const CustomEquationEditor = ({ + onChange, + expression, + fields, + aggregationTypes, + errors, +}: CustomEquationEditorProps) => { + const [customMetrics, setCustomMetrics] = useState( + expression?.customMetrics ?? [NEW_METRIC] + ); + const [label, setLabel] = useState(expression?.label || undefined); + const [equation, setEquation] = useState(expression?.equation || undefined); + const debouncedOnChange = useMemo(() => debounce(onChange, 500), [onChange]); + + const handleAddNewRow = useCallback(() => { + setCustomMetrics((previous) => { + const currentVars = previous?.map((m) => m.name) ?? []; + const name = first(xor(VAR_NAMES, currentVars))!; + const nextMetrics = [...(previous || []), { ...NEW_METRIC, name }]; + debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation, label }); + return nextMetrics; + }); + }, [debouncedOnChange, equation, expression, label]); + + const handleDelete = useCallback( + (name: string) => { + setCustomMetrics((previous) => { + const nextMetrics = previous?.filter((row) => row.name !== name) ?? [NEW_METRIC]; + const finalMetrics = (nextMetrics.length && nextMetrics) || [NEW_METRIC]; + debouncedOnChange({ ...expression, customMetrics: finalMetrics, equation, label }); + return finalMetrics; + }); + }, + [equation, expression, debouncedOnChange, label] + ); + + const handleChange = useCallback( + (metric: MetricExpressionCustomMetric) => { + setCustomMetrics((previous) => { + const nextMetrics = previous?.map((m) => (m.name === metric.name ? metric : m)); + debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation, label }); + return nextMetrics; + }); + }, + [equation, expression, debouncedOnChange, label] + ); + + const handleEquationChange = useCallback( + (e: React.ChangeEvent) => { + setEquation(e.target.value); + debouncedOnChange({ ...expression, customMetrics, equation: e.target.value, label }); + }, + [debouncedOnChange, expression, customMetrics, label] + ); + + const handleLabelChange = useCallback( + (e: React.ChangeEvent) => { + setLabel(e.target.value); + debouncedOnChange({ ...expression, customMetrics, equation, label: e.target.value }); + }, + [debouncedOnChange, expression, customMetrics, equation] + ); + + const disableAdd = customMetrics?.length === MAX_VARIABLES; + const disableDelete = customMetrics?.length === 1; + + const filteredAggregationTypes = omit(aggregationTypes, OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS); + + const metricRows = customMetrics?.map((row) => { + if (row.aggType === Aggregators.COUNT) { + return ( + + ); + } + return ( + + ); + }); + + const placeholder = useMemo(() => { + return customMetrics?.map((row) => row.name).join(' + '); + }, [customMetrics]); + + return ( +
+ + {metricRows} + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/index.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/index.tsx new file mode 100644 index 00000000000000..2c885581b3989c --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/index.tsx @@ -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 { CustomEquationEditor } from './custom_equation_editor'; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_controls.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_controls.tsx new file mode 100644 index 00000000000000..3c8efe19a01d7a --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_controls.tsx @@ -0,0 +1,31 @@ +/* + * 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 { EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; +import { DELETE_LABEL } from '../../i18n_strings'; + +interface MetricRowControlProps { + onDelete: () => void; + disableDelete: boolean; +} + +export const MetricRowControls = ({ onDelete, disableDelete }: MetricRowControlProps) => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_agg.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_agg.tsx new file mode 100644 index 00000000000000..1e80f743d7967f --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_agg.tsx @@ -0,0 +1,137 @@ +/* + * 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 { + EuiFormRow, + EuiHorizontalRule, + EuiFlexItem, + EuiFlexGroup, + EuiSelect, + EuiComboBox, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import React, { useMemo, useCallback } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { Aggregators, CustomMetricAggTypes } from '../../../../../common/alerting/metrics'; +import { MetricRowControls } from './metric_row_controls'; +import { NormalizedFields, MetricRowBaseProps } from './types'; + +interface MetricRowWithAggProps extends MetricRowBaseProps { + aggType?: CustomMetricAggTypes; + field?: string; + fields: NormalizedFields; +} + +export const MetricRowWithAgg = ({ + name, + aggType = Aggregators.AVERAGE, + field, + onDelete, + disableDelete, + fields, + aggregationTypes, + onChange, + errors, +}: MetricRowWithAggProps) => { + const handleDelete = useCallback(() => { + onDelete(name); + }, [name, onDelete]); + + const fieldOptions = useMemo( + () => + fields.reduce((acc, fieldValue) => { + if ( + aggType && + aggregationTypes[aggType].validNormalizedTypes.includes(fieldValue.normalizedType) + ) { + acc.push({ label: fieldValue.name }); + } + return acc; + }, [] as Array<{ label: string }>), + [fields, aggregationTypes, aggType] + ); + + const aggOptions = useMemo( + () => + Object.values(aggregationTypes).map((a) => ({ + text: a.text, + value: a.value, + })), + [aggregationTypes] + ); + + const handleFieldChange = useCallback( + (selectedOptions: EuiComboBoxOptionOption[]) => { + onChange({ + name, + field: (selectedOptions.length && selectedOptions[0].label) || undefined, + aggType, + }); + }, + [name, aggType, onChange] + ); + + const handleAggChange = useCallback( + (el: React.ChangeEvent) => { + onChange({ + name, + field, + aggType: el.target.value as CustomMetricAggTypes, + }); + }, + [name, field, onChange] + ); + + const isAggInvalid = get(errors, ['customMetrics', name, 'aggType']) != null; + const isFieldInvalid = get(errors, ['customMetrics', name, 'field']) != null || !field; + + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_count.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_count.tsx new file mode 100644 index 00000000000000..43ac682830bcde --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/metric_row_with_count.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiFieldText, + EuiFormRow, + EuiHorizontalRule, + EuiFlexItem, + EuiFlexGroup, + EuiSelect, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { Aggregators, CustomMetricAggTypes } from '../../../../../common/alerting/metrics'; +import { MetricRowControls } from './metric_row_controls'; +import { MetricRowBaseProps } from './types'; + +interface MetricRowWithCountProps extends MetricRowBaseProps { + agg?: Aggregators; + filter?: string; +} + +export const MetricRowWithCount = ({ + name, + agg, + filter, + onDelete, + disableDelete, + onChange, + aggregationTypes, +}: MetricRowWithCountProps) => { + const aggOptions = useMemo( + () => + Object.values(aggregationTypes) + .filter((aggType) => aggType.value !== Aggregators.CUSTOM) + .map((aggType) => ({ + text: aggType.text, + value: aggType.value, + })), + [aggregationTypes] + ); + + const handleDelete = useCallback(() => { + onDelete(name); + }, [name, onDelete]); + + const handleAggChange = useCallback( + (el: React.ChangeEvent) => { + onChange({ + name, + filter, + aggType: el.target.value as CustomMetricAggTypes, + }); + }, + [name, filter, onChange] + ); + + const handleFilterChange = useCallback( + (el: React.ChangeEvent) => { + onChange({ + name, + filter: el.target.value, + aggType: agg as CustomMetricAggTypes, + }); + }, + [name, agg, onChange] + ); + + return ( + <> + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/types.ts new file mode 100644 index 00000000000000..60069c6bb79d27 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/custom_equation/types.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 { AggregationType, IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; +import { MetricExpressionCustomMetric } from '../../../../../common/alerting/metrics'; +import { MetricExpression } from '../../types'; + +export type CustomMetrics = MetricExpression['customMetrics']; + +export interface AggregationTypes { + [x: string]: AggregationType; +} + +export interface NormalizedField { + name: string; + normalizedType: string; +} + +export type NormalizedFields = NormalizedField[]; + +export interface MetricRowBaseProps { + name: string; + onAdd: () => void; + onDelete: (name: string) => void; + disableDelete: boolean; + disableAdd: boolean; + onChange: (metric: MetricExpressionCustomMetric) => void; + aggregationTypes: AggregationTypes; + errors: IErrorObject; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 87bc52322c7d32..8fe00f5a34c73d 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -17,7 +17,10 @@ import { Color } from '../../../../common/color_palette'; import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; import { MetricExpression } from '../types'; -import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { + MetricsExplorerChartType, + MetricsExplorerOptionsMetric, +} from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric'; import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data'; @@ -32,6 +35,7 @@ import { getChartTheme, } from '../../common/criterion_preview_chart/criterion_preview_chart'; import { ThresholdAnnotations } from '../../common/criterion_preview_chart/threshold_annotations'; +import { CUSTOM_EQUATION } from '../i18n_strings'; interface Props { expression: MetricExpression; @@ -58,11 +62,15 @@ export const ExpressionChart: React.FC = ({ const { uiSettings } = useKibanaContextForPlugin().services; - const metric = { + const metric: MetricsExplorerOptionsMetric = { field: expression.metric, aggregation: expression.aggType as MetricsExplorerAggregation, color: Color.color0, }; + + if (metric.aggregation === 'custom') { + metric.label = expression.label || CUSTOM_EQUATION; + } const isDarkMode = uiSettings?.get('theme:darkMode') || false; const dateFormatter = useMemo(() => { const firstSeries = first(data?.series); @@ -79,10 +87,14 @@ export const ExpressionChart: React.FC = ({ /* eslint-disable-next-line react-hooks/exhaustive-deps */ const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]); - if (loading || !data) { + if (loading) { return ; } + if (!data) { + return ; + } + const criticalThresholds = expression.threshold.slice().sort(); const warningThresholds = expression.warningThreshold?.slice().sort() ?? []; const thresholds = [...criticalThresholds, ...warningThresholds].sort(); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx index 2204dd16fd46a3..14ff5b1e60eed6 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx @@ -20,16 +20,19 @@ import { omit } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { + AggregationType, builtInComparators, IErrorObject, OfExpression, ThresholdExpression, WhenExpression, } from '@kbn/triggers-actions-ui-plugin/public'; -import { Comparator } from '../../../../common/alerting/metrics'; +import { Aggregators, Comparator } from '../../../../common/alerting/metrics'; import { decimalToPct, pctToDecimal } from '../../../../common/utils/corrected_percent_convert'; import { DerivedIndexPattern } from '../../../containers/metrics_source'; import { AGGREGATION_TYPES, MetricExpression } from '../types'; +import { CustomEquationEditor } from './custom_equation'; +import { CUSTOM_EQUATION } from '../i18n_strings'; const customComparators = { ...builtInComparators, @@ -73,6 +76,7 @@ export const ExpressionRow: React.FC = (props) => { const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]); const { children, setRuleParams, expression, errors, expressionId, remove, fields, canDelete } = props; + const { aggType = AGGREGATION_TYPES.MAX, metric, @@ -92,7 +96,10 @@ export const ExpressionRow: React.FC = (props) => { setRuleParams(expressionId, { ...expression, aggType: at as MetricExpression['aggType'], - metric: at === 'count' ? undefined : expression.metric, + metric: ['custom', 'count'].includes(at) ? undefined : expression.metric, + customMetrics: at === 'custom' ? expression.customMetrics : undefined, + equation: at === 'custom' ? expression.equation : undefined, + label: at === 'custom' ? expression.label : undefined, }); }, [expressionId, expression, setRuleParams] @@ -166,6 +173,13 @@ export const ExpressionRow: React.FC = (props) => { expressionId, ]); + const handleCustomMetricChange = useCallback( + (exp) => { + setRuleParams(expressionId, exp); + }, + [expressionId, setRuleParams] + ); + const criticalThresholdExpression = ( = (props) => { /> ); + const normalizedFields = fields.map((f) => ({ + normalizedType: f.type, + name: f.name, + })); + return ( <> @@ -201,7 +220,7 @@ export const ExpressionRow: React.FC = (props) => { /> - + = (props) => { onChangeSelectedAggType={updateAggType} /> - {aggType !== 'count' && ( + {!['count', 'custom'].includes(aggType) && ( ({ - normalizedType: f.type, - name: f.name, - }))} + fields={normalizedFields} aggType={aggType} errors={errors} onChangeSelectedAggField={updateMetric} @@ -245,6 +261,25 @@ export const ExpressionRow: React.FC = (props) => { )} {!displayWarningThreshold && criticalThresholdExpression} + {!displayWarningThreshold && ( + <> + + + + + + + + )} {displayWarningThreshold && ( <> @@ -280,24 +315,19 @@ export const ExpressionRow: React.FC = (props) => { )} - {!displayWarningThreshold && ( + {aggType === Aggregators.CUSTOM && ( <> - {' '} - + - - - + + )} @@ -358,7 +388,7 @@ const ThresholdElement: React.FC<{ ); }; -export const aggregationType: { [key: string]: any } = { +export const aggregationType: { [key: string]: AggregationType } = { avg: { text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { defaultMessage: 'Average', @@ -431,4 +461,10 @@ export const aggregationType: { [key: string]: any } = { value: AGGREGATION_TYPES.P99, validNormalizedTypes: ['number', 'histogram'], }, + custom: { + text: CUSTOM_EQUATION, + fieldRequired: false, + value: AGGREGATION_TYPES.CUSTOM, + validNormalizedTypes: ['number', 'histogram'], + }, }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.test.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.test.ts new file mode 100644 index 00000000000000..ac1b545e4a302a --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.test.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 { EQUATION_REGEX } from './validation'; + +describe('Metric Threshold Validation', () => { + describe('valid equations', () => { + const validExpression = [ + '(A + B) / 100', + '(A - B) * 100', + 'A > 1 ? A : B', + 'A <= 1 ? A : B', + 'A && B || C', + ]; + validExpression.forEach((exp) => { + it(exp, () => { + expect(exp.match(EQUATION_REGEX)).toBeFalsy(); + }); + }); + }); + describe('invalid equations', () => { + const validExpression = ['Math.round(A + B) / 100', '(A^2 - B) * 100']; + validExpression.forEach((exp) => { + it(exp, () => { + expect(exp.match(EQUATION_REGEX)).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx index bc75b2512fbc1e..b3d4d423c58b53 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx @@ -7,13 +7,24 @@ import { i18n } from '@kbn/i18n'; import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public'; +import { isEmpty } from 'lodash'; import { + Aggregators, Comparator, + CustomMetricExpressionParams, FilterQuery, MetricExpressionParams, QUERY_INVALID, } from '../../../../common/alerting/metrics'; +export const EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; + +const isCustomMetricExpressionParams = ( + subject: MetricExpressionParams +): subject is CustomMetricExpressionParams => { + return subject.aggType === Aggregators.CUSTOM; +}; + export function validateMetricThreshold({ criteria, filterQuery, @@ -36,6 +47,9 @@ export function validateMetricThreshold({ threshold1: string[]; }; metric: string[]; + customMetricsError?: string; + customMetrics: Record; + equation?: string; }; } & { filterQuery?: string[] } = {}; validationResult.errors = errors; @@ -70,6 +84,7 @@ export function validateMetricThreshold({ }, metric: [], filterQuery: [], + customMetrics: {}, }; if (!c.aggType) { errors[id].aggField.push( @@ -136,16 +151,59 @@ export function validateMetricThreshold({ ); } - if (!c.metric && c.aggType !== 'count') { + if (!c.metric && c.aggType !== 'count' && c.aggType !== 'custom') { errors[id].metric.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { defaultMessage: 'Metric is required.', }) ); } + + if (isCustomMetricExpressionParams(c)) { + if (!c.customMetrics || (c.customMetrics && c.customMetrics.length < 1)) { + errors[id].customMetricsError = i18n.translate( + 'xpack.infra.metrics.alertFlyout.error.customMetricsError', + { + defaultMessage: 'You must define at least 1 custom metric', + } + ); + } else { + c.customMetrics.forEach((metric) => { + const customMetricErrors: { aggType?: string; field?: string } = {}; + if (!metric.aggType) { + customMetricErrors.aggType = i18n.translate( + 'xpack.infra.metrics.alertFlyout.error.customMetrics.aggTypeRequired', + { + defaultMessage: 'Aggregation is required', + } + ); + } + if (metric.aggType !== 'count' && !metric.field) { + customMetricErrors.field = i18n.translate( + 'xpack.infra.metrics.alertFlyout.error.customMetrics.fieldRequired', + { + defaultMessage: 'Field is required', + } + ); + } + if (!isEmpty(customMetricErrors)) { + errors[id].customMetrics[metric.name] = customMetricErrors; + } + }); + } + + if (c.equation && c.equation.match(EQUATION_REGEX)) { + errors[id].equation = i18n.translate( + 'xpack.infra.metrics.alertFlyout.error.equation.invalidCharacters', + { + defaultMessage: + 'The equation field only supports the following characters: A-Z, +, -, /, *, (, ), ?, !, &, :, |, >, <, =', + } + ); + } + } }); return validationResult; } - const isNumber = (value: unknown): value is number => typeof value === 'number'; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 1ae1bacfed42e4..6fcbd6df759189 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -7,10 +7,12 @@ import { DataViewBase } from '@kbn/es-query'; import { useMemo } from 'react'; +import { MetricExpressionCustomMetric } from '../../../../common/alerting/metrics'; import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { MetricExpression } from '../types'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; +import { MetricExplorerCustomMetricAggregations } from '../../../../common/http_api/metrics_explorer'; export const useMetricsExplorerChartData = ( expression: MetricExpression, @@ -20,6 +22,7 @@ export const useMetricsExplorerChartData = ( groupBy?: string | string[] ) => { const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; + const options: MetricsExplorerOptions = useMemo( () => ({ limit: 1, @@ -28,14 +31,26 @@ export const useMetricsExplorerChartData = ( groupBy, filterQuery, metrics: [ - { - field: expression.metric, - aggregation: expression.aggType, - }, + expression.aggType === 'custom' + ? { + aggregation: 'custom', + custom_metrics: + expression?.customMetrics?.map(mapMetricThresholdMetricToMetricsExplorerMetric) ?? + [], + equation: expression.equation, + } + : { field: expression.metric, aggregation: expression.aggType }, ], aggregation: expression.aggType || 'avg', }), - [expression.aggType, expression.metric, filterQuery, groupBy] + [ + expression.aggType, + expression.equation, + expression.metric, + expression.customMetrics, + filterQuery, + groupBy, + ] ); const timerange = useMemo( () => ({ @@ -55,3 +70,19 @@ export const useMetricsExplorerChartData = ( null ); }; + +const mapMetricThresholdMetricToMetricsExplorerMetric = (metric: MetricExpressionCustomMetric) => { + if (metric.aggType === 'count') { + return { + name: metric.name, + aggregation: 'count' as MetricExplorerCustomMetricAggregations, + filter: metric.filter, + }; + } + + return { + name: metric.name, + aggregation: metric.aggType as MetricExplorerCustomMetricAggregations, + field: metric.field, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/i18n_strings.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/i18n_strings.ts new file mode 100644 index 00000000000000..a2778bfc6b8329 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/i18n_strings.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const EQUATION_HELP_MESSAGE = i18n.translate( + 'xpack.infra.metrics.alertFlyout.customEquationEditor.equationHelpMessage', + { defaultMessage: 'Supports basic math expressions' } +); + +export const LABEL_LABEL = i18n.translate( + 'xpack.infra.metrics.alertFlyout.customEquationEditor.labelLabel', + { defaultMessage: 'Label (optional)' } +); + +export const LABEL_HELP_MESSAGE = i18n.translate( + 'xpack.infra.metrics.alertFlyout.customEquationEditor.labelHelpMessage', + { + defaultMessage: 'Custom label will show on the alert chart and in reason/alert title', + } +); + +export const CUSTOM_EQUATION = i18n.translate('xpack.infra.metrics.alertFlyout.customEquation', { + defaultMessage: 'Custom equation', +}); + +export const DELETE_LABEL = i18n.translate( + 'xpack.infra.metrics.alertFlyout.customEquationEditor.deleteRowButton', + { defaultMessage: 'Delete' } +); + +export const AGGREGATION_LABEL = (name: string) => + i18n.translate('xpack.infra.metrics.alertFlyout.customEquationEditor.aggregationLabel', { + defaultMessage: 'Aggregation {name}', + values: { name }, + }); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index a88dd1d4548b8d..aa9336cb6023d9 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -14,8 +14,14 @@ export interface AlertContextMeta { series?: MetricsExplorerSeries; } -export type MetricExpression = Omit & { +export type MetricExpression = Omit< + MetricExpressionParams, + 'metric' | 'timeSize' | 'timeUnit' | 'metrics' | 'equation' | 'customMetrics' +> & { metric?: MetricExpressionParams['metric']; + customMetrics?: MetricExpressionParams['customMetrics']; + label?: MetricExpressionParams['label']; + equation?: MetricExpressionParams['equation']; timeSize?: MetricExpressionParams['timeSize']; timeUnit?: MetricExpressionParams['timeUnit']; }; @@ -30,6 +36,7 @@ export enum AGGREGATION_TYPES { CARDINALITY = 'cardinality', P95 = 'p95', P99 = 'p99', + CUSTOM = 'custom', } export interface MetricThresholdAlertParams { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts index 32f0e1d06cff82..bc76dbba39e104 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get } from 'lodash'; +import { get, isNumber } from 'lodash'; import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; import { InfraFormatterType } from '../../../../lib/lib'; import { @@ -95,7 +95,7 @@ export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => (val: string | number) => { if (SnapshotCustomMetricInputRT.is(metric)) { const formatter = createFormatterForMetric(metric); - return formatter(val); + return isNumber(val) ? formatter(val) : val; } const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); if (val == null || !metricFormatter) { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx index 7b76cf22098fbb..d9117da0012479 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx @@ -9,6 +9,7 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; +import { xor } from 'lodash'; import { MetricsExplorerAggregation } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; import { @@ -22,6 +23,8 @@ interface Props { onChange: (aggregation: MetricsExplorerAggregation) => void; } +type MetricsExplorerAggregationWithoutCustom = Exclude; + export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) => { const AGGREGATION_LABELS = { ['avg']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.avg', { @@ -66,14 +69,18 @@ export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) = defaultMessage: 'Select an aggregation', }); + const METRIC_EXPLORER_AGGREGATIONS_WITHOUT_CUSTOM = xor(METRIC_EXPLORER_AGGREGATIONS, [ + 'custom', + ]) as MetricsExplorerAggregationWithoutCustom[]; + return ( ({ - text: AGGREGATION_LABELS[k as MetricsExplorerAggregation], + options={METRIC_EXPLORER_AGGREGATIONS_WITHOUT_CUSTOM.map((k) => ({ + text: AGGREGATION_LABELS[k], value: k, }))} onChange={handleChange} diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts index 3857b81825c00f..b6f77bfc260a41 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts @@ -5,11 +5,15 @@ * 2.0. */ +import numeral from '@elastic/numeral'; import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; import { createFormatter } from '../../../../../../common/formatters'; import { InfraFormatterType } from '../../../../../lib/lib'; import { metricToFormat } from './metric_to_format'; export const createFormatterForMetric = (metric?: MetricsExplorerMetric) => { + if (metric?.aggregation === 'custom') { + return (input: number) => numeral(input).format('0.[0000]'); + } if (metric && metric.field) { const format = metricToFormat(metric); if (format === InfraFormatterType.bits && metric.aggregation === 'rate') { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index 2c1be3d17f6f86..ccb1d316d73d2f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -67,13 +67,7 @@ export function useMetricsExplorerData( body: JSON.stringify({ forceInterval: options.forceInterval, dropLastBucket: options.dropLastBucket != null ? options.dropLastBucket : true, - metrics: - options.aggregation === 'count' - ? [{ aggregation: 'count' }] - : options.metrics.map((metric) => ({ - aggregation: metric.aggregation, - field: metric.field, - })), + metrics: options.aggregation === 'count' ? [{ aggregation: 'count' }] : options.metrics, groupBy: options.groupBy, afterKey, limit: options.limit, @@ -117,6 +111,7 @@ export function useMetricsExplorerData( }, onReject: (e: unknown) => { setError(e as Error); + setData(null); setLoading(false); }, }, diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts index 5e8d8e4e1ee95e..ab7b0490aa9015 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts @@ -20,6 +20,13 @@ export const DOCUMENT_COUNT_I18N = i18n.translate( } ); +export const CUSTOM_EQUATION_I18N = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.customEquation', + { + defaultMessage: 'Custom equation', + } +); + export const stateToAlertMessage = { [AlertStates.ALERT]: i18n.translate('xpack.infra.metrics.alerting.threshold.alertState', { defaultMessage: 'ALERT', @@ -76,7 +83,7 @@ const thresholdToI18n = ([a, b]: Array) => { }); }; -const formatGroup = (group: string) => (group === UNGROUPED_FACTORY_KEY ? 'all hosts' : group); +const formatGroup = (group: string) => (group === UNGROUPED_FACTORY_KEY ? '' : ` for ${group}`); export const buildFiredAlertReason: (alertResult: { group: string; @@ -89,7 +96,7 @@ export const buildFiredAlertReason: (alertResult: { }) => string = ({ group, metric, comparator, threshold, currentValue, timeSize, timeUnit }) => i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', { defaultMessage: - '{metric} is {currentValue} in the last {duration} for {group}. Alert when {comparator} {threshold}.', + '{metric} is {currentValue} in the last {duration}{group}. Alert when {comparator} {threshold}.', values: { group: formatGroup(group), metric, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_bucket_selector.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_bucket_selector.ts index d078cee95e45eb..a458104a7400a4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_bucket_selector.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_bucket_selector.ts @@ -32,12 +32,14 @@ export const createBucketSelector = ( const isCount = condition.aggType === Aggregators.COUNT; const isRate = condition.aggType === Aggregators.RATE; const bucketPath = isCount - ? 'currentPeriod>_count' + ? "currentPeriod['all']>_count" : isRate ? `aggregatedValue` : isPercentile - ? `currentPeriod>aggregatedValue[${condition.aggType === Aggregators.P95 ? '95' : '99'}]` - : 'currentPeriod>aggregatedValue'; + ? `currentPeriod[\'all\']>aggregatedValue[${ + condition.aggType === Aggregators.P95 ? '95' : '99' + }]` + : "currentPeriod['all']>aggregatedValue"; const shouldWarn = hasWarn ? { @@ -74,7 +76,7 @@ export const createBucketSelector = ( bucket_script: { buckets_path: { lastPeriod: 'lastPeriod>_count', - currentPeriod: 'currentPeriod>_count', + currentPeriod: "currentPeriod['all']>_count", }, script: 'params.lastPeriod > 0 && params.currentPeriod < 1 ? 1 : 0', }, @@ -83,7 +85,7 @@ export const createBucketSelector = ( bucket_script: { buckets_path: { lastPeriod: 'lastPeriod>_count', - currentPeriod: 'currentPeriod>_count', + currentPeriod: "currentPeriod['all']>_count", }, script: 'params.lastPeriod < 1 && params.currentPeriod > 0 ? 1 : 0', }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_rate_aggregation.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_rate_aggregation.ts index 2fdb8f5c6b834f..e6b24e334a7456 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_rate_aggregation.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_rate_aggregation.ts @@ -21,8 +21,8 @@ export const createRateAggsBucketScript = ( [id]: { bucket_script: { buckets_path: { - first: `currentPeriod>${id}_first_bucket.maxValue`, - second: `currentPeriod>${id}_second_bucket.maxValue`, + first: `currentPeriod['all']>${id}_first_bucket.maxValue`, + second: `currentPeriod['all']>${id}_second_bucket.maxValue`, }, script: `params.second > 0.0 && params.first > 0.0 && params.second > params.first ? (params.second - params.first) / ${intervalInSeconds}: null`, }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts index b9c7817eb5be48..7125f18aa2a8d4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts @@ -11,7 +11,7 @@ import type { Logger } from '@kbn/logging'; import { MetricExpressionParams } from '../../../../../common/alerting/metrics'; import { InfraSource } from '../../../../../common/source_configuration/source_configuration'; import { getIntervalInSeconds } from '../../../../../common/utils/get_interval_in_seconds'; -import { DOCUMENT_COUNT_I18N } from '../../common/messages'; +import { CUSTOM_EQUATION_I18N, DOCUMENT_COUNT_I18N } from '../../common/messages'; import { createTimerange } from './create_timerange'; import { getData } from './get_data'; import { checkMissingGroups, MissingGroupsRecord } from './check_missing_group'; @@ -101,7 +101,14 @@ export const evaluateRule = async ( ) => { return { currentPeriod: { - filter: { - range: { - [TIMESTAMP_FIELD]: { - gte: moment(timeframe.start).toISOString(), - lte: moment(timeframe.end).toISOString(), + filters: { + filters: { + all: { + range: { + [TIMESTAMP_FIELD]: { + gte: moment(timeframe.start).toISOString(), + lte: moment(timeframe.end).toISOString(), + }, + }, }, }, }, 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 409dede158515e..92fbc186dce5ff 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 @@ -878,8 +878,6 @@ describe('The metric threshold alert type', () => { expect(reasons[1]).toContain('Alert when >= 3'); expect(reasons[0]).toContain('in the last 1 min'); expect(reasons[1]).toContain('in the last 1 min'); - expect(reasons[0]).toContain('for all hosts'); - expect(reasons[1]).toContain('for all hosts'); }); }); describe('querying with the count aggregator', () => { @@ -1659,9 +1657,7 @@ describe('The metric threshold alert type', () => { const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); - expect(action.reason).toBe( - 'test.metric.1 is 2.5 in the last 1 min for all hosts. Alert when > 2.49.' - ); + expect(action.reason).toBe('test.metric.1 is 2.5 in the last 1 min. Alert when > 2.49.'); }); test('reports expected warning values to the action context for percentage metric', async () => { @@ -1675,9 +1671,7 @@ describe('The metric threshold alert type', () => { const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); - expect(action.reason).toBe( - 'system.cpu.user.pct is 82% in the last 1 min for all hosts. Alert when > 81%.' - ); + expect(action.reason).toBe('system.cpu.user.pct is 82% in the last 1 min. Alert when > 81%.'); }); }); }); @@ -1823,18 +1817,19 @@ declare global { } } -const baseNonCountCriterion: Pick< - NonCountMetricExpressionParams, - 'aggType' | 'metric' | 'timeSize' | 'timeUnit' -> = { +const baseNonCountCriterion = { aggType: Aggregators.AVERAGE, metric: 'test.metric.1', timeSize: 1, timeUnit: 'm', -}; + threshold: [0], + comparator: Comparator.GT, +} as NonCountMetricExpressionParams; -const baseCountCriterion: Pick = { +const baseCountCriterion = { aggType: Aggregators.COUNT, timeSize: 1, timeUnit: 'm', -}; + threshold: [0], + comparator: Comparator.GT, +} as CountMetricExpressionParams; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts index 82f8d1c441f732..b9d069db244fbc 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts @@ -70,12 +70,42 @@ export async function registerMetricThresholdRuleType( ...baseCriterion, metric: schema.string(), aggType: oneOfLiterals(METRIC_EXPLORER_AGGREGATIONS), + customMetrics: schema.never(), + equation: schema.never(), + label: schema.never(), }); const countCriterion = schema.object({ ...baseCriterion, aggType: schema.literal('count'), metric: schema.never(), + customMetrics: schema.never(), + equation: schema.never(), + label: schema.never(), + }); + + const customCriterion = schema.object({ + ...baseCriterion, + aggType: schema.literal('custom'), + metric: schema.never(), + customMetrics: schema.arrayOf( + schema.oneOf([ + schema.object({ + name: schema.string(), + aggType: oneOfLiterals(['avg', 'sum', 'max', 'min', 'cardinality']), + field: schema.string(), + filter: schema.never(), + }), + schema.object({ + name: schema.string(), + aggType: schema.literal('count'), + filter: schema.maybe(schema.string()), + field: schema.never(), + }), + ]) + ), + equation: schema.maybe(schema.string()), + label: schema.maybe(schema.string()), }); alertingPlugin.registerType({ @@ -86,7 +116,9 @@ export async function registerMetricThresholdRuleType( validate: { params: schema.object( { - criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), + criteria: schema.arrayOf( + schema.oneOf([countCriterion, nonCountCriterion, customCriterion]) + ), groupBy: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), filterQuery: schema.maybe( schema.string({ diff --git a/x-pack/plugins/infra/server/lib/create_custom_metrics_aggregations.ts b/x-pack/plugins/infra/server/lib/create_custom_metrics_aggregations.ts new file mode 100644 index 00000000000000..862ab7c2d29619 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/create_custom_metrics_aggregations.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 { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { isEmpty } from 'lodash'; +import { MetricExpressionCustomMetric } from '../../common/alerting/metrics'; +import { MetricsExplorerCustomMetric } from '../../common/http_api'; + +const isMetricExpressionCustomMetric = ( + subject: MetricsExplorerCustomMetric | MetricExpressionCustomMetric +): subject is MetricExpressionCustomMetric => { + return (subject as MetricExpressionCustomMetric).aggType != null; +}; + +export const createCustomMetricsAggregations = ( + id: string, + customMetrics: Array, + equation?: string +) => { + const bucketsPath: { [id: string]: string } = {}; + const metricAggregations = customMetrics.reduce((acc, metric) => { + const key = `${id}_${metric.name}`; + const aggregation = isMetricExpressionCustomMetric(metric) + ? metric.aggType + : metric.aggregation; + + if (aggregation === 'count') { + bucketsPath[metric.name] = `${key}>_count`; + return { + ...acc, + [key]: { + filter: metric.filter + ? toElasticsearchQuery(fromKueryExpression(metric.filter)) + : { match_all: {} }, + }, + }; + } + + if (aggregation && metric.field) { + bucketsPath[metric.name] = key; + return { + ...acc, + [key]: { + [aggregation]: { field: metric.field }, + }, + }; + } + + return acc; + }, {}); + + if (isEmpty(metricAggregations)) { + return {}; + } + + return { + ...metricAggregations, + [id]: { + bucket_script: { + buckets_path: bucketsPath, + script: { + source: convertEquationToPainless(bucketsPath, equation), + lang: 'painless', + }, + }, + }, + }; +}; + +const convertEquationToPainless = (bucketsPath: { [id: string]: string }, equation?: string) => { + const workingEquation = equation || Object.keys(bucketsPath).join(' + '); + return Object.keys(bucketsPath).reduce((acc, key) => { + return acc.replace(key, `params.${key}`); + }, workingEquation); +}; diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index b234c5df357cd0..0625ae4828625f 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -63,42 +63,46 @@ export const query = async ( }, }; - const response = await search<{}, MetricsESResponse>(params); + try { + const response = await search<{}, MetricsESResponse>(params); - if (response.hits.total.value === 0) { - return EMPTY_RESPONSE; - } + if (response.hits.total.value === 0) { + return EMPTY_RESPONSE; + } - if (!response.aggregations) { - throw new Error('Aggregations should be present.'); - } + if (!response.aggregations) { + throw new Error('Aggregations should be present.'); + } - const { bucketSize } = calculateBucketSize({ ...options.timerange, interval }); + const { bucketSize } = calculateBucketSize({ ...options.timerange, interval }); - if (hasGroupBy) { - const aggregations = decodeOrThrow(CompositeResponseRT)(response.aggregations); - const { groupings } = aggregations; - const limit = options.limit ?? DEFAULT_LIMIT; - const returnAfterKey = !!groupings.after_key && groupings.buckets.length === limit; - const afterKey = returnAfterKey ? groupings.after_key : null; + if (hasGroupBy) { + const aggregations = decodeOrThrow(CompositeResponseRT)(response.aggregations); + const { groupings } = aggregations; + const limit = options.limit ?? DEFAULT_LIMIT; + const returnAfterKey = !!groupings.after_key && groupings.buckets.length === limit; + const afterKey = returnAfterKey ? groupings.after_key : null; + + return { + series: getSeriesFromCompositeAggregations(groupings, options, bucketSize * 1000), + info: { + afterKey, + interval: rawOptions.includeTimeseries ? bucketSize : undefined, + }, + }; + } + const aggregations = decodeOrThrow(AggregationResponseRT)(response.aggregations); return { - series: getSeriesFromCompositeAggregations(groupings, options, bucketSize * 1000), + series: getSeriesFromHistogram(aggregations, options, bucketSize * 1000), info: { - afterKey, - interval: rawOptions.includeTimeseries ? bucketSize : undefined, + afterKey: null, + interval: bucketSize, }, }; + } catch (e) { + throw e; } - - const aggregations = decodeOrThrow(AggregationResponseRT)(response.aggregations); - return { - series: getSeriesFromHistogram(aggregations, options, bucketSize * 1000), - info: { - afterKey: null, - interval: bucketSize, - }, - }; }; const getSeriesFromHistogram = ( diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts index 5399d182f41856..0c87e8eca47d9e 100644 --- a/x-pack/plugins/infra/server/lib/metrics/types.ts +++ b/x-pack/plugins/infra/server/lib/metrics/types.ts @@ -68,6 +68,7 @@ export const BucketRT = rt.record( MetricValueTypeRT, TermsWithMetrics, rt.record(rt.string, rt.string), + rt.type({ doc_count: rt.number }), ]) ); diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts index a1c9791bc20a42..299bc75f03114c 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_metric_to_metrics_api_metric.ts @@ -5,8 +5,10 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricsAPIMetric, MetricsExplorerMetric } from '../../../../common/http_api'; +import { createCustomMetricsAggregations } from '../../../lib/create_custom_metrics_aggregations'; export const convertMetricToMetricsAPIMetric = ( metric: MetricsExplorerMetric, @@ -63,4 +65,18 @@ export const convertMetricToMetricsAPIMetric = ( }, }; } + + if (metric.aggregation === 'custom' && metric.custom_metrics) { + const customMetricAggregations = createCustomMetricsAggregations( + id, + metric.custom_metrics, + metric.equation + ); + if (!isEmpty(customMetricAggregations)) { + return { + id, + aggregations: customMetricAggregations, + }; + } + } }; diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts index 3766d2a06364bb..189f198db3812a 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -11,6 +11,7 @@ import { Aggregators, Comparator, CountMetricExpressionParams, + CustomMetricExpressionParams, NonCountMetricExpressionParams, } from '@kbn/infra-plugin/common/alerting/metrics'; import { InfraSource } from '@kbn/infra-plugin/common/source_configuration/source_configuration'; @@ -40,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) { comparator: Comparator.GT_OR_EQ, aggType: Aggregators.SUM, metric: 'value', - }, + } as NonCountMetricExpressionParams, ], }; @@ -138,6 +139,73 @@ export default function ({ getService }: FtrProviderContext) { }, ]); }); + it('should alert with custom metric that is a document ratio', async () => { + const params = { + ...baseParams, + criteria: [ + { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: Comparator.GT_OR_EQ, + aggType: Aggregators.CUSTOM, + customMetrics: [ + { name: 'A', aggType: 'count', filter: 'event.dataset: "apache2.error"' }, + { name: 'B', aggType: 'count' }, + ], + equation: '(A / B) * 100', + label: 'apache2 error ratio', + } as CustomMetricExpressionParams, + ], + }; + const config = { + ...configuration, + metricAlias: 'filebeat-*', + }; + const timeFrame = { end: DATES.ten_thousand_plus.max }; + const results = await evaluateRule( + esClient, + params, + config, + 10000, + true, + logger, + void 0, + timeFrame + ); + expect(results).to.eql([ + { + '*': { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'custom', + metric: 'apache2 error ratio', + label: 'apache2 error ratio', + customMetrics: [ + { name: 'A', aggType: 'count', filter: 'event.dataset: "apache2.error"' }, + { name: 'B', aggType: 'count' }, + ], + equation: '(A / B) * 100', + currentValue: 36.195262024407754, + timestamp: '2021-10-19T00:53:59.997Z', + shouldFire: true, + shouldWarn: false, + isNoData: false, + bucketKey: { groupBy0: '*' }, + context: { + cloud: undefined, + container: undefined, + host: undefined, + labels: undefined, + orchestrator: undefined, + tags: undefined, + }, + }, + }, + ]); + }); }); describe('with group by', () => { it('should trigger on document count', async () => { diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts index e5cd8babe8aff7..84c555f751a53c 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_explorer.ts @@ -118,6 +118,52 @@ export default function ({ getService }: FtrProviderContext) { expect(firstSeries.rows).to.have.length(0); }); + it('should work for custom metrics', async () => { + const postBody = { + timerange: { + field: '@timestamp', + to: max, + from: min, + interval: '>=1m', + }, + indexPattern: 'metricbeat-*', + metrics: [ + { + aggregation: 'custom', + custom_metrics: [ + { name: 'A', aggregation: 'avg', field: 'system.cpu.user.pct' }, + { name: 'B', aggregation: 'avg', field: 'system.cpu.user.pct' }, + ], + equation: '(A + B) * 100', + }, + ], + }; + const response = await supertest + .post('/api/infra/metrics_explorer') + .set('kbn-xsrf', 'xxx') + .send(postBody) + .expect(200); + const body = decodeOrThrow(metricsExplorerResponseRT)(response.body); + expect(body.series).length(1); + const firstSeries = first(body.series) as any; + expect(firstSeries).to.have.property('id', '*'); + expect(firstSeries.columns).to.eql([ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ]); + expect(firstSeries.rows).to.have.length(8); + expect(firstSeries.rows).to.eql([ + { timestamp: 1547571300000, metric_0: 1.0666666666666667 }, + { timestamp: 1547571360000, metric_0: 0.4333333333333334 }, + { timestamp: 1547571420000, metric_0: 0.36666666666666664 }, + { timestamp: 1547571480000, metric_0: 0.30000000000000004 }, + { timestamp: 1547571540000, metric_0: 0.33333333333333337 }, + { timestamp: 1547571600000, metric_0: 0.26666666666666666 }, + { timestamp: 1547571660000, metric_0: 0.36666666666666664 }, + { timestamp: 1547571720000, metric_0: 0.36666666666666664 }, + ]); + }); + it('should work with groupBy', async () => { const postBody = { timerange: { From 78992c6ca720733ae2038685a5246481eed76496 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Wed, 1 Feb 2023 16:26:09 +0100 Subject: [PATCH 49/59] [AO] Use EuiLoadingChart for AlertSummaryWidget loading state (#150052) ## Summary Improve loading state of AlertSummaryWidget component. Compact https://user-images.githubusercontent.com/12370520/216062356-94af9af8-1e4b-444d-8574-9b627004ef2e.mov Full-size https://user-images.githubusercontent.com/12370520/216062410-e15ee215-ca1c-478a-a7f1-c96b6cb46b5a.mov --- .../alert_summary/alert_summary_widget.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/alert_summary_widget.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/alert_summary_widget.tsx index 3bb1b56bfa78a2..a6b34a77c1154d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/alert_summary_widget.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/alert_summary_widget.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingChart } from '@elastic/eui'; import React from 'react'; import { useLoadAlertSummary } from '../../../../hooks/use_load_alert_summary'; import { AlertSummaryWidgetProps } from '.'; @@ -33,7 +33,19 @@ export const AlertSummaryWidget = ({ timeRange, }); - if (isLoading) return ; + if (isLoading) + return ( +
+ +
+ ); if (error) return ; return fullSize ? ( From 612b8e7d8a686f584e2f719416248e3255cfead0 Mon Sep 17 00:00:00 2001 From: Luke Gmys Date: Wed, 1 Feb 2023 16:29:22 +0100 Subject: [PATCH 50/59] [TIP] Ensure non-primitive values are not rendered (#150015) ## Summary Should fix https://github.com/elastic/security-team/issues/5856 Right now, it will just render the complex fields as empty. Should these be ommited or something? ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../indicators/utils/unwrap_value.test.ts | 4 ++++ .../modules/indicators/utils/unwrap_value.ts | 18 +++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.test.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.test.ts index 8edb6b53972090..1a9aeb53b76520 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.test.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.test.ts @@ -17,5 +17,9 @@ describe('unwrapValue()', () => { expect( unwrapValue({ fields: { [RawIndicatorFieldId.Type]: ['ip'] } }, RawIndicatorFieldId.Type) ).toEqual('ip'); + + expect( + unwrapValue({ fields: { [RawIndicatorFieldId.Type]: [{}] } }, RawIndicatorFieldId.Type) + ).toEqual(null); }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.ts index 8d3ef63ec9f0b2..babd540d1bb294 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/utils/unwrap_value.ts @@ -5,20 +5,24 @@ * 2.0. */ -import { Indicator, RawIndicatorFieldId } from '../../../../common/types/indicator'; +type IndicatorLike = Record<'fields', Record> | null | undefined; /** * Unpacks field value from raw indicator fields. Will return null if fields are missing entirely * or there is no record for given `fieldId` */ -export const unwrapValue = ( - indicator: Partial | null | undefined, - fieldId: RawIndicatorFieldId -): T | null => { +export const unwrapValue = (indicator: IndicatorLike, fieldId: string): T | null => { if (!indicator) { return null; } - const valueArray = indicator.fields?.[fieldId]; - return Array.isArray(valueArray) ? (valueArray[0] as T) : null; + const fieldValues = indicator.fields?.[fieldId]; + + if (!Array.isArray(fieldValues)) { + return null; + } + + const firstValue = fieldValues[0]; + + return typeof firstValue === 'object' ? null : (firstValue as T); }; From c8c27d7def7824328e00aab89fb4075f496ff478 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Wed, 1 Feb 2023 16:36:07 +0100 Subject: [PATCH 51/59] [Fleet] Add a visual indication of selected subcategory in Integrations page (#149954) Closes https://github.com/elastic/kibana/issues/149306 ## Summary Display a clear indication of selected subcategory in Integrations page https://user-images.githubusercontent.com/16084106/215807007-63dbea8d-4496-497f-b4f4-673825a21049.mov To test it locally, enable feature flag `showIntegrationsSubcategories`. Some screenshots: Screenshot 2023-01-31 at 16 12 35 Screenshot 2023-01-31 at 16 36 38 Screenshot 2023-01-31 at 16 36 51 I also split some of the components in `packageList` since that file is becoming too big and extracted another hook from `useAvailablePackages`, this hook only deals with the URL and the history. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../epm/components/package_list_grid.tsx | 469 ------------------ .../components/package_list_grid/controls.tsx | 165 ++++++ .../index.stories.tsx} | 4 +- .../components/package_list_grid/index.tsx | 272 ++++++++++ .../package_list_grid/search_box.tsx | 134 +++++ .../home/hooks/use_available_packages.tsx | 63 +-- .../home/hooks/use_build_integrations_url.tsx | 82 +++ 7 files changed, 667 insertions(+), 522 deletions(-) delete mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx rename x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/{package_list_grid.stories.tsx => package_list_grid/index.stories.tsx} (97%) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_build_integrations_url.tsx diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx deleted file mode 100644 index ccb53ac7dc3550..00000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ReactNode, FunctionComponent } from 'react'; -import { useMemo } from 'react'; -import React, { useCallback, useState } from 'react'; -import { css } from '@emotion/react'; - -import { - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiSpacer, - EuiTitle, - EuiFieldSearch, - EuiText, - useEuiTheme, - EuiIcon, - EuiScreenReaderOnly, - EuiButton, - EuiButtonIcon, - EuiPopover, - EuiContextMenuPanel, - EuiContextMenuItem, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import { Loading } from '../../../components'; -import { useLocalSearch, searchIdField } from '../../../hooks'; - -import type { IntegrationCardItem } from '../../../../../../common/types/models'; - -import type { ExtendedIntegrationCategory, CategoryFacet } from '../screens/home/category_facets'; - -import type { IntegrationsURLParameters } from '../screens/home/hooks/use_available_packages'; - -import { ExperimentalFeaturesService } from '../../../services'; - -import { promoteFeaturedIntegrations } from './utils'; - -import { PackageCard } from './package_card'; - -export interface Props { - isLoading?: boolean; - controls?: ReactNode | ReactNode[]; - list: IntegrationCardItem[]; - searchTerm: string; - setSearchTerm: (search: string) => void; - selectedCategory: ExtendedIntegrationCategory; - setCategory: (category: ExtendedIntegrationCategory) => void; - categories: CategoryFacet[]; - setUrlandReplaceHistory: (params: IntegrationsURLParameters) => void; - setUrlandPushHistory: (params: IntegrationsURLParameters) => void; - callout?: JSX.Element | null; - // Props used only in AvailablePackages component: - showCardLabels?: boolean; - title?: string; - availableSubCategories?: CategoryFacet[]; - selectedSubCategory?: string; - setSelectedSubCategory?: (c: string | undefined) => void; - showMissingIntegrationMessage?: boolean; -} - -export const PackageListGrid: FunctionComponent = ({ - isLoading, - controls, - title, - list, - searchTerm, - setSearchTerm, - selectedCategory, - setCategory, - categories, - availableSubCategories, - setSelectedSubCategory, - selectedSubCategory, - setUrlandReplaceHistory, - setUrlandPushHistory, - showMissingIntegrationMessage = false, - callout, - showCardLabels = true, -}) => { - const localSearchRef = useLocalSearch(list); - const { euiTheme } = useEuiTheme(); - - const [isPopoverOpen, setPopover] = useState(false); - - const MAX_SUBCATEGORIES_NUMBER = 6; - - const { showIntegrationsSubcategories } = ExperimentalFeaturesService.get(); - - const onButtonClick = () => { - setPopover(!isPopoverOpen); - }; - - const closePopover = () => { - setPopover(false); - }; - - const onQueryChange = (e: any) => { - const queryText = e.target.value; - setSearchTerm(queryText); - setUrlandReplaceHistory({ - searchString: queryText, - categoryId: selectedCategory, - subCategoryId: selectedSubCategory, - }); - }; - - const resetQuery = () => { - setSearchTerm(''); - setUrlandReplaceHistory({ searchString: '', categoryId: '', subCategoryId: '' }); - }; - - const onSubCategoryClick = useCallback( - (subCategory: string) => { - if (setSelectedSubCategory) setSelectedSubCategory(subCategory); - setUrlandPushHistory({ - categoryId: selectedCategory, - subCategoryId: subCategory, - }); - }, - [selectedCategory, setSelectedSubCategory, setUrlandPushHistory] - ); - - const selectedCategoryTitle = selectedCategory - ? categories.find((category) => category.id === selectedCategory)?.title - : undefined; - - const filteredPromotedList = useMemo(() => { - if (isLoading) return []; - const filteredList = searchTerm - ? list.filter((item) => - (localSearchRef.current!.search(searchTerm) as IntegrationCardItem[]) - .map((match) => match[searchIdField]) - .includes(item[searchIdField]) - ) - : list; - - return promoteFeaturedIntegrations(filteredList, selectedCategory); - }, [isLoading, list, localSearchRef, searchTerm, selectedCategory]); - - const splitSubcategories = ( - subcategories: CategoryFacet[] | undefined - ): { visibleSubCategories?: CategoryFacet[]; hiddenSubCategories?: CategoryFacet[] } => { - if (!subcategories) return {}; - else if (subcategories && subcategories?.length < MAX_SUBCATEGORIES_NUMBER) { - return { visibleSubCategories: subcategories, hiddenSubCategories: [] }; - } else if (subcategories && subcategories?.length >= MAX_SUBCATEGORIES_NUMBER) { - return { - visibleSubCategories: subcategories.slice(0, MAX_SUBCATEGORIES_NUMBER), - hiddenSubCategories: subcategories.slice(MAX_SUBCATEGORIES_NUMBER), - }; - } - return {}; - }; - - const splitSubcat = splitSubcategories(availableSubCategories); - const { visibleSubCategories } = splitSubcat; - const hiddenSubCategoriesItems = useMemo(() => { - return splitSubcat?.hiddenSubCategories?.map((subCategory) => { - return ( - { - onSubCategoryClick(subCategory.id); - closePopover(); - }} - > - {subCategory.title} - - ); - }); - }, [onSubCategoryClick, splitSubcat.hiddenSubCategories]); - - return ( - - - - - - onQueryChange(e)} - isClearable={true} - incremental={true} - fullWidth={true} - prepend={ - selectedCategoryTitle ? ( - - - Searching category: - - {selectedCategoryTitle} - - - ) : undefined - } - /> - {showIntegrationsSubcategories && availableSubCategories?.length ? : null} - {showIntegrationsSubcategories ? ( - - {visibleSubCategories?.map((subCategory) => ( - - onSubCategoryClick(subCategory.id)} - > - - - - ))} - {hiddenSubCategoriesItems?.length ? ( - - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - ) : null} - - ) : null} - {callout ? ( - <> - - {callout} - - ) : null} - - - {showMissingIntegrationMessage && ( - <> - - - - - )} - - - ); -}; - -interface ControlsColumnProps { - controls: ReactNode; - title: string | undefined; -} - -const ControlsColumn = ({ controls, title }: ControlsColumnProps) => { - let titleContent; - if (title) { - titleContent = ( - <> - -

{title}

-
- - - ); - } - return ( - - {titleContent} - {controls} - - ); -}; - -interface GridColumnProps { - list: IntegrationCardItem[]; - isLoading: boolean; - showMissingIntegrationMessage?: boolean; - showCardLabels?: boolean; -} - -const GridColumn = ({ - list, - showMissingIntegrationMessage = false, - showCardLabels = false, - isLoading, -}: GridColumnProps) => { - if (isLoading) return ; - - return ( - - {list.length ? ( - list.map((item) => { - return ( - .euiPopover, - & > .euiPopover > .euiPopover__anchor, - & > .euiPopover > .euiPopover__anchor > .euiCard { - height: 100%; - } - `} - > - - - ); - }) - ) : ( - - -

- {showMissingIntegrationMessage ? ( - - ) : ( - - )} -

-
-
- )} -
- ); -}; - -interface MissingIntegrationContentProps { - resetQuery: () => void; - setSelectedCategory: (category: ExtendedIntegrationCategory) => void; - setUrlandPushHistory: (params: IntegrationsURLParameters) => void; -} - -const MissingIntegrationContent = ({ - resetQuery, - setSelectedCategory, - setUrlandPushHistory, -}: MissingIntegrationContentProps) => { - const handleCustomInputsLinkClick = useCallback(() => { - resetQuery(); - setSelectedCategory('custom'); - setUrlandPushHistory({ - categoryId: 'custom', - subCategoryId: '', - }); - }, [resetQuery, setSelectedCategory, setUrlandPushHistory]); - - return ( - -

- - - - ), - forumLink: ( - - - - ), - }} - /> -

-
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx new file mode 100644 index 00000000000000..1466cd6cb9dc53 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/controls.tsx @@ -0,0 +1,165 @@ +/* + * 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 { ReactNode } from 'react'; +import React, { useCallback } from 'react'; +import { css } from '@emotion/react'; + +import { + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +import { Loading } from '../../../../components'; + +import type { IntegrationCardItem } from '../../../../../../../common/types/models'; + +import type { ExtendedIntegrationCategory } from '../../screens/home/category_facets'; + +import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages'; + +import { PackageCard } from '../package_card'; + +interface ControlsColumnProps { + controls: ReactNode; + title: string | undefined; +} + +export const ControlsColumn = ({ controls, title }: ControlsColumnProps) => { + let titleContent; + if (title) { + titleContent = ( + <> + +

{title}

+
+ + + ); + } + return ( + + {titleContent} + {controls} + + ); +}; + +interface GridColumnProps { + list: IntegrationCardItem[]; + isLoading: boolean; + showMissingIntegrationMessage?: boolean; + showCardLabels?: boolean; +} + +export const GridColumn = ({ + list, + showMissingIntegrationMessage = false, + showCardLabels = false, + isLoading, +}: GridColumnProps) => { + if (isLoading) return ; + + return ( + + {list.length ? ( + list.map((item) => { + return ( + .euiPopover, + & > .euiPopover > .euiPopover__anchor, + & > .euiPopover > .euiPopover__anchor > .euiCard { + height: 100%; + } + `} + > + + + ); + }) + ) : ( + + +

+ {showMissingIntegrationMessage ? ( + + ) : ( + + )} +

+
+
+ )} +
+ ); +}; + +interface MissingIntegrationContentProps { + resetQuery: () => void; + setSelectedCategory: (category: ExtendedIntegrationCategory) => void; + setUrlandPushHistory: (params: IntegrationsURLParameters) => void; +} + +export const MissingIntegrationContent = ({ + resetQuery, + setSelectedCategory, + setUrlandPushHistory, +}: MissingIntegrationContentProps) => { + const handleCustomInputsLinkClick = useCallback(() => { + resetQuery(); + setSelectedCategory('custom'); + setUrlandPushHistory({ + categoryId: 'custom', + subCategoryId: '', + }); + }, [resetQuery, setSelectedCategory, setUrlandPushHistory]); + + return ( + +

+ + + + ), + forumLink: ( + + + + ), + }} + /> +

+
+ ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx similarity index 97% rename from x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx rename to x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx index 1ff0c435693e2d..8aabb1771680ab 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import type { Props } from './package_list_grid'; -import { PackageListGrid } from './package_list_grid'; +import type { Props } from '.'; +import { PackageListGrid } from '.'; export default { component: PackageListGrid, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx new file mode 100644 index 00000000000000..d3d283accf4d04 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx @@ -0,0 +1,272 @@ +/* + * 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 { ReactNode, FunctionComponent } from 'react'; +import { useMemo } from 'react'; +import React, { useCallback, useState } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButton, + EuiButtonIcon, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +import { useLocalSearch, searchIdField } from '../../../../hooks'; + +import type { IntegrationCardItem } from '../../../../../../../common/types/models'; + +import type { + ExtendedIntegrationCategory, + CategoryFacet, +} from '../../screens/home/category_facets'; + +import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages'; + +import { ExperimentalFeaturesService } from '../../../../services'; + +import { promoteFeaturedIntegrations } from '../utils'; + +import { ControlsColumn, MissingIntegrationContent, GridColumn } from './controls'; +import { SearchBox } from './search_box'; + +export interface Props { + isLoading?: boolean; + controls?: ReactNode | ReactNode[]; + list: IntegrationCardItem[]; + searchTerm: string; + setSearchTerm: (search: string) => void; + selectedCategory: ExtendedIntegrationCategory; + setCategory: (category: ExtendedIntegrationCategory) => void; + categories: CategoryFacet[]; + setUrlandReplaceHistory: (params: IntegrationsURLParameters) => void; + setUrlandPushHistory: (params: IntegrationsURLParameters) => void; + callout?: JSX.Element | null; + // Props used only in AvailablePackages component: + showCardLabels?: boolean; + title?: string; + availableSubCategories?: CategoryFacet[]; + selectedSubCategory?: string; + setSelectedSubCategory?: (c: string | undefined) => void; + showMissingIntegrationMessage?: boolean; +} + +export const PackageListGrid: FunctionComponent = ({ + isLoading, + controls, + title, + list, + searchTerm, + setSearchTerm, + selectedCategory, + setCategory, + categories, + availableSubCategories, + setSelectedSubCategory, + selectedSubCategory, + setUrlandReplaceHistory, + setUrlandPushHistory, + showMissingIntegrationMessage = false, + callout, + showCardLabels = true, +}) => { + const localSearchRef = useLocalSearch(list); + + const [isPopoverOpen, setPopover] = useState(false); + + const MAX_SUBCATEGORIES_NUMBER = 6; + + const { showIntegrationsSubcategories } = ExperimentalFeaturesService.get(); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const resetQuery = () => { + setSearchTerm(''); + setUrlandReplaceHistory({ searchString: '', categoryId: '', subCategoryId: '' }); + }; + + const onSubCategoryClick = useCallback( + (subCategory: string) => { + if (setSelectedSubCategory) setSelectedSubCategory(subCategory); + setUrlandPushHistory({ + categoryId: selectedCategory, + subCategoryId: subCategory, + }); + }, + [selectedCategory, setSelectedSubCategory, setUrlandPushHistory] + ); + + const filteredPromotedList = useMemo(() => { + if (isLoading) return []; + const filteredList = searchTerm + ? list.filter((item) => + (localSearchRef.current!.search(searchTerm) as IntegrationCardItem[]) + .map((match) => match[searchIdField]) + .includes(item[searchIdField]) + ) + : list; + + return promoteFeaturedIntegrations(filteredList, selectedCategory); + }, [isLoading, list, localSearchRef, searchTerm, selectedCategory]); + + const splitSubcategories = ( + subcategories: CategoryFacet[] | undefined + ): { visibleSubCategories?: CategoryFacet[]; hiddenSubCategories?: CategoryFacet[] } => { + if (!subcategories) return {}; + else if (subcategories && subcategories?.length < MAX_SUBCATEGORIES_NUMBER) { + return { visibleSubCategories: subcategories, hiddenSubCategories: [] }; + } else if (subcategories && subcategories?.length >= MAX_SUBCATEGORIES_NUMBER) { + return { + visibleSubCategories: subcategories.slice(0, MAX_SUBCATEGORIES_NUMBER), + hiddenSubCategories: subcategories.slice(MAX_SUBCATEGORIES_NUMBER), + }; + } + return {}; + }; + + const splitSubcat = splitSubcategories(availableSubCategories); + const { visibleSubCategories } = splitSubcat; + const hiddenSubCategoriesItems = useMemo(() => { + return splitSubcat?.hiddenSubCategories?.map((subCategory) => { + return ( + { + onSubCategoryClick(subCategory.id); + closePopover(); + }} + icon={selectedSubCategory === subCategory.id ? 'check' : 'empty'} + > + {subCategory.title} + + ); + }); + }, [onSubCategoryClick, selectedSubCategory, splitSubcat?.hiddenSubCategories]); + + return ( + + + + + + + {showIntegrationsSubcategories && availableSubCategories?.length ? : null} + {showIntegrationsSubcategories ? ( + + {visibleSubCategories?.map((subCategory) => { + const isSelected = subCategory.id === selectedSubCategory; + return ( + + onSubCategoryClick(subCategory.id)} + > + + + + ); + })} + {hiddenSubCategoriesItems?.length ? ( + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + ) : null} + + ) : null} + {callout ? ( + <> + + {callout} + + ) : null} + + + {showMissingIntegrationMessage && ( + <> + + + + + )} + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx new file mode 100644 index 00000000000000..593ad3e60e82b3 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/search_box.tsx @@ -0,0 +1,134 @@ +/* + * 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 { FunctionComponent } from 'react'; +import React, { useMemo } from 'react'; + +import { EuiFieldSearch, EuiText, useEuiTheme, EuiIcon, EuiScreenReaderOnly } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import type { + ExtendedIntegrationCategory, + CategoryFacet, +} from '../../screens/home/category_facets'; + +import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages'; + +export interface Props { + searchTerm: string; + setSearchTerm: (search: string) => void; + selectedCategory: ExtendedIntegrationCategory; + setCategory: (category: ExtendedIntegrationCategory) => void; + categories: CategoryFacet[]; + availableSubCategories?: CategoryFacet[]; + setUrlandReplaceHistory: (params: IntegrationsURLParameters) => void; + selectedSubCategory?: string; + setSelectedSubCategory?: (c: string | undefined) => void; +} + +export const SearchBox: FunctionComponent = ({ + searchTerm, + setSearchTerm, + selectedCategory, + setCategory, + categories, + availableSubCategories, + setSelectedSubCategory, + selectedSubCategory, + setUrlandReplaceHistory, +}) => { + const { euiTheme } = useEuiTheme(); + + const onQueryChange = (e: any) => { + const queryText = e.target.value; + setSearchTerm(queryText); + setUrlandReplaceHistory({ + searchString: queryText, + categoryId: selectedCategory, + subCategoryId: selectedSubCategory, + }); + }; + + const selectedCategoryTitle = selectedCategory + ? categories.find((category) => category.id === selectedCategory)?.title + : undefined; + + const getCategoriesLabel = useMemo(() => { + const selectedSubCategoryTitle = + selectedSubCategory && availableSubCategories + ? availableSubCategories.find((subCat) => subCat.id === selectedSubCategory)?.title + : undefined; + + if (selectedCategoryTitle && selectedSubCategoryTitle) { + return `${selectedCategoryTitle}, ${selectedSubCategoryTitle}`; + } else if (selectedCategoryTitle) { + return `${selectedCategoryTitle}`; + } else return ''; + }, [availableSubCategories, selectedCategoryTitle, selectedSubCategory]); + + return ( + onQueryChange(e)} + isClearable={true} + incremental={true} + fullWidth={true} + prepend={ + selectedCategoryTitle ? ( + + + Searching category: + + {getCategoriesLabel} + + + ) : undefined + } + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx index c1190a66c70340..f13fd592daccf1 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx @@ -5,23 +5,20 @@ * 2.0. */ import React, { useState, useMemo } from 'react'; -import { useLocation, useParams, useHistory } from 'react-router-dom'; import { uniq, xorBy } from 'lodash'; import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common'; import type { IntegrationPreferenceType } from '../../../components/integration_preference'; -import { usePackages, useCategories, useStartServices } from '../../../../../hooks'; +import { usePackages, useCategories } from '../../../../../hooks'; import { useGetAppendCustomIntegrations, useGetReplacementCustomIntegrations, - useLink, } from '../../../../../hooks'; import { useMergeEprPackagesWithReplacements } from '../../../../../hooks/use_merge_epr_with_replacements'; -import type { CategoryParams } from '..'; -import { getParams, mapToCard } from '..'; +import { mapToCard } from '..'; import type { PackageList, PackageListItem } from '../../../../../types'; import { doesPackageHaveIntegrations } from '../../../../../services'; @@ -31,8 +28,6 @@ import { isIntegrationPolicyTemplate, } from '../../../../../../../../common/services'; -import { pagePathGetters } from '../../../../../constants'; - import type { IntegrationCardItem } from '../../../../../../../../common/types/models'; import { ALL_CATEGORY } from '../category_facets'; @@ -40,6 +35,8 @@ import type { CategoryFacet } from '../category_facets'; import { mergeCategoriesAndCount } from '../util'; +import { useBuildIntegrationsUrl } from './use_build_integrations_url'; + export interface IntegrationsURLParameters { searchString?: string; categoryId?: string; @@ -111,14 +108,17 @@ export const useAvailablePackages = () => { const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState< boolean | undefined >(undefined); - const { http } = useStartServices(); - const addBasePath = http.basePath.prepend; const { - selectedCategory: initialSelectedCategory, - selectedSubcategory: initialSubcategory, + initialSelectedCategory, + initialSubcategory, + setUrlandPushHistory, + setUrlandReplaceHistory, + getHref, + getAbsolutePath, searchParam, - } = getParams(useParams(), useLocation().search); + addBasePath, + } = useBuildIntegrationsUrl(); const [selectedCategory, setCategory] = useState(initialSelectedCategory); const [selectedSubCategory, setSelectedSubCategory] = useState( @@ -126,45 +126,6 @@ export const useAvailablePackages = () => { ); const [searchTerm, setSearchTerm] = useState(searchParam || ''); - const { getHref, getAbsolutePath } = useLink(); - const history = useHistory(); - - const buildUrl = ({ searchString, categoryId, subCategoryId }: IntegrationsURLParameters) => { - const url = pagePathGetters.integrations_all({ - category: categoryId ? categoryId : '', - subCategory: subCategoryId ? subCategoryId : '', - searchTerm: searchString ? searchString : '', - })[1]; - return url; - }; - - const setUrlandPushHistory = ({ - searchString, - categoryId, - subCategoryId, - }: IntegrationsURLParameters) => { - const url = buildUrl({ - categoryId, - searchString, - subCategoryId, - }); - history.push(url); - }; - - const setUrlandReplaceHistory = ({ - searchString, - categoryId, - subCategoryId, - }: IntegrationsURLParameters) => { - const url = buildUrl({ - categoryId, - searchString, - subCategoryId, - }); - // Use .replace so the browser's back button is not tied to single keystroke - history.replace(url); - }; - const { data: eprPackages, isLoading: isLoadingAllPackages, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_build_integrations_url.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_build_integrations_url.tsx new file mode 100644 index 00000000000000..4c2a45586844da --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_build_integrations_url.tsx @@ -0,0 +1,82 @@ +/* + * 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 { useLocation, useParams, useHistory } from 'react-router-dom'; + +import { useStartServices } from '../../../../../hooks'; +import { useLink } from '../../../../../hooks'; + +import type { CategoryParams } from '..'; +import { getParams } from '..'; + +import { pagePathGetters } from '../../../../../constants'; + +export interface IntegrationsURLParameters { + searchString?: string; + categoryId?: string; + subCategoryId?: string; +} + +export const useBuildIntegrationsUrl = () => { + const { http } = useStartServices(); + const addBasePath = http.basePath.prepend; + + const { + selectedCategory: initialSelectedCategory, + selectedSubcategory: initialSubcategory, + searchParam, + } = getParams(useParams(), useLocation().search); + + const { getHref, getAbsolutePath } = useLink(); + const history = useHistory(); + + const buildUrl = ({ searchString, categoryId, subCategoryId }: IntegrationsURLParameters) => { + const url = pagePathGetters.integrations_all({ + category: categoryId ? categoryId : '', + subCategory: subCategoryId ? subCategoryId : '', + searchTerm: searchString ? searchString : '', + })[1]; + return url; + }; + + const setUrlandPushHistory = ({ + searchString, + categoryId, + subCategoryId, + }: IntegrationsURLParameters) => { + const url = buildUrl({ + categoryId, + searchString, + subCategoryId, + }); + history.push(url); + }; + + const setUrlandReplaceHistory = ({ + searchString, + categoryId, + subCategoryId, + }: IntegrationsURLParameters) => { + const url = buildUrl({ + categoryId, + searchString, + subCategoryId, + }); + // Use .replace so the browser's back button is not tied to single keystroke + history.replace(url); + }; + + return { + initialSelectedCategory, + initialSubcategory, + setUrlandPushHistory, + setUrlandReplaceHistory, + getHref, + getAbsolutePath, + searchParam, + addBasePath, + }; +}; From a7cec2200c9b050a5409478d7dad0be291d89a60 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Wed, 1 Feb 2023 10:39:55 -0500 Subject: [PATCH 52/59] fix(slo): use fake timer in tests (#150066) --- .../historical_summary_client.test.ts.snap | 336 +++++++++--------- .../slo/historical_summary_client.test.ts | 5 + 2 files changed, 173 insertions(+), 168 deletions(-) diff --git a/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap b/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap index a05fded6706b78..323abf7c24d9f5 100644 --- a/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/__snapshots__/historical_summary_client.test.ts.snap @@ -2,12 +2,12 @@ exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 1`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.004449, + "consumed": 0.004019, "initial": 0.05, "isEstimated": true, - "remaining": 0.995551, + "remaining": 0.995981, }, "sliValue": 0.97, "status": "HEALTHY", @@ -16,12 +16,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 2`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.025879, + "consumed": 0.023374, "initial": 0.05, "isEstimated": true, - "remaining": 0.974121, + "remaining": 0.976626, }, "sliValue": 0.97, "status": "HEALTHY", @@ -30,12 +30,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 3`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.047306, + "consumed": 0.042725, "initial": 0.05, "isEstimated": true, - "remaining": 0.952694, + "remaining": 0.957275, }, "sliValue": 0.97, "status": "HEALTHY", @@ -44,12 +44,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 4`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.068729, + "consumed": 0.06208, "initial": 0.05, "isEstimated": true, - "remaining": 0.931271, + "remaining": 0.93792, }, "sliValue": 0.97, "status": "HEALTHY", @@ -58,12 +58,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 5`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.090171, + "consumed": 0.081433, "initial": 0.05, "isEstimated": true, - "remaining": 0.909829, + "remaining": 0.918567, }, "sliValue": 0.97, "status": "HEALTHY", @@ -72,12 +72,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 6`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.111593, + "consumed": 0.100784, "initial": 0.05, "isEstimated": true, - "remaining": 0.888407, + "remaining": 0.899216, }, "sliValue": 0.97, "status": "HEALTHY", @@ -86,12 +86,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 7`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.133038, + "consumed": 0.120137, "initial": 0.05, "isEstimated": true, - "remaining": 0.866962, + "remaining": 0.879863, }, "sliValue": 0.97, "status": "HEALTHY", @@ -100,12 +100,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 8`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.15444, + "consumed": 0.139494, "initial": 0.05, "isEstimated": true, - "remaining": 0.84556, + "remaining": 0.860506, }, "sliValue": 0.97, "status": "HEALTHY", @@ -114,12 +114,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 9`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.175896, + "consumed": 0.15887, "initial": 0.05, "isEstimated": true, - "remaining": 0.824104, + "remaining": 0.84113, }, "sliValue": 0.97, "status": "HEALTHY", @@ -128,12 +128,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 10`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.197304, + "consumed": 0.1782, "initial": 0.05, "isEstimated": true, - "remaining": 0.802696, + "remaining": 0.8218, }, "sliValue": 0.97, "status": "HEALTHY", @@ -142,12 +142,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 11`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.21876, + "consumed": 0.197546, "initial": 0.05, "isEstimated": true, - "remaining": 0.78124, + "remaining": 0.802454, }, "sliValue": 0.97, "status": "HEALTHY", @@ -156,12 +156,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 12`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.24016, + "consumed": 0.216933, "initial": 0.05, "isEstimated": true, - "remaining": 0.75984, + "remaining": 0.783067, }, "sliValue": 0.97, "status": "HEALTHY", @@ -170,12 +170,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 13`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.261569, + "consumed": 0.236292, "initial": 0.05, "isEstimated": true, - "remaining": 0.738431, + "remaining": 0.763708, }, "sliValue": 0.97, "status": "HEALTHY", @@ -184,12 +184,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 14`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.283019, + "consumed": 0.25563, "initial": 0.05, "isEstimated": true, - "remaining": 0.716981, + "remaining": 0.74437, }, "sliValue": 0.97, "status": "HEALTHY", @@ -198,12 +198,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 15`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.304465, + "consumed": 0.274977, "initial": 0.05, "isEstimated": true, - "remaining": 0.695535, + "remaining": 0.725023, }, "sliValue": 0.97, "status": "HEALTHY", @@ -212,12 +212,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 16`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.325866, + "consumed": 0.294298, "initial": 0.05, "isEstimated": true, - "remaining": 0.674134, + "remaining": 0.705702, }, "sliValue": 0.97, "status": "HEALTHY", @@ -226,12 +226,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 17`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.347293, + "consumed": 0.313653, "initial": 0.05, "isEstimated": true, - "remaining": 0.652707, + "remaining": 0.686347, }, "sliValue": 0.97, "status": "HEALTHY", @@ -240,12 +240,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Occurrences SLOs returns the summary 18`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.368727, + "consumed": 0.333025, "initial": 0.05, "isEstimated": true, - "remaining": 0.631273, + "remaining": 0.666975, }, "sliValue": 0.97, "status": "HEALTHY", @@ -254,12 +254,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 1`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.001488, + "consumed": 0.001344, "initial": 0.05, "isEstimated": false, - "remaining": 0.998512, + "remaining": 0.998656, }, "sliValue": 0.97, "status": "HEALTHY", @@ -268,12 +268,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 2`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.002976, + "consumed": 0.002688, "initial": 0.05, "isEstimated": false, - "remaining": 0.997024, + "remaining": 0.997312, }, "sliValue": 0.97, "status": "HEALTHY", @@ -282,12 +282,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 3`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.004464, + "consumed": 0.004032, "initial": 0.05, "isEstimated": false, - "remaining": 0.995536, + "remaining": 0.995968, }, "sliValue": 0.97, "status": "HEALTHY", @@ -296,12 +296,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 4`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.005952, + "consumed": 0.005376, "initial": 0.05, "isEstimated": false, - "remaining": 0.994048, + "remaining": 0.994624, }, "sliValue": 0.97, "status": "HEALTHY", @@ -310,12 +310,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 5`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.00744, + "consumed": 0.00672, "initial": 0.05, "isEstimated": false, - "remaining": 0.99256, + "remaining": 0.99328, }, "sliValue": 0.97, "status": "HEALTHY", @@ -324,12 +324,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 6`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.008929, + "consumed": 0.008065, "initial": 0.05, "isEstimated": false, - "remaining": 0.991071, + "remaining": 0.991935, }, "sliValue": 0.97, "status": "HEALTHY", @@ -338,12 +338,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 7`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.010417, + "consumed": 0.009409, "initial": 0.05, "isEstimated": false, - "remaining": 0.989583, + "remaining": 0.990591, }, "sliValue": 0.97, "status": "HEALTHY", @@ -352,12 +352,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 8`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.011905, + "consumed": 0.010753, "initial": 0.05, "isEstimated": false, - "remaining": 0.988095, + "remaining": 0.989247, }, "sliValue": 0.97, "status": "HEALTHY", @@ -366,12 +366,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 9`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.013393, + "consumed": 0.012097, "initial": 0.05, "isEstimated": false, - "remaining": 0.986607, + "remaining": 0.987903, }, "sliValue": 0.97, "status": "HEALTHY", @@ -380,12 +380,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 10`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.014881, + "consumed": 0.013441, "initial": 0.05, "isEstimated": false, - "remaining": 0.985119, + "remaining": 0.986559, }, "sliValue": 0.97, "status": "HEALTHY", @@ -394,12 +394,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 11`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.016369, + "consumed": 0.014785, "initial": 0.05, "isEstimated": false, - "remaining": 0.983631, + "remaining": 0.985215, }, "sliValue": 0.97, "status": "HEALTHY", @@ -408,12 +408,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 12`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.017857, + "consumed": 0.016129, "initial": 0.05, "isEstimated": false, - "remaining": 0.982143, + "remaining": 0.983871, }, "sliValue": 0.97, "status": "HEALTHY", @@ -422,12 +422,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 13`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.019345, + "consumed": 0.017473, "initial": 0.05, "isEstimated": false, - "remaining": 0.980655, + "remaining": 0.982527, }, "sliValue": 0.97, "status": "HEALTHY", @@ -436,12 +436,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 14`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.020833, + "consumed": 0.018817, "initial": 0.05, "isEstimated": false, - "remaining": 0.979167, + "remaining": 0.981183, }, "sliValue": 0.97, "status": "HEALTHY", @@ -450,12 +450,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 15`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.022321, + "consumed": 0.020161, "initial": 0.05, "isEstimated": false, - "remaining": 0.977679, + "remaining": 0.979839, }, "sliValue": 0.97, "status": "HEALTHY", @@ -464,12 +464,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 16`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.02381, + "consumed": 0.021505, "initial": 0.05, "isEstimated": false, - "remaining": 0.97619, + "remaining": 0.978495, }, "sliValue": 0.97, "status": "HEALTHY", @@ -478,12 +478,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 17`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.025298, + "consumed": 0.022849, "initial": 0.05, "isEstimated": false, - "remaining": 0.974702, + "remaining": 0.977151, }, "sliValue": 0.97, "status": "HEALTHY", @@ -492,12 +492,12 @@ Object { exports[`FetchHistoricalSummary Calendar Aligned and Timeslices SLOs returns the summary 18`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { - "consumed": 0.026786, + "consumed": 0.024194, "initial": 0.05, "isEstimated": false, - "remaining": 0.973214, + "remaining": 0.975806, }, "sliValue": 0.97, "status": "HEALTHY", @@ -506,7 +506,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 1`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -520,7 +520,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 2`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -534,7 +534,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 3`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -548,7 +548,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 4`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -562,7 +562,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 5`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -576,7 +576,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 6`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -590,7 +590,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 7`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -604,7 +604,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 8`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -618,7 +618,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 9`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -632,7 +632,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 10`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -646,7 +646,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 11`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -660,7 +660,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 12`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -674,7 +674,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 13`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -688,7 +688,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 14`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -702,7 +702,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 15`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -716,7 +716,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 16`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -730,7 +730,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 17`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -744,7 +744,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 18`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -758,7 +758,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 19`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -772,7 +772,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 20`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -786,7 +786,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 21`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -800,7 +800,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 22`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -814,7 +814,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 23`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -828,7 +828,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 24`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -842,7 +842,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 25`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -856,7 +856,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 26`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -870,7 +870,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 27`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -884,7 +884,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 28`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -898,7 +898,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 29`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -912,7 +912,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Occurrences SLOs returns the summary 30`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -926,7 +926,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 1`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -940,7 +940,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 2`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -954,7 +954,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 3`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -968,7 +968,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 4`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -982,7 +982,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 5`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -996,7 +996,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 6`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1010,7 +1010,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 7`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1024,7 +1024,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 8`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1038,7 +1038,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 9`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1052,7 +1052,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 10`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1066,7 +1066,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 11`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1080,7 +1080,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 12`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1094,7 +1094,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 13`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1108,7 +1108,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 14`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1122,7 +1122,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 15`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1136,7 +1136,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 16`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1150,7 +1150,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 17`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1164,7 +1164,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 18`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1178,7 +1178,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 19`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1192,7 +1192,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 20`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1206,7 +1206,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 21`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1220,7 +1220,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 22`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1234,7 +1234,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 23`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1248,7 +1248,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 24`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1262,7 +1262,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 25`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1276,7 +1276,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 26`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1290,7 +1290,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 27`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1304,7 +1304,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 28`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1318,7 +1318,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 29`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, @@ -1332,7 +1332,7 @@ Object { exports[`FetchHistoricalSummary Rolling and Timeslices SLOs returns the summary 30`] = ` Object { - "date": Any, + "date": Any, "errorBudget": Object { "consumed": 0.6, "initial": 0.05, diff --git a/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts b/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts index b55ca69ed3bb28..766af6be9417f9 100644 --- a/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts +++ b/x-pack/plugins/observability/server/services/slo/historical_summary_client.test.ts @@ -104,9 +104,14 @@ describe('FetchHistoricalSummary', () => { let esClientMock: ElasticsearchClientMock; beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date('2023-01-18T15:00:00.000Z')); esClientMock = elasticsearchServiceMock.createElasticsearchClient(); }); + afterAll(() => { + jest.useRealTimers(); + }); + describe('Rolling and Occurrences SLOs', () => { it('returns the summary', async () => { const slo = createSLO({ From 30ed57628e00ad20ea0acf2cb603192cfbfb8dd2 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 1 Feb 2023 16:55:24 +0100 Subject: [PATCH 53/59] fix banner overlap in dashboard embed and fullscreen mode (#150012) ## Summary Fix https://github.com/elastic/kibana/issues/116103, https://github.com/elastic/kibana/issues/149112 Better version of the initial attempt https://github.com/elastic/kibana/pull/149197 Fixes banner overlap in dashboard embed and fullscreen mode. The fix follows `chaos`'s suggestion https://github.com/elastic/kibana/issues/116103#issuecomment-957599532. The bug root cause is described in https://github.com/elastic/kibana/issues/116103#issuecomment-953252893 and https://github.com/elastic/kibana/issues/116103#issuecomment-957369465 To reproduce and test: Simple way to reproduce locally (no need for an iframe): 1. Add to kibana.dev.yml ``` xpack.banners: placement: 'top' textContent: 'P-System' textColor: '#FFFFFF' backgroundColor: '#FF0000' ``` 2. Enable platinum license trial through Stack Management > Stack > License Management (banners are not available in basic) 3. Fullscreen: View a dashboard and switch to fullscreen mode 4. Embed mode: Open a dashboard, copy the URL, open that URL in a new window appending `embed=true` to the URL. e.g.: `http://localhost:5601/jwy/app/dashboards#/view/edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b?embed=true&_g=()` fixed fullscreen (no panel title overlap): ![Screenshot 2023-02-01 at 11 29 23](https://user-images.githubusercontent.com/7784120/216018466-b49ef056-48fd-47d4-a503-bcc17ff87c3e.png) fixed embed (no controls overlap): ![Screenshot 2023-02-01 at 11 29 39](https://user-images.githubusercontent.com/7784120/216018547-a4735ee2-5306-4ae6-bd25-a79d651691bf.png) --- src/core/public/styles/rendering/_base.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/public/styles/rendering/_base.scss b/src/core/public/styles/rendering/_base.scss index b64595e69a7910..9d4296ca3b4efa 100644 --- a/src/core/public/styles/rendering/_base.scss +++ b/src/core/public/styles/rendering/_base.scss @@ -62,6 +62,8 @@ @include kbnAffordForHeader($kbnHeaderOffset); &.kbnBody--hasHeaderBanner { + padding-top: $kbnHeaderBannerHeight; + @include kbnAffordForHeader($kbnHeaderOffsetWithBanner); // Prevents banners from covering full screen data grids From 4f3760452462b0423ffe1dfe078cc08b7b3c6154 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 1 Feb 2023 17:00:50 +0100 Subject: [PATCH 54/59] Enable GitHub Code Scanning on the 7.17 branch (#150035) --- .github/workflows/codeql.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 806c6cbf07a0ac..f221a780c74229 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,14 +17,13 @@ jobs: fail-fast: false matrix: language: [ 'javascript' ] - # branch: [ 'main', '7.17' ] + branch: [ 'main', '7.17' ] steps: - name: Checkout repository uses: actions/checkout@v3 - # TODO: Enable once a `.github/codeql/codeql-config.yml` file has been committed to 7.17 - # with: - # ref: ${{ matrix.branch }} + with: + ref: ${{ matrix.branch }} - name: Initialize CodeQL uses: github/codeql-action/init@v2 From 0a6edd85018855e61d1c753fb14d7c8937132b1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 17:08:57 +0100 Subject: [PATCH 55/59] Update react-query to ^4.23.0 (main) (#148944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@tanstack/react-query](https://tanstack.com/query) ([source](https://togithub.com/tanstack/query)) | [`^4.20.9` -> `^4.23.0`](https://renovatebot.com/diffs/npm/@tanstack%2freact-query/4.22.0/4.23.0) | [![age](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query/4.23.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query/4.23.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query/4.23.0/compatibility-slim/4.22.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query/4.23.0/confidence-slim/4.22.0)](https://docs.renovatebot.com/merge-confidence/) | | [@tanstack/react-query-devtools](https://tanstack.com/query) ([source](https://togithub.com/tanstack/query)) | [`^4.20.9` -> `^4.23.0`](https://renovatebot.com/diffs/npm/@tanstack%2freact-query-devtools/4.22.0/4.23.0) | [![age](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query-devtools/4.23.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query-devtools/4.23.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query-devtools/4.23.0/compatibility-slim/4.22.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/@tanstack%2freact-query-devtools/4.23.0/confidence-slim/4.22.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
tanstack/query ### [`v4.23.0`](https://togithub.com/TanStack/query/releases/tag/v4.23.0) [Compare Source](https://togithub.com/tanstack/query/compare/v4.22.4...v4.23.0) Version 4.23.0 - 1/24/2023, 10:53 AM ##### Changes ##### Feat - client components: add `use client` directive at the top of files having client components ([#​4738](https://togithub.com/tanstack/query/issues/4738)) ([`f57c8dc`](https://togithub.com/tanstack/query/commit/f57c8dc1)) by Girish Sontakke ##### Docs - add readme.md to react adapter ([`ea67377`](https://togithub.com/tanstack/query/commit/ea673770)) by Dominik Dorfmeister - add QueryClient import ([#​4856](https://togithub.com/tanstack/query/issues/4856)) ([`69a7d72`](https://togithub.com/tanstack/query/commit/69a7d72d)) by Joël Kuijper - useMutation: correct docs for mutate function callbacks ([#​4601](https://togithub.com/tanstack/query/issues/4601)) ([`4ac7c1a`](https://togithub.com/tanstack/query/commit/4ac7c1a8)) by Qz ##### Packages - [@​tanstack/react-query-devtools](https://togithub.com/tanstack/react-query-devtools)[@​4](https://togithub.com/4).23.0 - [@​tanstack/react-query-persist-client](https://togithub.com/tanstack/react-query-persist-client)[@​4](https://togithub.com/4).23.0 - [@​tanstack/react-query](https://togithub.com/tanstack/react-query)[@​4](https://togithub.com/4).23.0 ### [`v4.22.4`](https://togithub.com/TanStack/query/releases/tag/v4.22.4) [Compare Source](https://togithub.com/tanstack/query/compare/v4.22.3...v4.22.4) Version 4.22.4 - 1/22/2023, 3:57 PM ##### Changes ##### Fix - core: do not call mutate callbacks if mutation started after unmount ([#​4848](https://togithub.com/tanstack/query/issues/4848)) ([`901e826`](https://togithub.com/tanstack/query/commit/901e826f)) by Jan ##### Packages - [@​tanstack/query-core](https://togithub.com/tanstack/query-core)[@​4](https://togithub.com/4).22.4 - [@​tanstack/react-query](https://togithub.com/tanstack/react-query)[@​4](https://togithub.com/4).22.4 - [@​tanstack/query-persist-client-core](https://togithub.com/tanstack/query-persist-client-core)[@​4](https://togithub.com/4).22.4 - [@​tanstack/query-async-storage-persister](https://togithub.com/tanstack/query-async-storage-persister)[@​4](https://togithub.com/4).22.4 - [@​tanstack/query-broadcast-client-experimental](https://togithub.com/tanstack/query-broadcast-client-experimental)[@​4](https://togithub.com/4).22.4 - [@​tanstack/query-sync-storage-persister](https://togithub.com/tanstack/query-sync-storage-persister)[@​4](https://togithub.com/4).22.4 - [@​tanstack/react-query-devtools](https://togithub.com/tanstack/react-query-devtools)[@​4](https://togithub.com/4).22.4 - [@​tanstack/react-query-persist-client](https://togithub.com/tanstack/react-query-persist-client)[@​4](https://togithub.com/4).22.4 - [@​tanstack/solid-query](https://togithub.com/tanstack/solid-query)[@​4](https://togithub.com/4).22.4 - [@​tanstack/svelte-query](https://togithub.com/tanstack/svelte-query)[@​4](https://togithub.com/4).22.4 - [@​tanstack/vue-query](https://togithub.com/tanstack/vue-query)[@​4](https://togithub.com/4).22.4 ### [`v4.22.3`](https://togithub.com/TanStack/query/releases/tag/v4.22.3) [Compare Source](https://togithub.com/tanstack/query/compare/v4.22.0...v4.22.3) Version 4.22.3 - 1/21/2023, 2:54 PM ##### Changes ##### Fix - svelte-query: Fix createMutation for functions that take no arguments ([#​4847](https://togithub.com/tanstack/query/issues/4847)) ([`4f515de`](https://togithub.com/tanstack/query/commit/4f515dec)) by Lachlan Collins ##### Chore - fix duplicated example package name ([#​4823](https://togithub.com/tanstack/query/issues/4823)) ([`365fdf3`](https://togithub.com/tanstack/query/commit/365fdf3b)) by Michal Tecza - Add sandbox.config.json ([#​4812](https://togithub.com/tanstack/query/issues/4812)) ([`e33bda3`](https://togithub.com/tanstack/query/commit/e33bda31)) by Lachlan Collins ##### Docs - useMutation: clarify `mutationFn` option default ([#​4837](https://togithub.com/tanstack/query/issues/4837)) ([`61c3d94`](https://togithub.com/tanstack/query/commit/61c3d94a)) by [@​louis-young](https://togithub.com/louis-young) - clarify the documentation on running examples ([#​4818](https://togithub.com/tanstack/query/issues/4818)) ([`07f144a`](https://togithub.com/tanstack/query/commit/07f144a2)) by Michal Tecza - update tkdodos blog ([#​4820](https://togithub.com/tanstack/query/issues/4820)) ([`532b90a`](https://togithub.com/tanstack/query/commit/532b90ac)) by Dominik Dorfmeister - svelte-query: Add recommended defaults to prefetchQuery setup ([#​4815](https://togithub.com/tanstack/query/issues/4815)) ([`86161ca`](https://togithub.com/tanstack/query/commit/86161ca6)) by Lachlan Collins - fix typo (quey -> query) ([#​4813](https://togithub.com/tanstack/query/issues/4813)) ([`832d4fb`](https://togithub.com/tanstack/query/commit/832d4fb0)) by Masaki Koyanagi - svelte-query: Rework SvelteKit setup ([#​4811](https://togithub.com/tanstack/query/issues/4811)) ([`2cd92ef`](https://togithub.com/tanstack/query/commit/2cd92ef3)) by Lachlan Collins - clarify interaction of query filter predicates with other criteria ([#​4532](https://togithub.com/tanstack/query/issues/4532)) ([`3a3d871`](https://togithub.com/tanstack/query/commit/3a3d871a)) by Ben Longo - Update devtools.md with note that mutations are not tracked ([#​4810](https://togithub.com/tanstack/query/issues/4810)) ([`6772333`](https://togithub.com/tanstack/query/commit/67723337)) by Joseph Markus - svelte-query: Expand SSR docs ([#​4809](https://togithub.com/tanstack/query/issues/4809)) ([`c05bb91`](https://togithub.com/tanstack/query/commit/c05bb910)) by Lachlan Collins ##### Test - stabilize various tests ([#​4825](https://togithub.com/tanstack/query/issues/4825)) ([`ff77512`](https://togithub.com/tanstack/query/commit/ff77512b)) by Michal Tecza ##### Packages - [@​tanstack/react-query](https://togithub.com/tanstack/react-query)[@​4](https://togithub.com/4).22.3 - [@​tanstack/svelte-query](https://togithub.com/tanstack/svelte-query)[@​4](https://togithub.com/4).22.3 - [@​tanstack/react-query-devtools](https://togithub.com/tanstack/react-query-devtools)[@​4](https://togithub.com/4).22.3 - [@​tanstack/react-query-persist-client](https://togithub.com/tanstack/react-query-persist-client)[@​4](https://togithub.com/4).22.3
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/elastic/kibana). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- yarn.lock | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 884f16533f16ba..f6d3863a812bc3 100644 --- a/package.json +++ b/package.json @@ -421,8 +421,8 @@ "@opentelemetry/semantic-conventions": "^1.4.0", "@reduxjs/toolkit": "1.7.2", "@slack/webhook": "^5.0.4", - "@tanstack/react-query": "^4.20.9", - "@tanstack/react-query-devtools": "^4.20.9", + "@tanstack/react-query": "^4.23.0", + "@tanstack/react-query-devtools": "^4.23.0", "@turf/along": "6.0.1", "@turf/area": "6.0.1", "@turf/bbox": "6.0.1", diff --git a/yarn.lock b/yarn.lock index ecab1493084499..76936463642e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6244,26 +6244,26 @@ dependencies: remove-accents "0.4.2" -"@tanstack/query-core@4.22.0": - version "4.22.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.22.0.tgz#7a786fcea64e229ed5d4308093dd644cdfaa895e" - integrity sha512-OeLyBKBQoT265f5G9biReijeP8mBxNFwY7ZUu1dKL+YzqpG5q5z7J/N1eT8aWyKuhyDTiUHuKm5l+oIVzbtrjw== +"@tanstack/query-core@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.24.4.tgz#6fe78777286fdd805ac319c7c743df4935e18ee2" + integrity sha512-9dqjv9eeB6VHN7lD3cLo16ZAjfjCsdXetSAD5+VyKqLUvcKTL0CklGQRJu+bWzdrS69R6Ea4UZo8obHYZnG6aA== -"@tanstack/react-query-devtools@^4.20.9": - version "4.22.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.22.0.tgz#e39f04e55428ff6ce2b2796f3171db170afcb2a8" - integrity sha512-YeYFBnfqvb+ZlA0IiJqiHNNSzepNhI1p2o9i8NlhQli9+Zrn230M47OBaBUs8qr3DD1dC2zGB1Dis50Ktz8gAA== +"@tanstack/react-query-devtools@^4.23.0": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.24.4.tgz#5f0bd45f929950ff2b56a1a3ed18a006780e3495" + integrity sha512-4mldcR99QDX8k94I+STM9gPsYF+FDAD2EQJvHtxR2HrDNegbfmY474xuW0QUZaNW/vJi09Gak6b6Vy2INWhL6w== dependencies: "@tanstack/match-sorter-utils" "^8.7.0" superjson "^1.10.0" use-sync-external-store "^1.2.0" -"@tanstack/react-query@^4.20.9": - version "4.22.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.22.0.tgz#aaa4b41a6d306be6958018c74a8a3bb3e9f1924c" - integrity sha512-P9o+HjG42uB/xHR6dMsJaPhtZydSe4v0xdG5G/cEj1oHZAXelMlm67/rYJNQGKgBamKElKogj+HYGF+NY2yHYg== +"@tanstack/react-query@^4.23.0": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.24.4.tgz#79e892edac33d8aa394795390c0f79e4a8c9be4d" + integrity sha512-RpaS/3T/a3pHuZJbIAzAYRu+1nkp+/enr9hfRXDS/mojwx567UiMksoqW4wUFWlwIvWTXyhot2nbIipTKEg55Q== dependencies: - "@tanstack/query-core" "4.22.0" + "@tanstack/query-core" "4.24.4" use-sync-external-store "^1.2.0" "@testim/chrome-version@^1.1.3": From f002889ccace11a65be793512e8e3791273df013 Mon Sep 17 00:00:00 2001 From: Or Ouziel Date: Wed, 1 Feb 2023 18:28:44 +0200 Subject: [PATCH 56/59] [Fleet] Create a new `UIExtensionPoint` to replace the integration define step (#149653) --- .../single_page_layout/index.tsx | 25 ++++++++- .../edit_package_policy_page/index.test.tsx | 56 ++++++++++++++++--- .../edit_package_policy_page/index.tsx | 31 +++++++++- .../fleet/public/types/ui_extensions.ts | 27 ++++++++- 4 files changed, 126 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index cd5b997d9c788c..9a1c943763bd86 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -267,6 +267,29 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ ); const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); + const replaceDefineStepView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-policy-replace-define-step' + ); + + if (replaceDefineStepView && extensionView) { + throw new Error( + "'package-policy-create' and 'package-policy-replace-define-step' cannot both be registered as UI extensions" + ); + } + + const replaceStepConfigurePackagePolicy = replaceDefineStepView && packageInfo?.name && ( + + + + ); const stepConfigurePackagePolicy = useMemo( () => @@ -329,7 +352,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ defaultMessage: 'Configure integration', }), 'data-test-subj': 'dataCollectionSetupStep', - children: stepConfigurePackagePolicy, + children: replaceStepConfigurePackagePolicy || stepConfigurePackagePolicy, }, { title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx index c295cdd41cecb3..303629a99bb692 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx @@ -331,11 +331,42 @@ describe('edit package policy page', () => { expect(useStartServices().application.navigateToUrl).not.toHaveBeenCalled(); }); - it('should show ready for upgrade if package useLatestPackageVersion and no conflicts', async () => { - (useUIExtension as MockFn).mockReturnValue({ - useLatestPackageVersion: true, - Component: TestComponent, + it("throws when both 'package-policy-edit' and 'package-policy-replace-define-step' are defined", async () => { + (useUIExtension as MockFn) + .mockReturnValueOnce({ + view: 'package-policy-edit', + Component: TestComponent, + }) + .mockReturnValueOnce({ + view: 'package-policy-replace-define-step', + Component: TestComponent, + }) + .mockReturnValueOnce({ + view: 'package-policy-edit-tabs', + Component: TestComponent, + }); + + render(); + + await waitFor(() => { + expect(renderResult.getByTestId('euiErrorBoundary')).toBeVisible(); }); + }); + + it('should show ready for upgrade if package useLatestPackageVersion and no conflicts', async () => { + (useUIExtension as MockFn) + .mockReturnValueOnce({ + view: 'package-policy-edit', + useLatestPackageVersion: true, + Component: TestComponent, + }) + .mockReturnValueOnce(undefined) + .mockReturnValueOnce({ + view: 'package-policy-edit-tabs', + useLatestPackageVersion: true, + Component: TestComponent, + }); + (sendUpgradePackagePolicyDryRun as MockFn).mockResolvedValue({ data: [ { @@ -357,10 +388,19 @@ describe('edit package policy page', () => { }); it('should show review field conflicts if package useLatestPackageVersion and has conflicts', async () => { - (useUIExtension as MockFn).mockReturnValue({ - useLatestPackageVersion: true, - Component: TestComponent, - }); + (useUIExtension as MockFn) + .mockReturnValueOnce({ + view: 'package-policy-edit', + useLatestPackageVersion: true, + Component: TestComponent, + }) + .mockReturnValueOnce(undefined) + .mockReturnValueOnce({ + view: 'package-policy-edit-tabs', + useLatestPackageVersion: true, + Component: TestComponent, + }); + (sendUpgradePackagePolicyDryRun as MockFn).mockResolvedValue({ data: [ { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index afab7a48ebb764..b066457b0aa2e3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -35,7 +35,7 @@ import { } from '../../../../integrations/hooks'; import { Loading, - Error, + Error as ErrorComponent, ExtensionWrapper, EuiButtonWithTooltip, DevtoolsRequestFlyoutButton, @@ -240,10 +240,21 @@ export const EditPackagePolicyForm = memo<{ }; const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); + const replaceDefineStepView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-policy-replace-define-step' + ); const extensionTabsView = useUIExtension( packagePolicy.package?.name ?? '', 'package-policy-edit-tabs' ); + + if (replaceDefineStepView && extensionView) { + throw new Error( + "'package-policy-create' and 'package-policy-replace-define-step' cannot both be registered as UI extensions" + ); + } + const tabsViews = extensionTabsView?.tabs; const [selectedTab, setSelectedTab] = useState(0); @@ -339,6 +350,20 @@ export const EditPackagePolicyForm = memo<{ ] ); + const replaceConfigurePackage = replaceDefineStepView && originalPackagePolicy && packageInfo && ( + + + + ); + const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } = ExperimentalFeaturesService.get(); @@ -361,7 +386,7 @@ export const EditPackagePolicyForm = memo<{ {isLoadingData ? ( ) : loadingError || !agentPolicy || !packageInfo ? ( - )} - {configurePackage} + {replaceConfigurePackage || configurePackage} {/* Extra space to accomodate the EuiBottomBar height */} diff --git a/x-pack/plugins/fleet/public/types/ui_extensions.ts b/x-pack/plugins/fleet/public/types/ui_extensions.ts index fc8726e7b813ad..53ae5322f0d9d7 100644 --- a/x-pack/plugins/fleet/public/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/types/ui_extensions.ts @@ -10,7 +10,9 @@ import type { ComponentType, LazyExoticComponent } from 'react'; import type { FleetServerAgentComponentUnit } from '../../common/types/models/agent'; -import type { Agent, NewPackagePolicy, PackageInfo, PackagePolicy } from '.'; +import type { PackagePolicyValidationResults } from '../services'; + +import type { Agent, AgentPolicy, NewPackagePolicy, PackageInfo, PackagePolicy } from '.'; /** Register a Fleet UI extension */ export type UIExtensionRegistrationCallback = (extensionPoint: UIExtensionPoint) => void; @@ -20,6 +22,22 @@ export interface UIExtensionsStorage { [key: string]: Partial>; } +/** + * UI Component Extension is used to replace the Define Step on + * the pages displaying the ability to edit/create an Integration Policy + */ +export type PackagePolicyReplaceDefineStepExtensionComponent = + ComponentType; + +export type PackagePolicyReplaceDefineStepExtensionComponentProps = ( + | (PackagePolicyEditExtensionComponentProps & { isEditPage: true }) + | (PackagePolicyCreateExtensionComponentProps & { isEditPage: false }) +) & { + validationResults?: PackagePolicyValidationResults; + agentPolicy?: AgentPolicy; + packageInfo: PackageInfo; +}; + /** * UI Component Extension is used on the pages displaying the ability to edit an * Integration Policy @@ -73,6 +91,12 @@ export interface PackageGenericErrorsListProps { packageErrors: FleetServerAgentComponentUnit[]; } +export interface PackagePolicyReplaceDefineStepExtension { + package: string; + view: 'package-policy-replace-define-step'; + Component: LazyExoticComponent; +} + /** Extension point registration contract for Integration Policy Edit views */ export interface PackagePolicyEditExtension { package: string; @@ -184,6 +208,7 @@ export interface AgentEnrollmentFlyoutFinalStepExtension { /** Fleet UI Extension Point */ export type UIExtensionPoint = + | PackagePolicyReplaceDefineStepExtension | PackagePolicyEditExtension | PackagePolicyResponseExtension | PackagePolicyEditTabsExtension From e8eb04420e33b603ad66bb0fe2c90bca58bbe0a5 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Wed, 1 Feb 2023 11:24:08 -0600 Subject: [PATCH 57/59] [Enterprise Search] Replace Engines UI Settings feature flag (#149996) ## Summary Replaced the Engines UI Settings feature flag with product access field from the client config. Then removed the Engines UI Setting. We opted for a backend feature flag instead of the UI setting so that a single feature flag could be used to gate both the UI and the API for engines for the 8.7 release, since this work will not be completed until 8.8. --- .../server/collectors/management/schema.ts | 4 ---- .../server/collectors/management/types.ts | 1 - src/plugins/telemetry/schema/oss_plugins.json | 6 ------ .../common/__mocks__/initial_app_data.ts | 1 + .../enterprise_search/common/types/index.ts | 1 + .../common/ui_settings_keys.ts | 1 - .../__mocks__/kea_logic/kibana_logic.mock.ts | 1 + .../components/engines/engines_router.tsx | 5 ++--- .../public/applications/index.tsx | 1 + .../applications/shared/layout/nav.test.tsx | 16 ++++++++++------ .../public/applications/shared/layout/nav.tsx | 5 ++--- .../server/lib/check_access.test.ts | 11 +++++++++++ .../server/lib/check_access.ts | 6 ++++-- .../lib/enterprise_search_config_api.test.ts | 3 +++ .../server/lib/enterprise_search_config_api.ts | 1 + .../enterprise_search/server/ui_settings.ts | 18 +----------------- 16 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index ec389fda1f084f..14d104d84e334f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -570,8 +570,4 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'enterpriseSearch:enableEnginesSection': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 6a0743256465f7..8103c0e93f3018 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -152,5 +152,4 @@ export interface UsageStats { 'securitySolution:enableGroupedNav': boolean; 'securitySolution:showRelatedIntegrations': boolean; 'visualization:visualize:legacyGaugeChartsLibrary': boolean; - 'enterpriseSearch:enableEnginesSection': boolean; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 1219d8c7aad4e3..ddc16fba2319a4 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9113,12 +9113,6 @@ "_meta": { "description": "Non-default value of setting." } - }, - "enterpriseSearch:enableEnginesSection": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } } } }, diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index dd6dd58d02f70c..02a9cf81758976 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -29,6 +29,7 @@ export const DEFAULT_INITIAL_APP_DATA = { }, access: { hasAppSearchAccess: true, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }, appSearch: { diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index 2e17862edaaf47..e1b0f2045893ee 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -32,6 +32,7 @@ export interface ConfiguredLimits { export interface ProductAccess { hasAppSearchAccess: boolean; + hasSearchEnginesAccess: boolean; hasWorkplaceSearchAccess: boolean; } diff --git a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts index 9438c0a803f988..6a2a189e0cff97 100644 --- a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts +++ b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts @@ -6,4 +6,3 @@ */ export const enterpriseSearchFeatureId = 'enterpriseSearch'; -export const enableEnginesSection = 'enterpriseSearch:enableEnginesSection'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index dd654fb8357af7..4689317cfd61a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -29,6 +29,7 @@ export const mockKibanaValues = { navigateToUrl: jest.fn(), productAccess: { hasAppSearchAccess: true, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }, uiSettings: uiSettingsServiceMock.createStartContract(), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.tsx index 10687556a1e336..fdbc4e544c1352 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_router.tsx @@ -10,7 +10,6 @@ import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { enableEnginesSection } from '../../../../../common/ui_settings_keys'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINES_PATH, ENGINE_PATH } from '../../routes'; @@ -20,8 +19,8 @@ import { NotFound } from '../not_found'; import { EnginesList } from './engines_list'; export const EnginesRouter: React.FC = () => { - const { uiSettings } = useValues(KibanaLogic); - const enginesSectionEnabled = uiSettings?.get(enableEnginesSection, false); + const { productAccess } = useValues(KibanaLogic); + const enginesSectionEnabled = productAccess.hasSearchEnginesAccess; if (!enginesSectionEnabled) { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 1d6de5d5b3b93b..2b25fabe056f03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -45,6 +45,7 @@ export const renderApp = ( const noProductAccess: ProductAccess = { hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }; const productAccess = data.access || noProductAccess; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 103d24ebbe6159..17ce1fce0512ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -15,8 +15,6 @@ import { EuiSideNavItemType } from '@elastic/eui'; import { ProductAccess } from '../../../../common/types'; -import { enableEnginesSection } from '../../../../common/ui_settings_keys'; - import { useEnterpriseSearchNav, useEnterpriseSearchEngineNav } from './nav'; describe('useEnterpriseSearchContentNav', () => { @@ -28,6 +26,7 @@ describe('useEnterpriseSearchContentNav', () => { it('returns an array of top-level Enterprise Search nav items', () => { const fullProductAccess: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }; setMockValues({ productAccess: fullProductAccess }); @@ -93,12 +92,12 @@ describe('useEnterpriseSearchContentNav', () => { name: 'Search', }, ]); - expect(mockKibanaValues.uiSettings.get).toHaveBeenCalledWith(enableEnginesSection, false); }); it('excludes legacy products when the user has no access to them', () => { const noProductAccess: ProductAccess = { hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }; @@ -129,6 +128,7 @@ describe('useEnterpriseSearchContentNav', () => { it('excludes App Search when the user has no access to it', () => { const workplaceSearchProductAccess: ProductAccess = { hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }; @@ -163,6 +163,7 @@ describe('useEnterpriseSearchContentNav', () => { it('excludes Workplace Search when the user has no access to it', () => { const appSearchProductAccess: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }; @@ -197,6 +198,7 @@ describe('useEnterpriseSearchContentNav', () => { it('excludes engines when feature flag is off', () => { const fullProductAccess: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }; setMockValues({ productAccess: fullProductAccess }); @@ -209,12 +211,12 @@ describe('useEnterpriseSearchContentNav', () => { describe('useEnterpriseSearchContentNav Engines feature flag', () => { beforeEach(() => { jest.clearAllMocks(); - mockKibanaValues.uiSettings.get.mockReturnValue(true); }); it('returns an array of top-level Enterprise Search nav items', () => { const fullProductAccess: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: true, hasWorkplaceSearchAccess: true, }; setMockValues({ productAccess: fullProductAccess }); @@ -286,12 +288,12 @@ describe('useEnterpriseSearchContentNav Engines feature flag', () => { name: 'Standalone Experiences', }, ]); - expect(mockKibanaValues.uiSettings.get).toHaveBeenCalledWith(enableEnginesSection, false); }); it('excludes standalone experiences when the user has no access to them', () => { const fullProductAccess: ProductAccess = { hasAppSearchAccess: false, + hasSearchEnginesAccess: true, hasWorkplaceSearchAccess: false, }; setMockValues({ productAccess: fullProductAccess }); @@ -302,6 +304,7 @@ describe('useEnterpriseSearchContentNav Engines feature flag', () => { it('excludes App Search when the user has no access to it', () => { const fullProductAccess: ProductAccess = { hasAppSearchAccess: false, + hasSearchEnginesAccess: true, hasWorkplaceSearchAccess: true, }; setMockValues({ productAccess: fullProductAccess }); @@ -324,6 +327,7 @@ describe('useEnterpriseSearchContentNav Engines feature flag', () => { it('excludes Workplace Search when the user has no access to it', () => { const fullProductAccess: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: true, hasWorkplaceSearchAccess: false, }; setMockValues({ productAccess: fullProductAccess }); @@ -351,6 +355,7 @@ describe('useEnterpriseSearchEngineNav', () => { mockKibanaValues.uiSettings.get.mockReturnValue(true); const fullProductAccess: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: true, hasWorkplaceSearchAccess: true, }; setMockValues({ productAccess: fullProductAccess }); @@ -424,7 +429,6 @@ describe('useEnterpriseSearchEngineNav', () => { name: 'Standalone Experiences', }, ]); - expect(mockKibanaValues.uiSettings.get).toHaveBeenCalledWith(enableEnginesSection, false); }); it('returns selected engine sub nav items', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index 8ffb822352324e..a6e550e7c58b24 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -19,7 +19,6 @@ import { SEARCH_EXPERIENCES_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../../../../common/constants'; -import { enableEnginesSection } from '../../../../common/ui_settings_keys'; import { ENGINES_PATH, SEARCH_INDICES_PATH, @@ -31,9 +30,9 @@ import { KibanaLogic } from '../kibana'; import { generateNavLink } from './nav_link_helpers'; export const useEnterpriseSearchNav = () => { - const { productAccess, uiSettings } = useValues(KibanaLogic); + const { productAccess } = useValues(KibanaLogic); - const enginesSectionEnabled = uiSettings?.get(enableEnginesSection, false); + const enginesSectionEnabled = productAccess.hasSearchEnginesAccess; const navItems: Array> = [ { diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index 3da63a63828ae5..d2be503dbdebde 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -59,6 +59,7 @@ describe('checkAccess', () => { }; expect(await checkAccess({ ...mockDependencies, security })).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -71,6 +72,7 @@ describe('checkAccess', () => { }; expect(await checkAccess({ ...mockDependencies, request })).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -81,6 +83,7 @@ describe('checkAccess', () => { mockSpaces.spacesService.getActiveSpace.mockResolvedValueOnce(disabledSpace); expect(await checkAccess({ ...mockDependencies })).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -94,6 +97,7 @@ describe('checkAccess', () => { ); expect(await checkAccess({ ...mockDependencies })).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -134,6 +138,7 @@ describe('checkAccess', () => { }; expect(await checkAccess({ ...mockDependencies, security })).toEqual({ hasAppSearchAccess: true, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }); }); @@ -149,6 +154,7 @@ describe('checkAccess', () => { }; expect(await checkAccess({ ...mockDependencies, security })).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -170,6 +176,7 @@ describe('checkAccess', () => { const config = { host: undefined }; expect(await checkAccess({ ...mockDependencies, config })).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -180,11 +187,13 @@ describe('checkAccess', () => { (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({ access: { hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }, })); expect(await checkAccess(mockDependencies)).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: true, }); }); @@ -193,6 +202,7 @@ describe('checkAccess', () => { (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({})); expect(await checkAccess(mockDependencies)).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); @@ -204,6 +214,7 @@ describe('checkAccess', () => { })); expect(await checkAccess(mockDependencies)).toEqual({ hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index 444fa9d4fdb294..1f351d05785ad6 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -23,12 +23,14 @@ interface CheckAccess { log: Logger; } -const ALLOW_ALL_PLUGINS = { +const ALLOW_ALL_PLUGINS: ProductAccess = { hasAppSearchAccess: true, + hasSearchEnginesAccess: false, // still false unless Feature Flag explicitly enabled on backend hasWorkplaceSearchAccess: true, }; -const DENY_ALL_PLUGINS = { +const DENY_ALL_PLUGINS: ProductAccess = { hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 5826fbca88b5cf..41031eaf06678e 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -70,6 +70,7 @@ describe('callEnterpriseSearchConfigAPI', () => { name: 'someuser', access: { app_search: true, + search_engines: true, workplace_search: false, }, app_search: { @@ -123,6 +124,7 @@ describe('callEnterpriseSearchConfigAPI', () => { kibanaVersion: '1.0.0', access: { hasAppSearchAccess: true, + hasSearchEnginesAccess: true, hasWorkplaceSearchAccess: false, }, publicUrl: 'http://some.vanity.url', @@ -136,6 +138,7 @@ describe('callEnterpriseSearchConfigAPI', () => { kibanaVersion: '1.0.0', access: { hasAppSearchAccess: false, + hasSearchEnginesAccess: false, hasWorkplaceSearchAccess: false, }, publicUrl: undefined, diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 3b9a37b32d1d7c..b9e2ebdebfafac 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -87,6 +87,7 @@ export const callEnterpriseSearchConfigAPI = async ({ kibanaVersion: kibanaPackageJson.version, access: { hasAppSearchAccess: !!data?.current_user?.access?.app_search, + hasSearchEnginesAccess: !!data?.current_user?.access?.search_engines, hasWorkplaceSearchAccess: !!data?.current_user?.access?.workplace_search, }, publicUrl: stripTrailingSlash(data?.settings?.external_url), diff --git a/x-pack/plugins/enterprise_search/server/ui_settings.ts b/x-pack/plugins/enterprise_search/server/ui_settings.ts index c49d1287b1bf62..99fb5906a30502 100644 --- a/x-pack/plugins/enterprise_search/server/ui_settings.ts +++ b/x-pack/plugins/enterprise_search/server/ui_settings.ts @@ -5,25 +5,9 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from '@kbn/core/types'; -import { i18n } from '@kbn/i18n'; -import { enterpriseSearchFeatureId, enableEnginesSection } from '../common/ui_settings_keys'; /** * uiSettings definitions for Enterprise Search */ -export const uiSettings: Record> = { - [enableEnginesSection]: { - category: [enterpriseSearchFeatureId], - description: i18n.translate('xpack.enterpriseSearch.uiSettings.engines.description', { - defaultMessage: 'Enable the new Engines section in Enterprise Search.', - }), - name: i18n.translate('xpack.enterpriseSearch.uiSettings.engines.name', { - defaultMessage: 'Enable Engines', - }), - requiresPageReload: false, - schema: schema.boolean(), - value: false, - }, -}; +export const uiSettings: Record> = {}; From 4795910ef3635c04f9bd1d9d6c1d8e3841498142 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Wed, 1 Feb 2023 18:34:27 +0100 Subject: [PATCH 58/59] [Synthetics] Overview and Management filters (#149469) Closes #135160 Fixes https://github.com/elastic/kibana/issues/146075 ## Summary Adds the Frequency and Project filter on Management page and all the filters on Overview page as well. The PR doesn't show the filters on a dialog as in the design, for the sake of utilizing existing available component and the fact that opening a dialog adds one additional step to reach filters. The applied filter pills/tags (as in the design) are also not implemented as the filter components show a highlighted number if any filter is applied. Incase this implementation is not sufficient, the same components can be converted to match the design easily. Screenshot 2023-01-25 at 23 18 30 Screenshot 2023-01-25 at 23 19 12 --------- Co-authored-by: shahzad31 --- .../runtime_types/monitor_management/state.ts | 8 +- .../synthetics_overview_status.ts | 1 + .../synthetics/getting_started.journey.ts | 1 + .../synthetics/management_list.journey.ts | 31 ++- .../common/monitor_filters/filter_button.tsx | 55 +++++ .../common/monitor_filters/filter_fields.ts | 91 ++++++++ .../monitor_filters}/filter_group.tsx | 56 +++-- .../monitor_filters}/list_filters.tsx | 14 +- .../monitor_filters}/use_filters.test.ts | 56 ++++- .../common/monitor_filters/use_filters.ts | 221 ++++++++++++++++++ .../monitors_page/common/search_field.tsx | 19 +- .../hooks/use_monitor_list.test.tsx | 46 ++-- .../monitors_page/hooks/use_monitor_list.ts | 70 +++--- .../management/list_filters/filter_button.tsx | 46 ---- .../management/list_filters/use_filters.ts | 82 ------- .../management/monitor_list_container.tsx | 25 +- .../management/monitor_list_table/columns.tsx | 14 +- .../monitor_list_table/monitor_list.tsx | 7 +- .../monitor_stats/monitor_stats.tsx | 14 +- .../monitor_stats/monitor_test_runs.tsx | 6 +- .../monitor_test_runs_sparkline.tsx | 6 +- .../monitors_page/monitors_page.tsx | 14 +- .../monitors_page/overview/overview_page.tsx | 26 +-- .../synthetics/state/monitor_list/actions.ts | 40 +--- .../apps/synthetics/state/monitor_list/api.ts | 4 +- .../synthetics/state/monitor_list/effects.ts | 12 +- .../synthetics/state/monitor_list/index.ts | 10 +- .../synthetics/state/monitor_list/models.ts | 42 +++- .../state/monitor_list/selectors.ts | 10 + .../apps/synthetics/state/overview/api.ts | 4 +- .../apps/synthetics/state/overview/effects.ts | 5 +- .../apps/synthetics/state/overview/models.ts | 6 +- .../__mocks__/synthetics_store.mock.ts | 3 +- .../get_supported_url_params.test.ts | 4 +- .../url_params/get_supported_url_params.ts | 12 +- .../status_rule/status_rule_executor.ts | 1 + .../server/queries/query_monitor_status.ts | 11 +- .../synthetics/server/routes/common.ts | 31 ++- .../routes/monitor_cruds/get_monitor.ts | 100 +++++--- .../routes/status/current_status.test.ts | 11 +- .../server/routes/status/current_status.ts | 21 +- .../synthetics_monitor/get_all_monitors.ts | 7 +- 42 files changed, 855 insertions(+), 388 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_fields.ts rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/{management/list_filters => common/monitor_filters}/filter_group.tsx (54%) rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/{management/list_filters => common/monitor_filters}/list_filters.tsx (60%) rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/{management/list_filters => common/monitor_filters}/use_filters.test.ts (65%) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.ts delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_button.tsx delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts index d997c554eef0e4..a167e869dcb9ae 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts @@ -16,7 +16,9 @@ export const FetchMonitorManagementListQueryArgsCodec = t.partial({ searchFields: t.array(t.string), tags: t.array(t.string), locations: t.array(t.string), - monitorType: t.array(t.string), + monitorTypes: t.array(t.string), + projects: t.array(t.string), + schedules: t.array(t.string), }); export type FetchMonitorManagementListQueryArgs = t.TypeOf< @@ -28,7 +30,9 @@ export const FetchMonitorOverviewQueryArgsCodec = t.partial({ searchFields: t.array(t.string), tags: t.array(t.string), locations: t.array(t.string), - monitorType: t.array(t.string), + projects: t.array(t.string), + schedules: t.array(t.string), + monitorTypes: t.array(t.string), sortField: t.string, sortOrder: t.string, }); diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts index a942316202ebb1..a519036240b502 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts @@ -27,6 +27,7 @@ export const OverviewStatusCodec = t.interface({ disabledCount: t.number, upConfigs: t.record(t.string, OverviewStatusMetaDataCodec), downConfigs: t.record(t.string, OverviewStatusMetaDataCodec), + allIds: t.array(t.string), enabledIds: t.array(t.string), }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts index ddbdd8091bce04..5b8f6670c2f215 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts @@ -42,6 +42,7 @@ journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: a }); step('shows validation error on submit', async () => { + await page.locator('.euiSideNavItem').locator('text=Synthetics').click(); await page.click('text=Create monitor'); expect(await page.isVisible('text=URL is required')).toBeTruthy(); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts index ad220cbc20dedf..f9751d37c6a41e 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts @@ -36,7 +36,10 @@ journey(`MonitorManagementList`, async ({ page, params }) => { await addTestMonitor(params.kibanaUrl, testMonitor1); await addTestMonitor(params.kibanaUrl, testMonitor2); - await addTestMonitor(params.kibanaUrl, testMonitor3); + await addTestMonitor(params.kibanaUrl, testMonitor3, { + type: 'browser', + schedule: { unit: 'm', number: '5' }, + }); }); after(async () => { @@ -64,17 +67,17 @@ journey(`MonitorManagementList`, async ({ page, params }) => { }); step( - 'Click [aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text=browser', + 'Click [aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text="Journey / Page"', async () => { await page.click( - '[aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text=browser' + '[aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text="Journey / Page"' ); await page.click('[aria-label="Apply the selected filters for Type"]'); - expect(page.url()).toBe(`${pageBaseUrl}?monitorType=%5B%22browser%22%5D`); + expect(page.url()).toBe(`${pageBaseUrl}?monitorTypes=%5B%22browser%22%5D`); await page.click('[placeholder="Search by name, url, host, tag, project or location"]'); await Promise.all([ page.waitForNavigation({ - url: `${pageBaseUrl}?monitorType=%5B%22browser%22%5D&query=3`, + url: `${pageBaseUrl}?monitorTypes=%5B%22browser%22%5D&query=3`, }), page.fill('[placeholder="Search by name, url, host, tag, project or location"]', '3'), ]); @@ -99,4 +102,22 @@ journey(`MonitorManagementList`, async ({ page, params }) => { await expect(statSummaryPanel.locator('text=3').count()).resolves.toEqual(1); // Configurations await expect(statSummaryPanel.locator('text=0').count()).resolves.toEqual(1); // Disabled }); + + step('Filter by Frequency', async () => { + const frequencyFilter = page.locator('.euiFilterButton__textShift', { hasText: 'Frequency' }); + const fiveMinuteScheduleOption = page.getByText('Every 5 minutes').first(); + + await frequencyFilter.click(); + await fiveMinuteScheduleOption.click(); + await page.getByText('Apply').click(); + + // There should be only 1 monitor with schedule 5 minutes + await page.waitForSelector('text=1-1'); + + // Clear the filter + await frequencyFilter.click(); + await fiveMinuteScheduleOption.click(); + await page.getByText('Apply').click(); + await page.waitForSelector('text=1-3'); + }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx new file mode 100644 index 00000000000000..65cc56a473cd18 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { FieldValueSelection } from '@kbn/observability-plugin/public'; +import { + getSyntheticsFilterDisplayValues, + SyntheticsMonitorFilterItem, + valueToLabelWithEmptyCount, +} from './filter_fields'; +import { useGetUrlParams } from '../../../../hooks'; +import { useMonitorFiltersState } from './use_filters'; + +export const FilterButton = ({ + filter, + handleFilterChange, +}: { + filter: SyntheticsMonitorFilterItem; + handleFilterChange: ReturnType['handleFilterChange']; +}) => { + const { label, values, field } = filter; + + const [query, setQuery] = useState(''); + + const urlParams = useGetUrlParams(); + + // Transform the values to readable labels (if any) so that selected values are checked on filter dropdown + const selectedValueLabels = getSyntheticsFilterDisplayValues( + (urlParams[field] || []).map(valueToLabelWithEmptyCount), + field, + [] + ).map(({ label: selectedValueLabel }) => selectedValueLabel); + + return ( + str.toLowerCase().includes(query.toLowerCase())) + : values + } + setQuery={setQuery} + onChange={(selectedValues) => handleFilterChange(field, selectedValues)} + allowExclusions={false} + loading={false} + asFilterButton={true} + /> + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_fields.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_fields.ts new file mode 100644 index 00000000000000..7753102fb3fdf0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_fields.ts @@ -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 { i18n } from '@kbn/i18n'; +import { invert } from 'lodash'; +import { DataStream, ServiceLocations } from '../../../../../../../common/runtime_types'; +import { MonitorFilterState } from '../../../../state'; + +export type SyntheticsMonitorFilterField = keyof MonitorFilterState; + +export interface LabelWithCountValue { + label: string; + count: number; +} + +export interface SyntheticsMonitorFilterItem { + label: string; + values: LabelWithCountValue[]; + field: SyntheticsMonitorFilterField; +} + +export function getMonitorFilterFields(): SyntheticsMonitorFilterField[] { + return ['tags', 'locations', 'monitorTypes', 'projects', 'schedules']; +} + +export type SyntheticsMonitorFilterChangeHandler = ( + field: SyntheticsMonitorFilterField, + selectedValues: string[] | undefined +) => void; + +export function getSyntheticsFilterDisplayValues( + values: LabelWithCountValue[], + field: SyntheticsMonitorFilterField, + locations: ServiceLocations +) { + switch (field) { + case 'monitorTypes': + return values.map(({ label, count }: { label: string; count: number }) => ({ + label: monitorTypeKeyLabelMap[label as DataStream] ?? label, + count, + })); + case 'schedules': + return values.map(({ label, count }: { label: string; count: number }) => ({ + label: i18n.translate('xpack.synthetics.monitorFilters.frequencyLabel', { + defaultMessage: `Every {count} minutes`, + values: { count: label }, + }), + count, + })); + case 'locations': + return values.map(({ label, count }) => { + const foundLocation = locations.find( + ({ id: locationId, label: locationLabel }) => + label === locationId || label === locationLabel + ); + return { + label: foundLocation?.label ?? label, + count, + }; + }); + default: + return values; + } +} + +export function getSyntheticsFilterKeyForLabel(value: string, field: SyntheticsMonitorFilterField) { + switch (field) { + case 'monitorTypes': + return invert(monitorTypeKeyLabelMap)[value] ?? value; + case 'schedules': + return (value ?? '').replace(/\D/g, ''); + default: + return value; + } +} + +export const valueToLabelWithEmptyCount = (value: string): LabelWithCountValue => ({ + label: value, + count: 0, +}); + +const monitorTypeKeyLabelMap: Record = { + [DataStream.BROWSER]: 'Journey / Page', + [DataStream.HTTP]: 'HTTP', + [DataStream.TCP]: 'TCP', + [DataStream.ICMP]: 'ICMP', +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_group.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_group.tsx similarity index 54% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_group.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_group.tsx index 904c3c7f18a3fb..a7db1094ff2626 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_group.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_group.tsx @@ -10,50 +10,64 @@ import { EuiFilterGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; import { ServiceLocations } from '../../../../../../../common/runtime_types'; -import { useFilters } from './use_filters'; -import { FilterButton } from './filter_button'; import { selectServiceLocationsState } from '../../../../state'; -export interface FilterItem { - label: string; - values: Array<{ label: string; count: number }>; - field: 'tags' | 'status' | 'locations' | 'monitorType'; -} +import { + SyntheticsMonitorFilterItem, + getSyntheticsFilterDisplayValues, + SyntheticsMonitorFilterChangeHandler, +} from './filter_fields'; +import { useFilters } from './use_filters'; +import { FilterButton } from './filter_button'; export const findLocationItem = (query: string, locations: ServiceLocations) => { return locations.find(({ id, label }) => query === id || label === query); }; -export const FilterGroup = () => { +export const FilterGroup = ({ + handleFilterChange, +}: { + handleFilterChange: SyntheticsMonitorFilterChangeHandler; +}) => { const data = useFilters(); const { locations } = useSelector(selectServiceLocationsState); - const filters: FilterItem[] = [ + const filters: SyntheticsMonitorFilterItem[] = [ { label: TYPE_LABEL, - field: 'monitorType', - values: data.types, + field: 'monitorTypes', + values: getSyntheticsFilterDisplayValues(data.monitorTypes, 'monitorTypes', locations), }, { label: LOCATION_LABEL, field: 'locations', - values: data.locations.map(({ label, count }) => ({ - label: findLocationItem(label, locations)?.label ?? label, - count, - })), + values: getSyntheticsFilterDisplayValues(data.locations, 'locations', locations), }, { label: TAGS_LABEL, field: 'tags', - values: data.tags, + values: getSyntheticsFilterDisplayValues(data.tags, 'tags', locations), + }, + { + label: SCHEDULE_LABEL, + field: 'schedules', + values: getSyntheticsFilterDisplayValues(data.schedules, 'schedules', locations), }, ]; + if (data.projects.length > 0) { + filters.push({ + label: PROJECT_LABEL, + field: 'projects', + values: getSyntheticsFilterDisplayValues(data.projects, 'projects', locations), + }); + } + return ( {filters.map((filter, index) => ( - + ))} ); @@ -63,6 +77,10 @@ const TYPE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.typ defaultMessage: `Type`, }); +const PROJECT_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.projectLabel', { + defaultMessage: `Project`, +}); + const LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.locationLabel', { defaultMessage: `Location`, }); @@ -70,3 +88,7 @@ const LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter const TAGS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.tagsLabel', { defaultMessage: `Tags`, }); + +const SCHEDULE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.frequencyLabel', { + defaultMessage: `Frequency`, +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/list_filters.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/list_filters.tsx similarity index 60% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/list_filters.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/list_filters.tsx index 1d693a3f991bd0..9e654da2078e38 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/list_filters.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/list_filters.tsx @@ -7,17 +7,23 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { FilterGroup } from './filter_group'; -import { SearchField } from '../../common/search_field'; +import { SearchField } from '../search_field'; +import { SyntheticsMonitorFilterChangeHandler } from './filter_fields'; -export function ListFilters() { +export function ListFilters({ + handleFilterChange, +}: { + handleFilterChange: SyntheticsMonitorFilterChangeHandler; +}) { return ( - + - + ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.test.ts similarity index 65% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.test.ts index 5a345ff88bcceb..be231f10e8d84b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.test.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.test.ts @@ -13,18 +13,30 @@ describe('useMonitorListFilters', () => { it('returns expected results', () => { const { result } = renderHook(() => useFilters(), { wrapper: WrappedHelper }); - expect(result.current).toStrictEqual({ locations: [], tags: [], types: [] }); + expect(result.current).toStrictEqual({ + locations: [], + tags: [], + monitorTypes: [], + projects: [], + schedules: [], + }); expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({ aggs: { locations: { terms: { field: 'synthetics-monitor.attributes.locations.id', size: 10000 }, }, + monitorTypes: { + terms: { field: 'synthetics-monitor.attributes.type.keyword', size: 10000 }, + }, + projects: { + terms: { field: 'synthetics-monitor.attributes.project_id', size: 10000 }, + }, + schedules: { + terms: { field: 'synthetics-monitor.attributes.schedule.number', size: 10000 }, + }, tags: { terms: { field: 'synthetics-monitor.attributes.tags', size: 10000 }, }, - types: { - terms: { field: 'synthetics-monitor.attributes.type.keyword', size: 10000 }, - }, }, perPage: 0, type: 'synthetics-monitor', @@ -40,18 +52,30 @@ describe('useMonitorListFilters', () => { { key: 'Test 2', doc_count: 2 }, ], }, - tags: { + monitorTypes: { buckets: [ { key: 'Test 3', doc_count: 3 }, { key: 'Test 4', doc_count: 4 }, ], }, - types: { + projects: { buckets: [ { key: 'Test 5', doc_count: 5 }, { key: 'Test 6', doc_count: 6 }, ], }, + schedules: { + buckets: [ + { key: 'Test 7', doc_count: 7 }, + { key: 'Test 8', doc_count: 8 }, + ], + }, + tags: { + buckets: [ + { key: 'Test 9', doc_count: 9 }, + { key: 'Test 10', doc_count: 10 }, + ], + }, }, }); @@ -59,7 +83,13 @@ describe('useMonitorListFilters', () => { wrapper: WrappedHelper, }); - expect(result.current).toStrictEqual({ locations: [], tags: [], types: [] }); + expect(result.current).toStrictEqual({ + locations: [], + tags: [], + monitorTypes: [], + projects: [], + schedules: [], + }); await waitForNextUpdate(); @@ -68,14 +98,22 @@ describe('useMonitorListFilters', () => { { label: 'Test 1', count: 1 }, { label: 'Test 2', count: 2 }, ], - tags: [ + monitorTypes: [ { label: 'Test 3', count: 3 }, { label: 'Test 4', count: 4 }, ], - types: [ + projects: [ { label: 'Test 5', count: 5 }, { label: 'Test 6', count: 6 }, ], + schedules: [ + { label: 'Test 7', count: 7 }, + { label: 'Test 8', count: 8 }, + ], + tags: [ + { label: 'Test 9', count: 9 }, + { label: 'Test 10', count: 10 }, + ], }); }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.ts new file mode 100644 index 00000000000000..77b32da98aef0e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/use_filters.ts @@ -0,0 +1,221 @@ +/* + * 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 { useMemo, useEffect, useCallback, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useFetcher } from '@kbn/observability-plugin/public'; + +import { ConfigKey } from '../../../../../../../common/runtime_types'; +import { syntheticsMonitorType } from '../../../../../../../common/types/saved_objects'; +import { + MonitorFilterState, + selectMonitorFiltersAndQueryState, + setOverviewPageStateAction, + updateManagementPageStateAction, +} from '../../../../state'; +import { SyntheticsUrlParams } from '../../../../utils/url_params'; +import { useUrlParams } from '../../../../hooks'; + +import { + SyntheticsMonitorFilterField, + getMonitorFilterFields, + getSyntheticsFilterKeyForLabel, + SyntheticsMonitorFilterChangeHandler, +} from './filter_fields'; + +const aggs = { + monitorTypes: { + terms: { + field: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_TYPE}.keyword`, + size: 10000, + }, + }, + tags: { + terms: { + field: `${syntheticsMonitorType}.attributes.${ConfigKey.TAGS}`, + size: 10000, + }, + }, + locations: { + terms: { + field: `${syntheticsMonitorType}.attributes.${ConfigKey.LOCATIONS}.id`, + size: 10000, + }, + }, + projects: { + terms: { + field: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}`, + size: 10000, + }, + }, + schedules: { + terms: { + field: `${syntheticsMonitorType}.attributes.${ConfigKey.SCHEDULE}.number`, + size: 10000, + }, + }, +}; + +type Buckets = Array<{ + key: string; + doc_count: number; +}>; + +interface AggsResponse { + monitorTypes: { + buckets: Buckets; + }; + locations: { + buckets: Buckets; + }; + tags: { + buckets: Buckets; + }; + projects: { + buckets: Buckets; + }; + schedules: { + buckets: Buckets; + }; +} + +export const useFilters = (): Record< + SyntheticsMonitorFilterField, + Array<{ label: string; count: number }> +> => { + const { savedObjects } = useKibana().services; + + const { data } = useFetcher(async () => { + return savedObjects?.client.find({ + type: syntheticsMonitorType, + perPage: 0, + aggs, + }); + }, []); + + return useMemo(() => { + const { monitorTypes, tags, locations, projects, schedules } = + (data?.aggregations as AggsResponse) ?? {}; + return { + monitorTypes: + monitorTypes?.buckets?.map(({ key, doc_count: count }) => ({ + label: key, + count, + })) ?? [], + tags: + tags?.buckets?.map(({ key, doc_count: count }) => ({ + label: key, + count, + })) ?? [], + locations: + locations?.buckets?.map(({ key, doc_count: count }) => ({ + label: key, + count, + })) ?? [], + projects: + projects?.buckets + ?.filter(({ key }) => key) + .map(({ key, doc_count: count }) => ({ + label: key, + count, + })) ?? [], + schedules: + schedules?.buckets?.map(({ key, doc_count: count }) => ({ + label: key, + count, + })) ?? [], + }; + }, [data]); +}; + +type FilterFieldWithQuery = SyntheticsMonitorFilterField | 'query'; +type FilterStateWithQuery = MonitorFilterState & { query?: string }; + +export function useMonitorFiltersState() { + const [getUrlParams, updateUrlParams] = useUrlParams(); + const urlParams = getUrlParams(); + + const filterFieldsWithQuery: FilterFieldWithQuery[] = useMemo(() => { + const filterFields = getMonitorFilterFields(); + return [...filterFields, 'query']; + }, []); + + const dispatch = useDispatch(); + + const serializeFilterValue = useCallback( + (field: FilterFieldWithQuery, selectedValues: string[] | undefined) => { + if (field === 'query') { + return selectedValues?.length ? selectedValues.toString() : undefined; + } + + return selectedValues && selectedValues.length > 0 + ? JSON.stringify( + selectedValues.map((value) => getSyntheticsFilterKeyForLabel(value, field)) + ) + : undefined; + }, + [] + ); + + const serializeStateValues = useCallback( + (state: FilterStateWithQuery) => { + return filterFieldsWithQuery.reduce( + (acc, cur) => ({ + ...acc, + [cur]: serializeFilterValue( + cur as SyntheticsMonitorFilterField, + state[cur as SyntheticsMonitorFilterField] + ), + }), + {} + ); + }, + [filterFieldsWithQuery, serializeFilterValue] + ); + + const handleFilterChange: SyntheticsMonitorFilterChangeHandler = useCallback( + (field: SyntheticsMonitorFilterField, selectedValues: string[] | undefined) => { + // Update url to reflect the changed filter + updateUrlParams({ + [field]: serializeFilterValue(field, selectedValues), + }); + }, + [serializeFilterValue, updateUrlParams] + ); + + const reduxState = useSelector(selectMonitorFiltersAndQueryState); + const reduxStateSnapshot = JSON.stringify(serializeStateValues(reduxState)); + const urlState = filterFieldsWithQuery.reduce( + (acc, cur) => ({ ...acc, [cur]: urlParams[cur as keyof SyntheticsUrlParams] }), + {} + ); + const urlStateSerializedSnapshot = JSON.stringify(serializeStateValues(urlState)); + + const isUrlHydratedFromRedux = useRef(false); + useEffect(() => { + if (urlStateSerializedSnapshot !== reduxStateSnapshot) { + if ( + urlStateSerializedSnapshot === '{}' && + reduxStateSnapshot !== '{}' && + !isUrlHydratedFromRedux.current + ) { + // Hydrate url only during initialization + updateUrlParams(serializeStateValues(reduxState)); + } else { + dispatch(updateManagementPageStateAction(urlState)); + dispatch(setOverviewPageStateAction(urlState)); + } + } + isUrlHydratedFromRedux.current = true; + + // Only depend on the serialized snapshot + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [urlStateSerializedSnapshot, reduxStateSnapshot]); + + return { handleFilterChange, filterState: reduxState }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/search_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/search_field.tsx index 6e1a5899c19476..1c93669fb41127 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/search_field.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/search_field.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { EuiFieldSearch } from '@elastic/eui'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; @@ -15,7 +15,7 @@ export function SearchField() { const { query } = useGetUrlParams(); const [_, updateUrlParams] = useUrlParams(); - const [search, setSearch] = useState(query || ''); + const [search, setSearch] = useState(''); useDebounce( () => { @@ -27,13 +27,26 @@ export function SearchField() { [search] ); + // Hydrate search input + const hasInputChangedRef = useRef(false); + useEffect(() => { + if (query && query !== search && !hasInputChangedRef.current) { + setSearch(query); + } + + // Run only to sync url with input + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query]); + return ( { - setSearch(e.target.value); + hasInputChangedRef.current = true; + setSearch(e.target.value ?? ''); }} isClearable={true} aria-label={PLACEHOLDER_TEXT} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.test.tsx index f07805ee99cff9..86dabaedd7b28a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.test.tsx @@ -14,8 +14,8 @@ import { WrappedHelper } from '../../../utils/testing'; import { SyntheticsAppState } from '../../../state/root_reducer'; import { selectEncryptedSyntheticsSavedMonitors, - fetchMonitorListAction, - MonitorListPageState, + updateManagementPageStateAction, + MonitorFilterState, } from '../../../state'; import { useMonitorList } from './use_monitor_list'; @@ -23,7 +23,8 @@ import { useMonitorList } from './use_monitor_list'; describe('useMonitorList', () => { let state: SyntheticsAppState; let initialState: Omit, 'loadPage' | 'reloadPage'>; - let defaultPageState: MonitorListPageState; + let filterState: MonitorFilterState; + let filterStateWithQuery: MonitorFilterState & { query?: string | undefined }; const dispatchMockFn = jest.fn(); beforeEach(() => { @@ -40,15 +41,12 @@ describe('useMonitorList', () => { pageState: state.monitorList.pageState, isDataQueried: false, syntheticsMonitors: selectEncryptedSyntheticsSavedMonitors.resultFunc(state.monitorList), + overviewStatus: null, + handleFilterChange: jest.fn(), }; - defaultPageState = { - ...state.monitorList.pageState, - query: '', - locations: [], - monitorType: [], - tags: [], - }; + filterState = { locations: [], monitorTypes: [], projects: [], schedules: [], tags: [] }; + filterStateWithQuery = { ...filterState, query: 'xyz' }; }); afterEach(() => { @@ -60,7 +58,7 @@ describe('useMonitorList', () => { result: { current: hookResult }, } = renderHook(() => useMonitorList(), { wrapper: WrappedHelper }); - expect(hookResult).toMatchObject(initialState); + expect(hookResult).toMatchObject({ ...initialState, handleFilterChange: expect.any(Function) }); }); it('dispatches correct action for query url param', async () => { @@ -79,18 +77,26 @@ describe('useMonitorList', () => { renderHook(() => useMonitorList(), { wrapper: WrapperWithState }); expect(dispatchMockFn).toHaveBeenCalledWith( - fetchMonitorListAction.get({ ...defaultPageState, query }) + updateManagementPageStateAction(filterStateWithQuery) ); }); it('dispatches correct action for filter url param', async () => { - const tags = ['abc', 'xyz']; - const locations = ['loc1', 'loc1']; - const monitorType = ['browser']; + const exp = { + ...filterStateWithQuery, + tags: ['abc', 'xyz'], + locations: ['loc1', 'loc1'], + monitorTypes: ['browser'], + schedules: ['browser'], + projects: ['proj-1'], + query: '', + }; - const url = `/monitor/1?tags=${JSON.stringify(tags)}&locations=${JSON.stringify( - locations - )}&monitorType=${JSON.stringify(monitorType)}`; + const url = `/monitor/1?tags=${JSON.stringify(exp.tags)}&locations=${JSON.stringify( + exp.locations + )}&monitorTypes=${JSON.stringify(exp.monitorTypes)}&schedules=${JSON.stringify( + exp.schedules + )}&projects=${JSON.stringify(exp.projects)}`; jest.useFakeTimers().setSystemTime(Date.now()); const WrapperWithState = ({ children }: { children: React.ReactElement }) => { @@ -103,8 +109,6 @@ describe('useMonitorList', () => { renderHook(() => useMonitorList(), { wrapper: WrapperWithState }); - expect(dispatchMockFn).toHaveBeenCalledWith( - fetchMonitorListAction.get({ ...defaultPageState, tags, locations, monitorType }) - ); + expect(dispatchMockFn).toHaveBeenCalledWith(updateManagementPageStateAction(exp)); }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts index 9b30c5590f9507..5963ff30964f97 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts @@ -5,16 +5,21 @@ * 2.0. */ -import { useEffect, useCallback, useRef } from 'react'; +import { useCallback, useRef, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { useGetUrlParams } from '../../../hooks'; +import { useDebounce } from 'react-use'; import { fetchMonitorListAction, + quietFetchMonitorListAction, + fetchOverviewStatusAction, MonitorListPageState, selectEncryptedSyntheticsSavedMonitors, selectMonitorListState, + selectOverviewStatus, + updateManagementPageStateAction, } from '../../../state'; +import { useSyntheticsRefreshContext } from '../../../contexts'; +import { useMonitorFiltersState } from '../common/monitor_filters/use_filters'; export function useMonitorList() { const dispatch = useDispatch(); @@ -22,43 +27,44 @@ export function useMonitorList() { const { pageState, loading, loaded, error, data } = useSelector(selectMonitorListState); const syntheticsMonitors = useSelector(selectEncryptedSyntheticsSavedMonitors); + const { status: overviewStatus } = useSelector(selectOverviewStatus); - const { query, tags, monitorType, locations: locationFilters } = useGetUrlParams(); - - const { search } = useLocation(); + const { handleFilterChange } = useMonitorFiltersState(); + const { refreshInterval } = useSyntheticsRefreshContext(); const loadPage = useCallback( - (state: MonitorListPageState) => - dispatch( - fetchMonitorListAction.get({ - ...state, - query, - tags, - monitorType, - locations: locationFilters, - }) - ), - [dispatch, locationFilters, monitorType, query, tags] + (state: MonitorListPageState) => { + dispatch(updateManagementPageStateAction(state)); + }, + [dispatch] ); - const reloadPage = useCallback(() => loadPage(pageState), [pageState, loadPage]); - useEffect(() => { - reloadPage(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [search]); + const reloadPageQuiet = useCallback(() => { + dispatch(quietFetchMonitorListAction(pageState)); + }, [dispatch, pageState]); - // Initial loading + // Periodically refresh useEffect(() => { - if (!loading && !isDataQueriedRef.current) { - isDataQueriedRef.current = true; - reloadPage(); - } + const intervalId = setInterval(() => { + reloadPageQuiet(); + }, refreshInterval); - if (loading) { - isDataQueriedRef.current = true; - } - }, [reloadPage, syntheticsMonitors, loading]); + return () => { + clearInterval(intervalId); + }; + }, [reloadPageQuiet, refreshInterval]); + + useDebounce( + () => { + const overviewStatusArgs = { ...pageState, perPage: pageState.pageSize }; + + dispatch(fetchOverviewStatusAction.get(overviewStatusArgs)); + dispatch(fetchMonitorListAction.get(pageState)); + }, + 500, + [pageState] + ); return { loading, @@ -71,5 +77,7 @@ export function useMonitorList() { reloadPage, isDataQueried: isDataQueriedRef.current, absoluteTotal: data.absoluteTotal ?? 0, + overviewStatus, + handleFilterChange, }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_button.tsx deleted file mode 100644 index 463c4bbeba003b..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/filter_button.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { FieldValueSelection } from '@kbn/observability-plugin/public'; -import { FilterItem } from './filter_group'; -import { useGetUrlParams, useUrlParams } from '../../../../hooks'; - -export const FilterButton = ({ filter }: { filter: FilterItem }) => { - const { label, values, field } = filter; - - const [query, setQuery] = useState(''); - - const [, updateUrlParams] = useUrlParams(); - - const urlParams = useGetUrlParams(); - - return ( - str.toLowerCase().includes(query.toLowerCase())) - : values - } - setQuery={setQuery} - onChange={(selectedValues) => { - updateUrlParams({ - [field]: - selectedValues && selectedValues.length > 0 - ? JSON.stringify(selectedValues) - : undefined, - }); - }} - allowExclusions={false} - loading={false} - asFilterButton={true} - /> - ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts deleted file mode 100644 index 9b9486d5d7398b..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/list_filters/use_filters.ts +++ /dev/null @@ -1,82 +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 { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useFetcher } from '@kbn/observability-plugin/public'; -import { useMemo } from 'react'; -import { syntheticsMonitorType } from '../../../../../../../common/types/saved_objects'; - -const aggs = { - types: { - terms: { - field: `${syntheticsMonitorType}.attributes.type.keyword`, - size: 10000, - }, - }, - tags: { - terms: { - field: `${syntheticsMonitorType}.attributes.tags`, - size: 10000, - }, - }, - locations: { - terms: { - field: `${syntheticsMonitorType}.attributes.locations.id`, - size: 10000, - }, - }, -}; - -type Buckets = Array<{ - key: string; - doc_count: number; -}>; - -interface AggsResponse { - types: { - buckets: Buckets; - }; - locations: { - buckets: Buckets; - }; - tags: { - buckets: Buckets; - }; -} - -export const useFilters = () => { - const { savedObjects } = useKibana().services; - - const { data } = useFetcher(async () => { - return savedObjects?.client.find({ - type: syntheticsMonitorType, - perPage: 0, - aggs, - }); - }, []); - - return useMemo(() => { - const { types, tags, locations } = (data?.aggregations as AggsResponse) ?? {}; - return { - types: - types?.buckets?.map(({ key, doc_count: count }) => ({ - label: key, - count, - })) ?? [], - tags: - tags?.buckets?.map(({ key, doc_count: count }) => ({ - label: key, - count, - })) ?? [], - locations: - locations?.buckets?.map(({ key, doc_count: count }) => ({ - label: key, - count, - })) ?? [], - }; - }, [data]); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx index 8b5f57fe7efdc8..51b4932bdfe15d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import React, { useMemo, useEffect } from 'react'; +import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import type { useMonitorList } from '../hooks/use_monitor_list'; import { MonitorAsyncError } from './monitor_errors/monitor_async_error'; -import { useOverviewStatus } from '../hooks/use_overview_status'; -import { ListFilters } from './list_filters/list_filters'; +import { ListFilters } from '../common/monitor_filters/list_filters'; import { MonitorList } from './monitor_list_table/monitor_list'; import { MonitorStats } from './monitor_stats/monitor_stats'; @@ -31,6 +30,8 @@ export const MonitorListContainer = ({ absoluteTotal, loadPage, reloadPage, + overviewStatus, + handleFilterChange, } = monitorListProps; // TODO: Display inline errors in the management table @@ -41,18 +42,6 @@ export const MonitorListContainer = ({ // sortOrder: pageState.sortOrder, // }); - const overviewStatusArgs = useMemo(() => { - return { - pageState: { ...pageState, perPage: pageState.pageSize }, - }; - }, [pageState]); - - const { status, reload: reloadStatus } = useOverviewStatus(overviewStatusArgs); - - useEffect(() => { - reloadStatus(); - }, [reloadStatus, syntheticsMonitors]); - if (!isEnabled && absoluteTotal === 0) { return null; } @@ -60,9 +49,9 @@ export const MonitorListContainer = ({ return ( <> - + - + ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx index 08477bd9eb7d93..57e8d846130cf8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx @@ -34,15 +34,13 @@ import { MonitorLocations } from './monitor_locations'; export function useMonitorListColumns({ canEditSynthetics, - reloadPage, loading, - status, + overviewStatus, setMonitorPendingDeletion, }: { canEditSynthetics: boolean; loading: boolean; - status: OverviewStatusState | null; - reloadPage: () => void; + overviewStatus: OverviewStatusState | null; setMonitorPendingDeletion: (config: EncryptedSyntheticsSavedMonitor) => void; }): Array> { const history = useHistory(); @@ -66,7 +64,7 @@ export function useMonitorListColumns({ ), }, // Only show Project ID column if project monitors are present - ...(status?.projectMonitorsCount ?? 0 > 0 + ...(overviewStatus?.projectMonitorsCount ?? 0 > 0 ? [ { align: 'left' as const, @@ -91,7 +89,7 @@ export function useMonitorListColumns({ ariaLabel={labels.getFilterForTypeMessage(monitor[ConfigKey.MONITOR_TYPE])} onClick={() => { history.push({ - search: `monitorType=${encodeURIComponent( + search: `monitorTypes=${encodeURIComponent( JSON.stringify([monitor[ConfigKey.MONITOR_TYPE]]) )}`, }); @@ -119,7 +117,7 @@ export function useMonitorListColumns({ ) : null, }, @@ -149,7 +147,7 @@ export function useMonitorListColumns({ {}} isSwitchable={!loading} /> ), diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx index b08f64c980a18d..2ac8c0f6129d65 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx @@ -37,7 +37,7 @@ interface Props { loading: boolean; loadPage: (state: MonitorListPageState) => void; reloadPage: () => void; - status: OverviewStatusState | null; + overviewStatus: OverviewStatusState | null; } export const MonitorList = ({ @@ -46,7 +46,7 @@ export const MonitorList = ({ total, error, loading, - status, + overviewStatus, loadPage, reloadPage, }: Props) => { @@ -98,8 +98,7 @@ export const MonitorList = ({ const columns = useMonitorListColumns({ canEditSynthetics, loading, - reloadPage, - status, + overviewStatus, setMonitorPendingDeletion, }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_stats.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_stats.tsx index 71a52993ad6f70..1a7dc9a7b758b0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_stats.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_stats.tsx @@ -24,7 +24,11 @@ import * as labels from '../labels'; import { MonitorTestRunsCount } from './monitor_test_runs'; import { MonitorTestRunsSparkline } from './monitor_test_runs_sparkline'; -export const MonitorStats = ({ status }: { status: OverviewStatusState | null }) => { +export const MonitorStats = ({ + overviewStatus, +}: { + overviewStatus: OverviewStatusState | null; +}) => { const { euiTheme } = useEuiTheme(); return ( @@ -43,11 +47,11 @@ export const MonitorStats = ({ status }: { status: OverviewStatusState | null }) @@ -64,9 +68,9 @@ export const MonitorStats = ({ status }: { status: OverviewStatusState | null }) - + - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx index 8874690f0e0e55..84865837bac6ea 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx @@ -15,7 +15,7 @@ import { useAbsoluteDate } from '../../../../hooks'; import { ClientPluginsStart } from '../../../../../../plugin'; import * as labels from '../labels'; -export const MonitorTestRunsCount = () => { +export const MonitorTestRunsCount = ({ monitorIds }: { monitorIds: string[] }) => { const { observability } = useKibana().services; const theme = useTheme(); @@ -31,11 +31,11 @@ export const MonitorTestRunsCount = () => { { time: { from: absFrom, to: absTo }, reportDefinitions: { - 'monitor.id': [], - 'observer.geo.name': [], + 'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty }, dataType: 'synthetics', selectedMetricField: 'monitor_total_runs', + filters: [], name: labels.TEST_RUNS_LABEL, color: theme.eui.euiColorVis1, }, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx index 60d565e9417f09..76227cb199bd6f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx @@ -14,7 +14,7 @@ import { useAbsoluteDate } from '../../../../hooks'; import { ClientPluginsStart } from '../../../../../../plugin'; import * as labels from '../labels'; -export const MonitorTestRunsSparkline = () => { +export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[] }) => { const { observability } = useKibana().services; const { ExploratoryViewEmbeddable } = observability; @@ -34,11 +34,11 @@ export const MonitorTestRunsSparkline = () => { seriesType: 'area', time: { from, to }, reportDefinitions: { - 'monitor.id': [], - 'observer.geo.name': [], + 'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty }, dataType: 'synthetics', selectedMetricField: 'monitor.check_group', + filters: [], name: labels.TEST_RUNS_LABEL, color: theme.eui.euiColorVis1, operationType: 'unique_count', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitors_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitors_page.tsx index 6bd9578fa5374d..f799643785e807 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitors_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitors_page.tsx @@ -30,6 +30,13 @@ const MonitorPage: React.FC = () => { useMonitorListBreadcrumbs(); + const { + error: enablementError, + enablement: { isEnabled, canEnable }, + loading: enablementLoading, + enableSynthetics, + } = useEnablement(); + const monitorListProps = useMonitorList(); const { syntheticsMonitors, @@ -38,13 +45,6 @@ const MonitorPage: React.FC = () => { absoluteTotal, } = monitorListProps; - const { - error: enablementError, - enablement: { isEnabled, canEnable }, - loading: enablementLoading, - enableSynthetics, - } = useEnablement(); - const { loading: locationsLoading } = useLocations(); const showEmptyState = isEnabled !== undefined && syntheticsMonitors.length === 0; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index 1b75d65c891f74..a971b99e5f4300 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -9,13 +9,13 @@ import { EuiFlexGroup, EuiSpacer, EuiFlexItem } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; import { useTrackPageview } from '@kbn/observability-plugin/public'; import { Redirect, useLocation } from 'react-router-dom'; +import { FilterGroup } from '../common/monitor_filters/filter_group'; import { OverviewAlerts } from './overview/overview_alerts'; -import { useEnablement, useGetUrlParams } from '../../../hooks'; +import { useEnablement } from '../../../hooks'; import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; import { fetchMonitorOverviewAction, quietFetchOverviewAction, - setOverviewPageStateAction, selectOverviewPageState, selectServiceLocationsState, } from '../../../state'; @@ -40,7 +40,6 @@ export const OverviewPage: React.FC = () => { const dispatch = useDispatch(); const { lastRefresh } = useSyntheticsRefreshContext(); - const { query } = useGetUrlParams(); const { search } = useLocation(); const pageState = useSelector(selectOverviewPageState); @@ -52,14 +51,6 @@ export const OverviewPage: React.FC = () => { } }, [dispatch, locationsLoaded, locationsLoading]); - // fetch overview for query state changes - useEffect(() => { - if (pageState.query !== query) { - dispatch(fetchMonitorOverviewAction.get({ ...pageState, query })); - dispatch(setOverviewPageStateAction({ query })); - } - }, [dispatch, pageState, query]); - // fetch overview for all other page state changes useEffect(() => { dispatch(fetchMonitorOverviewAction.get(pageState)); @@ -75,13 +66,19 @@ export const OverviewPage: React.FC = () => { loading: enablementLoading, } = useEnablement(); - const { syntheticsMonitors, loading: monitorsLoading, loaded: monitorsLoaded } = useMonitorList(); + const { + syntheticsMonitors, + loading: monitorsLoading, + loaded: monitorsLoaded, + handleFilterChange, + } = useMonitorList(); if ( !search && !enablementLoading && isEnabled && !monitorsLoading && + monitorsLoaded && syntheticsMonitors.length === 0 ) { return ; @@ -99,13 +96,16 @@ export const OverviewPage: React.FC = () => { return ( <> - + + + + {Boolean(!monitorsLoaded || syntheticsMonitors?.length > 0) && ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts index fb400f97fd7296..b43fb185ea652d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts @@ -5,16 +5,10 @@ * 2.0. */ -import { ErrorToastOptions } from '@kbn/core-notifications-browser'; import { createAction } from '@reduxjs/toolkit'; -import { UpsertMonitorResponse } from '..'; -import { - EncryptedSyntheticsMonitor, - MonitorManagementListResult, - SyntheticsMonitor, -} from '../../../../../common/runtime_types'; +import { UpsertMonitorError, UpsertMonitorRequest, UpsertMonitorResponse } from '..'; +import { MonitorManagementListResult } from '../../../../../common/runtime_types'; import { createAsyncAction } from '../utils/actions'; -import { IHttpSerializedFetchError } from '../utils/http_error'; import { MonitorListPageState } from './models'; @@ -22,29 +16,9 @@ export const fetchMonitorListAction = createAsyncAction< MonitorListPageState, MonitorManagementListResult >('fetchMonitorListAction'); - -interface ToastParams { - message: MessageType; - lifetimeMs: number; - testAttribute?: string; -} - -export interface UpsertMonitorRequest { - configId: string; - monitor: Partial | Partial; - success: ToastParams; - error: ToastParams; - /** - * The effect will perform a quiet refresh of the overview state - * after a successful upsert. The default behavior is to perform the fetch. - */ - shouldQuietFetchAfterSuccess?: boolean; -} - -interface UpsertMonitorError { - configId: string; - error: IHttpSerializedFetchError; -} +export const quietFetchMonitorListAction = createAction( + 'quietFetchMonitorListAction' +); export const fetchUpsertMonitorAction = createAction('fetchUpsertMonitor'); export const fetchUpsertSuccessAction = createAction<{ @@ -62,3 +36,7 @@ export const enableMonitorAlertAction = createAsyncAction< >('enableMonitorAlertAction'); export const clearMonitorUpsertStatus = createAction('clearMonitorUpsertStatus'); + +export const updateManagementPageStateAction = createAction>( + 'updateManagementPageState' +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts index 0956fad85cfd61..9d1f7c21f56963 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts @@ -31,7 +31,9 @@ function toMonitorManagementListQueryArgs( query: pageState.query, tags: pageState.tags, locations: pageState.locations, - monitorType: pageState.monitorType, + monitorTypes: pageState.monitorTypes, + projects: pageState.projects, + schedules: pageState.schedules, searchFields: [], }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts index 4520a9d0c6f0ce..14f0be3493ac66 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts @@ -6,9 +6,10 @@ */ import { PayloadAction } from '@reduxjs/toolkit'; -import { call, put, takeEvery, takeLeading, select } from 'redux-saga/effects'; +import { call, put, takeEvery, select, debounce } from 'redux-saga/effects'; import { SavedObject } from '@kbn/core-saved-objects-common'; import { enableDefaultAlertingAction } from '../alert_rules'; +import { ConfigKey, SyntheticsMonitor } from '../../../../../common/runtime_types'; import { kibanaService } from '../../../../utils/kibana_service'; import { MonitorOverviewPageState, quietFetchOverviewStatusAction } from '../overview'; import { quietFetchOverviewAction } from '../overview/actions'; @@ -22,15 +23,16 @@ import { fetchUpsertFailureAction, fetchUpsertMonitorAction, fetchUpsertSuccessAction, - UpsertMonitorRequest, + quietFetchMonitorListAction, } from './actions'; import { fetchMonitorManagementList, fetchUpsertMonitor } from './api'; import { toastTitle } from './toast_title'; -import { ConfigKey, SyntheticsMonitor } from '../../../../../common/runtime_types'; +import { UpsertMonitorRequest } from './models'; export function* fetchMonitorListEffect() { - yield takeLeading( - fetchMonitorListAction.get, + yield debounce( + 300, // Only take the latest while ignoring any intermediate triggers + [fetchMonitorListAction.get, quietFetchMonitorListAction], fetchEffectFactory( fetchMonitorManagementList, fetchMonitorListAction.success, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts index ff07001f74ef39..e5086dbb63b217 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { isEqual } from 'lodash'; import { createReducer } from '@reduxjs/toolkit'; import { FETCH_STATUS } from '@kbn/observability-plugin/public'; @@ -26,6 +25,7 @@ import { fetchUpsertFailureAction, fetchUpsertMonitorAction, fetchUpsertSuccessAction, + updateManagementPageStateAction, } from './actions'; export interface MonitorListState { @@ -56,10 +56,10 @@ const initialState: MonitorListState = { export const monitorListReducer = createReducer(initialState, (builder) => { builder - .addCase(fetchMonitorListAction.get, (state, action) => { - if (!isEqual(state.pageState, action.payload)) { - state.pageState = action.payload; - } + .addCase(updateManagementPageStateAction, (state, action) => { + state.pageState = { ...state.pageState, ...action.payload }; + }) + .addCase(fetchMonitorListAction.get, (state) => { state.loading = true; state.loaded = false; }) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.ts index b43de3a1914d08..3df6bd275cc968 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.ts @@ -5,20 +5,54 @@ * 2.0. */ +import { ErrorToastOptions } from '@kbn/core-notifications-browser'; + import { + EncryptedSyntheticsMonitor, EncryptedSyntheticsSavedMonitor, FetchMonitorManagementListQueryArgs, + SyntheticsMonitor, } from '../../../../../common/runtime_types'; +import { IHttpSerializedFetchError } from '../utils/http_error'; + export type MonitorListSortField = `${keyof EncryptedSyntheticsSavedMonitor}.keyword` | 'enabled'; -export interface MonitorListPageState { +export interface MonitorFilterState { + tags?: string[]; + monitorTypes?: string[]; + projects?: string[]; + schedules?: string[]; + locations?: string[]; +} + +export interface MonitorListPageState extends MonitorFilterState { query?: string; pageIndex: number; pageSize: number; sortField: MonitorListSortField; sortOrder: NonNullable; - tags?: string[]; - monitorType?: string[]; - locations?: string[]; +} + +interface ToastParams { + message: MessageType; + lifetimeMs: number; + testAttribute?: string; +} + +export interface UpsertMonitorRequest { + configId: string; + monitor: Partial | Partial; + success: ToastParams; + error: ToastParams; + /** + * The effect will perform a quiet refresh of the overview state + * after a successful upsert. The default behavior is to perform the fetch. + */ + shouldQuietFetchAfterSuccess?: boolean; +} + +export interface UpsertMonitorError { + configId: string; + error: IHttpSerializedFetchError; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.ts index 74f3f62a30ee2e..4f1b26dec51424 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.ts @@ -9,6 +9,7 @@ import { createSelector } from 'reselect'; import { ConfigKey, EncryptedSyntheticsSavedMonitor } from '../../../../../common/runtime_types'; import { SyntheticsAppState } from '../root_reducer'; +import { MonitorFilterState } from './models'; export const selectMonitorListState = (state: SyntheticsAppState) => state.monitorList; export const selectEncryptedSyntheticsSavedMonitors = createSelector( @@ -21,6 +22,15 @@ export const selectEncryptedSyntheticsSavedMonitors = createSelector( created_at: monitor.created_at, })) as EncryptedSyntheticsSavedMonitor[] ); + +export const selectMonitorFiltersAndQueryState = createSelector(selectMonitorListState, (state) => { + const { monitorTypes, tags, locations, projects, schedules }: MonitorFilterState = + state.pageState; + const { query } = state.pageState; + + return { monitorTypes, tags, locations, projects, schedules, query }; +}); + export const selectMonitorUpsertStatuses = (state: SyntheticsAppState) => state.monitorList.monitorUpsertStatuses; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts index 5ede7bad9bb9ba..69cf734c1592ab 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts @@ -32,7 +32,9 @@ function toMonitorOverviewQueryArgs( query: pageState.query, tags: pageState.tags, locations: pageState.locations, - monitorType: pageState.monitorType, + projects: pageState.projects, + schedules: pageState.schedules, + monitorTypes: pageState.monitorTypes, sortField: pageState.sortField, sortOrder: pageState.sortOrder, searchFields: [], diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts index 3454b0f7c7eee8..b0b6a0f467bf13 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { takeLatest, takeLeading } from 'redux-saga/effects'; +import { takeLatest, debounce } from 'redux-saga/effects'; import { fetchEffectFactory } from '../utils/fetch_effect'; import { fetchMonitorOverviewAction, @@ -16,7 +16,8 @@ import { import { fetchMonitorOverview, fetchOverviewStatus } from './api'; export function* fetchMonitorOverviewEffect() { - yield takeLeading( + yield debounce( + 300, // Only take the latest while ignoring any intermediate triggers [fetchMonitorOverviewAction.get, quietFetchOverviewAction.get], fetchEffectFactory( fetchMonitorOverview, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts index f5454a7d739601..fdc6dcb07df50c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts @@ -7,13 +7,11 @@ import { MonitorOverviewResult, OverviewStatusState } from '../../../../../common/runtime_types'; import { IHttpSerializedFetchError } from '../utils/http_error'; +import { MonitorFilterState } from '../monitor_list'; -export interface MonitorOverviewPageState { +export interface MonitorOverviewPageState extends MonitorFilterState { perPage: number; query?: string; - tags?: string[]; - monitorType?: string[]; - locations?: string[]; sortOrder: 'asc' | 'desc'; sortField: string; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index 6f94e7082d2b73..7cb8cb78583535 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -67,7 +67,8 @@ export const mockState: SyntheticsAppState = { sortField: `${ConfigKey.NAME}.keyword`, sortOrder: 'asc', tags: undefined, - monitorType: undefined, + monitorTypes: undefined, + projects: undefined, locations: undefined, }, monitorUpsertStatuses: {}, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts index c428d43babf682..0af05a77269ad5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts @@ -71,7 +71,9 @@ describe('getSupportedUrlParams', () => { statusFilter: STATUS_FILTER, query: '', locations: [], - monitorType: [], + monitorTypes: [], + projects: [], + schedules: [], tags: [], }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts index 902b296e3db0ed..1441a8ca071f3a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts @@ -28,9 +28,11 @@ export interface SyntheticsUrlParams { query?: string; tags?: string[]; locations?: string[]; - monitorType?: string[]; + monitorTypes?: string[]; status?: string[]; locationId?: string; + projects?: string[]; + schedules?: string[]; } const { @@ -87,9 +89,11 @@ export const getSupportedUrlParams = (params: { focusConnectorField, query, tags, - monitorType, + monitorTypes, locations, locationId, + projects, + schedules, } = filteredParams; return { @@ -114,8 +118,10 @@ export const getSupportedUrlParams = (params: { focusConnectorField: !!focusConnectorField, query: query || '', tags: tags ? JSON.parse(tags) : [], - monitorType: monitorType ? JSON.parse(monitorType) : [], + monitorTypes: monitorTypes ? JSON.parse(monitorTypes) : [], locations: locations ? JSON.parse(locations) : [], + projects: projects ? JSON.parse(projects) : [], + schedules: schedules ? JSON.parse(schedules) : [], locationId: locationId || undefined, }; }; diff --git a/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts b/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts index 0225eeb08acfde..ba5327d200b464 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts @@ -153,6 +153,7 @@ export class StatusRuleExecutor { allMonitorsCount: allIds.length, disabledMonitorsCount: allIds.length, projectMonitorsCount, + allIds, }; } diff --git a/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts b/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts index 4498f09f131017..41f18f5ddd8c6c 100644 --- a/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts +++ b/x-pack/plugins/synthetics/server/queries/query_monitor_status.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { intersection } from 'lodash'; import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; import { SUMMARY_FILTER } from '../../common/constants/client_defaults'; import { UptimeEsClient } from '../legacy_uptime/lib/lib'; @@ -117,7 +118,6 @@ export async function queryMonitorStatus( for await (const response of promises) { response.body.aggregations?.id.buckets.forEach( ({ location, key: queryId }: { location: any; key: string }) => { - const monLocations = monitorLocationsMap?.[queryId]; const locationSummaries = location.buckets.map( ({ status, key: locationName }: { key: string; status: any }) => { const ping = status.hits.hits[0]._source as Ping & { '@timestamp': string }; @@ -125,8 +125,11 @@ export async function queryMonitorStatus( } ) as Array<{ location: string; ping: Ping & { '@timestamp': string } }>; - // discard any locations that are not in the monitorLocationsMap for the given monitor - monLocations?.forEach((monLocation) => { + // discard any locations that are not in the monitorLocationsMap for the given monitor as well as those which are + // in monitorLocationsMap but not in listOfLocations + const monLocations = monitorLocationsMap?.[queryId]; + const monQueriedLocations = intersection(monLocations, listOfLocations); + monQueriedLocations?.forEach((monLocation) => { const locationSummary = locationSummaries.find( (summary) => summary.location === monLocation ); @@ -166,5 +169,5 @@ export async function queryMonitorStatus( } ); } - return { up, down, pending, upConfigs, downConfigs, enabledIds: ids }; + return { up, down, pending, upConfigs, downConfigs, enabledIds: ids, allIds: ids }; } diff --git a/x-pack/plugins/synthetics/server/routes/common.ts b/x-pack/plugins/synthetics/server/routes/common.ts index da8a6b476ceb0d..7ac41cfd843ae8 100644 --- a/x-pack/plugins/synthetics/server/routes/common.ts +++ b/x-pack/plugins/synthetics/server/routes/common.ts @@ -20,8 +20,10 @@ export const QuerySchema = schema.object({ query: schema.maybe(schema.string()), filter: schema.maybe(schema.string()), tags: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), - monitorType: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + monitorTypes: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), locations: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + projects: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + schedules: 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())), @@ -51,19 +53,23 @@ export const getMonitors = ( sortOrder, query, tags, - monitorType, + monitorTypes, locations, filter = '', fields, searchAfter, + projects, + schedules, } = request as MonitorsQuery; const filterStr = getMonitorFilters({ filter, - monitorTypes: monitorType, + monitorTypes, tags, locations, serviceLocations: syntheticsService.locations, + projects, + schedules, }); return savedObjectsClient.find({ @@ -85,13 +91,17 @@ export const getMonitorFilters = ({ ports, filter, locations, + projects, monitorTypes, + schedules, serviceLocations, }: { filter?: string; tags?: string | string[]; monitorTypes?: string | string[]; locations?: string | string[]; + projects?: string | string[]; + schedules?: string | string[]; ports?: string | string[]; serviceLocations: ServiceLocations; }) => { @@ -100,8 +110,10 @@ export const getMonitorFilters = ({ return [ filter, getKqlFilter({ field: 'tags', values: tags }), + getKqlFilter({ field: 'project_id', values: projects }), getKqlFilter({ field: 'type', values: monitorTypes }), getKqlFilter({ field: 'locations.id', values: locationFilter }), + getKqlFilter({ field: 'schedule.number', values: schedules }), ] .filter((f) => !!f) .join(' AND '); @@ -129,7 +141,7 @@ export const getKqlFilter = ({ } if (Array.isArray(values)) { - return `${fieldKey}:"${values.join(`" ${operator} ${fieldKey}:"`)}"`; + return ` (${fieldKey}:"${values.join(`" ${operator} ${fieldKey}:"`)}" )`; } return `${fieldKey}:"${values}"`; @@ -143,7 +155,7 @@ const parseLocationFilter = (serviceLocations: ServiceLocations, locations?: str if (Array.isArray(locations)) { return locations .map((loc) => findLocationItem(loc, serviceLocations)?.id ?? '') - .filter((val) => !val); + .filter((val) => !!val); } return findLocationItem(locations, serviceLocations)?.id ?? ''; @@ -159,15 +171,18 @@ export const findLocationItem = (query: string, locations: ServiceLocations) => * @param monitorQuery { MonitorsQuery } */ export const isMonitorsQueryFiltered = (monitorQuery: MonitorsQuery) => { - const { query, tags, monitorType, locations, status, filter } = monitorQuery; + const { query, tags, monitorTypes, locations, status, filter, projects, schedules } = + monitorQuery; return ( !!query || !!filter || !!locations?.length || - !!monitorType?.length || + !!monitorTypes?.length || !!tags?.length || - !!status?.length + !!status?.length || + !!projects?.length || + !!schedules?.length ); }; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts index 9b328e017b65a3..e2edf19d3ff317 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -6,14 +6,26 @@ */ import { schema } from '@kbn/config-schema'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { getAllMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config'; -import { ConfigKey, MonitorOverviewItem, SyntheticsMonitor } from '../../../common/runtime_types'; +import { + ConfigKey, + EncryptedSyntheticsMonitor, + MonitorOverviewItem, +} from '../../../common/runtime_types'; import { UMServerLibs } from '../../legacy_uptime/lib/lib'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS, SYNTHETICS_API_URLS } from '../../../common/constants'; import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor'; import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors'; -import { getMonitors, isMonitorsQueryFiltered, QuerySchema, SEARCH_FIELDS } from '../common'; +import { + getMonitorFilters, + getMonitors, + isMonitorsQueryFiltered, + MonitorsQuery, + QuerySchema, + SEARCH_FIELDS, +} from '../common'; export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -89,13 +101,24 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory = validate: { query: QuerySchema, }, - handler: async ({ request, savedObjectsClient }): Promise => { - const { sortField, sortOrder, query } = request.query; - const finder = savedObjectsClient.createPointInTimeFinder({ - type: syntheticsMonitorType, - sortField: sortField === 'status' ? `${ConfigKey.NAME}.keyword` : sortField, + handler: async ({ request, savedObjectsClient, syntheticsMonitorClient }): Promise => { + const { + sortField, sortOrder, - perPage: 1000, + query, + locations: queriedLocations, + } = request.query as MonitorsQuery; + + const filtersStr = getMonitorFilters({ + ...request.query, + serviceLocations: syntheticsMonitorClient.syntheticsService.locations, + }); + + const allMonitorConfigs = await getAllMonitors({ + sortOrder, + filter: filtersStr, + soClient: savedObjectsClient, + sortField: sortField === 'status' ? `${ConfigKey.NAME}.keyword` : sortField, search: query ? `${query}*` : undefined, searchFields: SEARCH_FIELDS, }); @@ -104,29 +127,13 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory = let total = 0; const allMonitors: MonitorOverviewItem[] = []; - for await (const result of finder.find()) { - /* collect all monitor ids for use - * in filtering overview requests */ - result.saved_objects.forEach((monitor) => { - const id = monitor.attributes[ConfigKey.MONITOR_QUERY_ID]; - const configId = monitor.attributes[ConfigKey.CONFIG_ID]; - allMonitorIds.push(configId); - - /* for each location, add a config item */ - const locations = monitor.attributes[ConfigKey.LOCATIONS]; - locations.forEach((location) => { - const config = { - id, - configId, - name: monitor.attributes[ConfigKey.NAME], - location, - isEnabled: monitor.attributes[ConfigKey.ENABLED], - isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]), - }; - allMonitors.push(config); - total++; - }); - }); + for (const { attributes } of allMonitorConfigs) { + const configId = attributes[ConfigKey.CONFIG_ID]; + allMonitorIds.push(configId); + + const monitorConfigsPerLocation = getOverviewConfigsPerLocation(attributes, queriedLocations); + allMonitors.push(...monitorConfigsPerLocation); + total += monitorConfigsPerLocation.length; } return { @@ -136,3 +143,34 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory = }; }, }); + +function getOverviewConfigsPerLocation( + attributes: EncryptedSyntheticsMonitor, + queriedLocations: string | string[] | undefined +) { + const id = attributes[ConfigKey.MONITOR_QUERY_ID]; + const configId = attributes[ConfigKey.CONFIG_ID]; + + /* for each location, add a config item */ + const locations = attributes[ConfigKey.LOCATIONS]; + const queriedLocationsArray = + queriedLocations && !Array.isArray(queriedLocations) ? [queriedLocations] : queriedLocations; + + /* exclude nob matching locations if location filter is present */ + const filteredLocations = queriedLocationsArray?.length + ? locations.filter( + (loc) => + (loc.label && queriedLocationsArray.includes(loc.label)) || + queriedLocationsArray.includes(loc.id) + ) + : locations; + + return filteredLocations.map((location) => ({ + id, + configId, + name: attributes[ConfigKey.NAME], + location, + isEnabled: attributes[ConfigKey.ENABLED], + isStatusAlertEnabled: isStatusEnabled(attributes[ConfigKey.ALERT_CONFIG]), + })); +} diff --git a/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts b/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts index d2c08d495973c1..b86d611edf32f5 100644 --- a/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts +++ b/x-pack/plugins/synthetics/server/routes/status/current_status.test.ts @@ -178,6 +178,7 @@ describe('current status route', () => { pending: 0, down: 1, enabledIds: ['id1', 'id2'], + allIds: ['id1', 'id2'], up: 2, upConfigs: { 'id1-Asia/Pacific - Japan': { @@ -321,18 +322,24 @@ describe('current status route', () => { * * The expectation here is we will send the test client two separate "requests", one for each of the two IDs. */ + const concernedLocations = [ + 'Asia/Pacific - Japan', + 'Europe - Germany', + 'Asia/Pacific - Japan', + ]; expect( await queryMonitorStatus( uptimeEsClient, - times(10000).map((n) => 'Europe - Germany' + n), + [...concernedLocations, ...times(9997).map((n) => 'Europe - Germany' + n)], { from: 2500, to: 'now' }, ['id1', 'id2'], - { id1: ['Asia/Pacific - Japan'], id2: ['Europe - Germany', 'Asia/Pacific - Japan'] } + { id1: [concernedLocations[0]], id2: [concernedLocations[1], concernedLocations[2]] } ) ).toEqual({ pending: 0, down: 1, enabledIds: ['id1', 'id2'], + allIds: ['id1', 'id2'], up: 2, upConfigs: { 'id1-Asia/Pacific - Japan': { diff --git a/x-pack/plugins/synthetics/server/routes/status/current_status.ts b/x-pack/plugins/synthetics/server/routes/status/current_status.ts index a2554cffc652c8..48874fb83f675b 100644 --- a/x-pack/plugins/synthetics/server/routes/status/current_status.ts +++ b/x-pack/plugins/synthetics/server/routes/status/current_status.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { intersection } from 'lodash'; import datemath, { Unit } from '@kbn/datemath'; import { SavedObjectsClientContract } from '@kbn/core/server'; import { @@ -19,7 +20,7 @@ import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes'; import { UptimeEsClient } from '../../legacy_uptime/lib/lib'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { ConfigKey } from '../../../common/runtime_types'; -import { QuerySchema, MonitorsQuery } from '../common'; +import { QuerySchema, MonitorsQuery, getMonitorFilters } from '../common'; /** * Helper function that converts a monitor's schedule to a value to use to generate @@ -46,7 +47,7 @@ export async function getStatus( syntheticsMonitorClient: SyntheticsMonitorClient, params: MonitorsQuery ) { - const { query } = params; + const { query, locations: queryLocations } = params; /** * Walk through all monitor saved objects, bucket IDs by disabled/enabled status. * @@ -54,9 +55,14 @@ export async function getStatus( * latest ping for all enabled monitors. */ + const filtersStr = getMonitorFilters({ + ...params, + serviceLocations: syntheticsMonitorClient.syntheticsService.locations, + }); const allMonitors = await getAllMonitors({ soClient, search: query ? `${query}*` : undefined, + filter: filtersStr, fields: [ ConfigKey.ENABLED, ConfigKey.LOCATIONS, @@ -67,6 +73,7 @@ export async function getStatus( }); const { + allIds, enabledIds, disabledCount, maxPeriod, @@ -76,15 +83,23 @@ export async function getStatus( projectMonitorsCount, } = await processMonitors(allMonitors, server, soClient, syntheticsMonitorClient); + // Account for locations filter + const queryLocationsArray = + queryLocations && !Array.isArray(queryLocations) ? [queryLocations] : queryLocations; + const listOfLocationAfterFilter = queryLocationsArray + ? intersection(listOfLocations, queryLocationsArray) + : listOfLocations; + const { up, down, pending, upConfigs, downConfigs } = await queryMonitorStatus( uptimeEsClient, - listOfLocations, + listOfLocationAfterFilter, { from: maxPeriod, to: 'now' }, enabledIds, monitorLocationMap ); return { + allIds, allMonitorsCount: allMonitors.length, disabledMonitorsCount, projectMonitorsCount, diff --git a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts index d2c28d811eb546..4bb52d9090906f 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts @@ -27,12 +27,15 @@ export const getAllMonitors = async ({ soClient, search, fields, + filter, sortField, sortOrder, + searchFields, }: { soClient: SavedObjectsClientContract; search?: string; -} & Pick) => { + filter?: string; +} & Pick) => { const finder = soClient.createPointInTimeFinder({ type: syntheticsMonitorType, perPage: 1000, @@ -40,6 +43,8 @@ export const getAllMonitors = async ({ sortField, sortOrder, fields, + filter, + searchFields, }); const hits: Array> = []; From 7ae33a75ac6418271f46f3c6cf7aee0cf66068cb Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 1 Feb 2023 18:50:53 +0100 Subject: [PATCH 59/59] [Cases] Create internal endpoint to get user action stats (#149863) Fixes #149390 ## Summary This PR creates an internal API to get the count of the different user actions associated with the current case. This will be used to help filter and paginate the case activity. aux **Endpoint:** `GET /internal/cases//user_actions/_stats` Example Response: ``` { total: 3 total_comments: 2 total_other_actions: 1 } ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../common/api/cases/user_actions/response.ts | 4 + .../common/api/cases/user_actions/stats.ts | 16 ++ x-pack/plugins/cases/common/api/helpers.ts | 5 + x-pack/plugins/cases/common/constants.ts | 2 + .../__snapshots__/audit_logger.test.ts.snap | 84 +++++++ .../cases/server/authorization/index.ts | 8 + .../cases/server/authorization/types.ts | 1 + x-pack/plugins/cases/server/client/mocks.ts | 3 +- .../server/client/user_actions/client.ts | 8 + .../cases/server/client/user_actions/stats.ts | 39 ++++ .../server/routes/api/get_internal_routes.ts | 2 + .../internal/get_case_user_actions_stats.ts | 36 +++ x-pack/plugins/cases/server/services/mocks.ts | 1 + .../server/services/user_actions/index.ts | 54 +++++ .../common/lib/user_actions.ts | 21 ++ .../security_and_spaces/tests/common/index.ts | 1 + .../user_actions/get_user_action_stats.ts | 206 ++++++++++++++++++ .../security_and_spaces/tests/trial/index.ts | 1 + .../trial/internal/get_user_action_stats.ts | 132 +++++++++++ 19 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/cases/common/api/cases/user_actions/stats.ts create mode 100644 x-pack/plugins/cases/server/client/user_actions/stats.ts create mode 100644 x-pack/plugins/cases/server/routes/api/internal/get_case_user_actions_stats.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_user_action_stats.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_user_action_stats.ts diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/response.ts b/x-pack/plugins/cases/common/api/cases/user_actions/response.ts index 5a2b2cd497d5b4..8f0ef5517db4f5 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/response.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/response.ts @@ -8,6 +8,7 @@ import * as rt from 'io-ts'; import type { ActionsRt, ActionTypeValues } from './common'; + import { CaseUserActionInjectedIdsRt, CaseUserActionInjectedDeprecatedIdsRt, @@ -25,6 +26,7 @@ import { StatusUserActionRt } from './status'; import { DeleteCaseUserActionRt } from './delete_case'; import { SeverityUserActionRt } from './severity'; import { AssigneesUserActionRt } from './assignees'; +import { CaseUserActionStatsRt } from './stats'; const CommonUserActionsRt = rt.union([ DescriptionUserActionRt, @@ -83,11 +85,13 @@ const CaseUserActionResponseRt = rt.intersection([ const CaseUserActionAttributesRt = CaseUserActionBasicRt; export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRt); export const CaseUserActionsDeprecatedResponseRt = rt.array(CaseUserActionDeprecatedResponseRt); +export const CaseUserActionStatsResponseRt = CaseUserActionStatsRt; export type CaseUserActionAttributes = rt.TypeOf; export type CaseUserActionAttributesWithoutConnectorId = rt.TypeOf< typeof CaseUserActionBasicWithoutConnectorIdRt >; +export type CaseUserActionStatsResponse = rt.TypeOf; export type CaseUserActionsResponse = rt.TypeOf; export type CaseUserActionResponse = rt.TypeOf; export type CaseUserActionsDeprecatedResponse = rt.TypeOf< diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/stats.ts b/x-pack/plugins/cases/common/api/cases/user_actions/stats.ts new file mode 100644 index 00000000000000..de0a6439e0bdba --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/user_actions/stats.ts @@ -0,0 +1,16 @@ +/* + * 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 rt from 'io-ts'; + +export const CaseUserActionStatsRt = rt.type({ + total: rt.number, + total_comments: rt.number, + total_other_actions: rt.number, +}); + +export type CaseUserActionStats = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts index ae1f89f4071b71..746f2bd6972abd 100644 --- a/x-pack/plugins/cases/common/api/helpers.ts +++ b/x-pack/plugins/cases/common/api/helpers.ts @@ -16,6 +16,7 @@ import { CASE_ALERTS_URL, CASE_COMMENT_DELETE_URL, CASE_FIND_USER_ACTIONS_URL, + INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL, INTERNAL_BULK_GET_ATTACHMENTS_URL, INTERNAL_CONNECTORS_URL, } from '../constants'; @@ -44,6 +45,10 @@ export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; +export const getCaseUserActionStatsUrl = (id: string): string => { + return INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL.replace('{case_id}', id); +}; + export const getCaseFindUserActionsUrl = (id: string): string => { return CASE_FIND_USER_ACTIONS_URL.replace('{case_id}', id); }; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 1bb91af242cdb4..c359353f80b807 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -94,6 +94,8 @@ export const INTERNAL_SUGGEST_USER_PROFILES_URL = `${CASES_INTERNAL_URL}/_suggest_user_profiles` as const; export const INTERNAL_CONNECTORS_URL = `${CASES_INTERNAL_URL}/{case_id}/_connectors` as const; export const INTERNAL_BULK_GET_CASES_URL = `${CASES_INTERNAL_URL}/_bulk_get` as const; +export const INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL = + `${CASES_INTERNAL_URL}/{case_id}/user_actions/_stats` as const; /** * Action routes diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index 1cfc6634e372e7..193451e9d146a1 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -2184,6 +2184,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_user_action_get_stats", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-user-actions", + }, + }, + "message": "Failed attempt to access cases-user-actions [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_user_action_get_stats", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a user actions as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_user_action_get_stats", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-user-actions", + }, + }, + "message": "User has accessed cases-user-actions [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_user_action_get_stats", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a user actions as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index ce9f5cae4d84a1..cbfef6f4713e3d 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -359,4 +359,12 @@ export const Operations: Record; const createUserActionsSubClientMock = (): UserActionsSubClientMock => { return { + find: jest.fn(), getAll: jest.fn(), getConnectors: jest.fn(), - find: jest.fn(), + stats: jest.fn(), }; }; diff --git a/x-pack/plugins/cases/server/client/user_actions/client.ts b/x-pack/plugins/cases/server/client/user_actions/client.ts index 7a091cb43a0d2a..2676955719adb8 100644 --- a/x-pack/plugins/cases/server/client/user_actions/client.ts +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -7,12 +7,14 @@ import type { GetCaseConnectorsResponse, + CaseUserActionStatsResponse, UserActionFindResponse, CaseUserActionsDeprecatedResponse, } from '../../../common/api'; import type { CasesClientArgs } from '../types'; import { get } from './get'; import { getConnectors } from './connectors'; +import { getStats } from './stats'; import type { GetConnectorsRequest, UserActionFind, UserActionGet } from './types'; import { find } from './find'; import type { CasesClient } from '../client'; @@ -30,6 +32,11 @@ export interface UserActionsSubClient { * Retrieves all the connectors used within a given case */ getConnectors(params: GetConnectorsRequest): Promise; + + /** + * Retrieves the total of comments and user actions in a given case + */ + stats(params: UserActionGet): Promise; } /** @@ -43,6 +50,7 @@ export const createUserActionsSubClient = ( find: (params) => find(params, casesClient, clientArgs), getAll: (params) => get(params, clientArgs), getConnectors: (params) => getConnectors(params, clientArgs), + stats: (params) => getStats(params, casesClient, clientArgs), }; return Object.freeze(attachmentSubClient); diff --git a/x-pack/plugins/cases/server/client/user_actions/stats.ts b/x-pack/plugins/cases/server/client/user_actions/stats.ts new file mode 100644 index 00000000000000..b93f4a19cda204 --- /dev/null +++ b/x-pack/plugins/cases/server/client/user_actions/stats.ts @@ -0,0 +1,39 @@ +/* + * 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 { CaseUserActionStatsResponse } from '../../../common/api'; +import { CaseUserActionStatsResponseRt } from '../../../common/api'; +import { createCaseError } from '../../common/error'; +import type { CasesClientArgs } from '..'; +import type { UserActionGet } from './types'; +import type { CasesClient } from '../client'; + +export const getStats = async ( + { caseId }: UserActionGet, + casesClient: CasesClient, + clientArgs: CasesClientArgs +): Promise => { + const { + services: { userActionService }, + logger, + } = clientArgs; + + try { + await casesClient.cases.resolve({ id: caseId, includeComments: false }); + const totals = await userActionService.getCaseUserActionStats({ + caseId, + }); + + return CaseUserActionStatsResponseRt.encode(totals); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve user action stats for case id: ${caseId}: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts index 8ad54a93bedeef..41d9904de01bda 100644 --- a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts +++ b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts @@ -7,6 +7,7 @@ import type { UserProfileService } from '../../services'; import { getConnectorsRoute } from './internal/get_connectors'; +import { getCaseUserActionStatsRoute } from './internal/get_case_user_actions_stats'; import { bulkCreateAttachmentsRoute } from './internal/bulk_create_attachments'; import { bulkGetCasesRoute } from './internal/bulk_get_cases'; import { suggestUserProfilesRoute } from './internal/suggest_user_profiles'; @@ -19,5 +20,6 @@ export const getInternalRoutes = (userProfileService: UserProfileService) => suggestUserProfilesRoute(userProfileService), getConnectorsRoute, bulkGetCasesRoute, + getCaseUserActionStatsRoute, bulkGetAttachmentsRoute, ] as CaseRoute[]; diff --git a/x-pack/plugins/cases/server/routes/api/internal/get_case_user_actions_stats.ts b/x-pack/plugins/cases/server/routes/api/internal/get_case_user_actions_stats.ts new file mode 100644 index 00000000000000..58d92c5f9d7293 --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/internal/get_case_user_actions_stats.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import { INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; + +export const getCaseUserActionStatsRoute = createCasesRoute({ + method: 'get', + path: INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const casesContext = await context.cases; + const casesClient = await casesContext.getCasesClient(); + const caseId = request.params.case_id; + + return response.ok({ + body: await casesClient.userActions.stats({ caseId }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve stats in route case id: ${request.params.case_id}: ${error}`, + error, + }); + } + }, +}); diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 9e10c1e884a9e4..ee65ce172c596a 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -124,6 +124,7 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => { getAll: jest.fn(), getUniqueConnectors: jest.fn(), getUserActionIdsForCases: jest.fn(), + getCaseUserActionStats: jest.fn(), }; // the cast here is required because jest.Mocked tries to include private members and would throw an error diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index a96f113305444b..159cb40a5f1f86 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -88,6 +88,16 @@ interface ConnectorFieldsBeforePushAggsResult { }; } +interface UserActionsStatsAggsResult { + total: number; + totals: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; +} + export class CaseUserActionService { private readonly _creator: UserActionPersister; private readonly _finder: UserActionFinder; @@ -661,4 +671,48 @@ export class CaseUserActionService { }, }; } + + public async getCaseUserActionStats({ caseId }: { caseId: string }) { + const response = await this.context.unsecuredSavedObjectsClient.find< + CaseUserActionAttributesWithoutConnectorId, + UserActionsStatsAggsResult + >({ + type: CASE_USER_ACTION_SAVED_OBJECT, + hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: CaseUserActionService.buildUserActionStatsAgg(), + }); + + const result = { + total: response.total, + total_comments: 0, + total_other_actions: 0, + }; + + response.aggregations?.totals.buckets.forEach(({ key, doc_count: docCount }) => { + if (key === 'user') { + result.total_comments = docCount; + } + }); + + result.total_other_actions = result.total - result.total_comments; + + return result; + } + + private static buildUserActionStatsAgg(): Record< + string, + estypes.AggregationsAggregationContainer + > { + return { + totals: { + terms: { + field: `${CASE_USER_ACTION_SAVED_OBJECT}.attributes.payload.comment.type`, + size: 100, + }, + }, + }; + } } diff --git a/x-pack/test/cases_api_integration/common/lib/user_actions.ts b/x-pack/test/cases_api_integration/common/lib/user_actions.ts index 54e528b4336ba8..d471fafca6b91e 100644 --- a/x-pack/test/cases_api_integration/common/lib/user_actions.ts +++ b/x-pack/test/cases_api_integration/common/lib/user_actions.ts @@ -12,6 +12,8 @@ import { getCaseUserActionUrl, CaseUserActionDeprecatedResponse, CaseUserActionsDeprecatedResponse, + getCaseUserActionStatsUrl, + CaseUserActionStatsResponse, } from '@kbn/cases-plugin/common/api'; import type SuperTest from 'supertest'; import { User } from './authentication/types'; @@ -68,3 +70,22 @@ export const findCaseUserActions = async ({ return userActions; }; + +export const getCaseUserActionStats = async ({ + supertest, + caseID, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + caseID: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: userActionStats } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionStatsUrl(caseID)}`) + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return userActionStats; +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index 0d3bb17ef23093..b4392c95d5351b 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -31,6 +31,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/tags/get_tags')); loadTestFile(require.resolve('./user_actions/get_all_user_actions')); loadTestFile(require.resolve('./user_actions/find_user_actions')); + loadTestFile(require.resolve('./user_actions/get_user_action_stats')); loadTestFile(require.resolve('./configure/get_configure')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_user_action_stats.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_user_action_stats.ts new file mode 100644 index 00000000000000..0f004820ad52bf --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_user_action_stats.ts @@ -0,0 +1,206 @@ +/* + * 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 { + CaseResponse, + CaseSeverity, + CaseStatuses, + ConnectorTypes, +} from '@kbn/cases-plugin/common/api'; + +import { + globalRead, + obsSec, + obsSecRead, + noKibanaPrivileges, + secOnly, + secOnlyRead, + superUser, + obsOnly, + obsOnlyRead, +} from '../../../../common/lib/authentication/users'; +import { getCaseUserActionStats } from '../../../../common/lib/user_actions'; +import { + getPostCaseRequest, + persistableStateAttachment, + postCaseReq, + postCommentActionsReq, + postCommentAlertReq, + postCommentUserReq, + postExternalReferenceESReq, +} from '../../../../common/lib/mock'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + bulkCreateAttachments, + createCase, + createComment, + deleteAllCaseItems, + updateCase, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; + +const getCaseUpdateData = (id: string, version: string) => ({ + status: CaseStatuses.open, + severity: CaseSeverity.MEDIUM, + title: 'new title', + description: 'new desc', + settings: { + syncAlerts: false, + }, + tags: ['one', 'two'], + connector: { + id: 'my-id', + name: 'Jira', + type: ConnectorTypes.jira as const, + fields: null, + }, + id, + version, +}); + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_user_action_stats', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('returns correct total for comments', async () => { + // 1 creation action + const theCase = await createCase(supertest, postCaseReq); + + await bulkCreateAttachments({ + supertest, + caseId: theCase.id, + params: [ + // Only this one should show up in total_comments + postCommentUserReq, + // The ones below count as total_other_actions + postExternalReferenceESReq, + persistableStateAttachment, + postCommentActionsReq, + postCommentAlertReq, + ], + }); + + const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id }); + + expect(userActionTotals.total).to.equal(6); + expect(userActionTotals.total_comments).to.equal(1); + expect(userActionTotals.total_other_actions).to.equal(5); + expect(userActionTotals.total).to.equal( + userActionTotals.total_comments + userActionTotals.total_other_actions + ); + }); + + it('returns the correct stats when a case update occurs', async () => { + // 1 creation action + const theCase = await createCase(supertest, postCaseReq); + + // this update should account for 7 "other actions" + await updateCase({ + supertest, + params: { + cases: [getCaseUpdateData(theCase.id, theCase.version)], + }, + }); + + await bulkCreateAttachments({ + supertest, + caseId: theCase.id, + params: [ + // only this one should show up in total_comments + postCommentUserReq, + // the ones below count as total_other_actions + postExternalReferenceESReq, + persistableStateAttachment, + postCommentActionsReq, + postCommentAlertReq, + ], + }); + + const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id }); + + expect(userActionTotals.total).to.equal(13); + expect(userActionTotals.total_comments).to.equal(1); + expect(userActionTotals.total_other_actions).to.equal(12); + expect(userActionTotals.total).to.equal( + userActionTotals.total_comments + userActionTotals.total_other_actions + ); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let theCase: CaseResponse; + beforeEach(async () => { + theCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [getCaseUpdateData(theCase.id, theCase.version)], + }, + auth: superUserSpace1Auth, + }); + + await createComment({ + supertest, + caseId: theCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActionTotals = await getCaseUserActionStats({ + supertest: supertestWithoutAuth, + caseID: theCase.id, + auth: { user, space: 'space1' }, + }); + + expect(userActionTotals.total).to.equal(9); + expect(userActionTotals.total_comments).to.equal(1); + expect(userActionTotals.total_other_actions).to.equal(8); + expect(userActionTotals.total).to.equal( + userActionTotals.total_comments + userActionTotals.total_other_actions + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + { user: obsOnly, space: 'space1' }, + { user: obsOnlyRead, space: 'space1' }, + ]) { + it(`should 403 when requesting the user action stats of a case with user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await getCaseUserActionStats({ + supertest: supertestWithoutAuth, + caseID: theCase.id, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index 4a3a2f876f843e..a167b8ded642aa 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -38,6 +38,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./user_profiles/get_current')); // Internal routes + loadTestFile(require.resolve('./internal/get_user_action_stats')); loadTestFile(require.resolve('./internal/suggest_user_profiles')); loadTestFile(require.resolve('./internal/get_connectors')); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_user_action_stats.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_user_action_stats.ts new file mode 100644 index 00000000000000..370cd2a98149b0 --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/internal/get_user_action_stats.ts @@ -0,0 +1,132 @@ +/* + * 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 http from 'http'; +import expect from '@kbn/expect'; + +import { ConnectorTypes } from '@kbn/cases-plugin/common/api'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { createCase, deleteAllCaseItems, pushCase, updateCase } from '../../../../common/lib/utils'; +import { postCaseReq } from '../../../../common/lib/mock'; +import { + createCaseWithConnector, + createConnector, + getJiraConnector, + getServiceNowSimulationServer, +} from '../../../../common/lib/connectors'; +import { getCaseUserActionStats } from '../../../../common/lib/user_actions'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const actionsRemover = new ActionsRemover(supertest); + + describe('get_case_user_action_stats', () => { + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; + + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + await actionsRemover.removeAll(); + }); + + after(async () => { + serviceNowServer.close(); + }); + + it('connectors are counted in total_other_actions', async () => { + const [{ postedCase, connector: serviceNowConnector }, jiraConnector] = await Promise.all([ + createCaseWithConnector({ + supertest, + serviceNowSimulatorURL, + actionsRemover, + }), + createConnector({ + supertest, + req: { + ...getJiraConnector(), + }, + }), + ]); + + actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); + + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: serviceNowConnector.id, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: theCase.id, + version: theCase.version, + connector: { + id: jiraConnector.id, + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, + }, + ], + }, + }); + + const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id }); + + expect(userActionTotals.total).to.equal(3); + expect(userActionTotals.total_comments).to.equal(0); + expect(userActionTotals.total_other_actions).to.equal(3); + expect(userActionTotals.total).to.equal( + userActionTotals.total_comments + userActionTotals.total_other_actions + ); + }); + + it('assignees are counted in total_other_actions', async () => { + // 1 creation action + const theCase = await createCase(supertest, postCaseReq); + + // 1 assignee action + await updateCase({ + supertest, + params: { + cases: [ + { + assignees: [ + { + uid: '123', + }, + ], + id: theCase.id, + version: theCase.version, + }, + ], + }, + }); + + const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id }); + + expect(userActionTotals.total).to.equal(2); + expect(userActionTotals.total_comments).to.equal(0); + expect(userActionTotals.total_other_actions).to.equal(2); + expect(userActionTotals.total).to.equal( + userActionTotals.total_comments + userActionTotals.total_other_actions + ); + }); + }); +};