diff --git a/.buildkite/scripts/steps/artifacts/docker_image.sh b/.buildkite/scripts/steps/artifacts/docker_image.sh index 1744a02fe4243..671704b6c50e8 100755 --- a/.buildkite/scripts/steps/artifacts/docker_image.sh +++ b/.buildkite/scripts/steps/artifacts/docker_image.sh @@ -76,19 +76,19 @@ buildkite-agent artifact upload "kibana-$BASE_VERSION-docker-image-aarch64.tar.g buildkite-agent artifact upload "dependencies-$GIT_ABBREV_COMMIT.csv" cd - +# This part is related with updating the configuration of kibana-controller, +# so that new stack instances contain the latest and greatest image of kibana, +# and the respective stack components of course. echo "--- Trigger image tag update" if [[ "$BUILDKITE_BRANCH" == "$KIBANA_BASE_BRANCH" ]]; then - cat << EOF | buildkite-agent pipeline upload steps: - - trigger: k8s-gitops-update-image-tag + - trigger: serverless-gitops-update-stack-image-tag async: true label: ":argo: Update image tag for Kibana" branches: main build: env: - MODE: sed - TARGET_FILE: kibana-controller.yaml IMAGE_TAG: "git-$GIT_ABBREV_COMMIT" SERVICE: kibana-controller NAMESPACE: kibana-ci diff --git a/src/plugins/vis_types/timeseries/common/empty_label.test.ts b/src/plugins/vis_types/timeseries/common/empty_label.test.ts new file mode 100644 index 0000000000000..1eb83759ff23b --- /dev/null +++ b/src/plugins/vis_types/timeseries/common/empty_label.test.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 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 { getValueOrEmpty } from './empty_label'; + +describe('getValueOrEmpty', () => { + test('returns the value if not empty or slash value given', () => { + expect(getValueOrEmpty('/test/blog')).toEqual('/test/blog'); + }); + + test('returns (empty) if value is slash', () => { + expect(getValueOrEmpty('/')).toEqual('(empty)'); + }); + + test('returns (empty) if value is empty', () => { + expect(getValueOrEmpty('')).toEqual('(empty)'); + }); + + test('returns (empty) if value is null', () => { + expect(getValueOrEmpty(null)).toEqual('(empty)'); + }); +}); diff --git a/src/plugins/vis_types/timeseries/common/empty_label.ts b/src/plugins/vis_types/timeseries/common/empty_label.ts index d95e8fe3f7f16..8f200419855e0 100644 --- a/src/plugins/vis_types/timeseries/common/empty_label.ts +++ b/src/plugins/vis_types/timeseries/common/empty_label.ts @@ -13,7 +13,7 @@ export const emptyLabel = i18n.translate('visTypeTimeseries.emptyTextValue', { }); export const getValueOrEmpty = (value: unknown) => { - if (value === '' || value === null || value === undefined) { + if (value === '' || value === '/' || value === null || value === undefined) { return emptyLabel; } return `${value}`; diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/chart_component.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/chart_component.tsx index 6cc620a804691..e22249b9fa8ac 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/chart_component.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/chart_component.tsx @@ -31,35 +31,39 @@ export const ChartComponent: FC = React.memo(({ annotation const timeRange = useTimeRangeUpdates(); const { dataView } = useDataSource(); - const { requestParams, bucketInterval } = useChangePointDetectionContext(); + const { requestParams, bucketInterval, resultQuery, resultFilters } = + useChangePointDetectionContext(); const filters = useMemo(() => { - return annotation.group - ? [ - { - meta: { - index: dataView.id!, - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: annotation.group.name, - params: { - query: annotation.group.value, + return [ + ...resultFilters, + ...(annotation.group + ? [ + { + meta: { + index: dataView.id!, + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: annotation.group.name, + params: { + query: annotation.group.value, + }, }, - }, - query: { - match_phrase: { - [annotation.group.name]: annotation.group.value, + query: { + match_phrase: { + [annotation.group.name]: annotation.group.value, + }, + }, + $state: { + store: FilterStateStore.APP_STATE, }, }, - $state: { - store: FilterStateStore.APP_STATE, - }, - }, - ] - : []; - }, [dataView.id, annotation.group]); + ] + : []), + ]; + }, [dataView.id, annotation.group, resultFilters]); // @ts-ignore incorrect types for attributes const attributes = useMemo(() => { @@ -151,10 +155,7 @@ export const ChartComponent: FC = React.memo(({ annotation : []), ], }, - query: { - query: '', - language: 'kuery', - }, + query: resultQuery, filters, datasourceStates: { formBased: { @@ -202,13 +203,26 @@ export const ChartComponent: FC = React.memo(({ annotation adHocDataViews: {}, }, }; - }, [dataView.id, dataView.timeFieldName, annotation, requestParams, filters, bucketInterval]); + }, [ + annotation.group?.value, + annotation.timestamp, + annotation.label, + dataView.id, + dataView.timeFieldName, + resultQuery, + filters, + bucketInterval.expression, + requestParams.fn, + requestParams.metricField, + ]); return ( { + const groupsStr = groups + .map(({ anchor, text }) => `- `) + .join('\n'); + + return `--- + id: uiMlKibanaRestApi + slug: /ml-team/docs/ui/rest-api/ml-kibana-rest-api + title: Machine Learning Kibana REST API + image: https://source.unsplash.com/400x175/?Nature + description: This page contains documentation for the ML Kibana REST API. + date: ${moment().format('YYYY-MM-DD')} + tags: ['machine learning','internal docs', 'UI'] + --- + + _Updated for ${kibanaPackageJson.version}_ + + Some of the features of the Machine Learning (ML) Kibana plugin are provided via a REST API, which is ideal for creating an integration with the ML plugin. + + Each API is experimental and can include breaking changes in any version of the ML plugin, or might have been entirely removed from the plugin. + + - + + The following APIs are available: + +${groupsStr} + `; +}; + +export const generateContentPage = () => { + const doc = createDoc({ + src: path.resolve(__dirname, '..', '..', '..', 'server', 'routes'), + config: path.resolve(__dirname, '..', 'apidoc_config', 'apidoc.json'), + // if you don't want to generate the output files: + dryRun: true, + // if you don't want to see any log output: + silent: true, + }); + + const groups = [...new Set(doc.data.map((v) => v.group))].map((group) => { + return { + anchor: `-${group.toLowerCase()}`, + text: group.replace(/([a-z])([A-Z])/g, '$1 $2'), + }; + }); + + fs.writeFileSync(path.resolve(__dirname, '..', 'ml_kibana_api.mdx'), getContent(groups)); +}; diff --git a/x-pack/plugins/ml/scripts/apidoc_scripts/content_page/index.js b/x-pack/plugins/ml/scripts/apidoc_scripts/content_page/index.js new file mode 100644 index 0000000000000..5b0aced7aed1b --- /dev/null +++ b/x-pack/plugins/ml/scripts/apidoc_scripts/content_page/index.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../../src/setup_node_env'); +require('./content_page').generateContentPage(); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5c903966bd0cc..8794e81e4a441 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -61,6 +61,7 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000 as const; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100 as const; export const SECURITY_FEATURE_ID = 'Security' as const; export const SECURITY_TAG_NAME = 'Security Solution' as const; +export const SECURITY_TAG_DESCRIPTION = 'Security Solution auto-generated tag' as const; export const DEFAULT_SPACE_ID = 'default' as const; export const DEFAULT_RELATIVE_DATE_THRESHOLD = 24 as const; @@ -303,6 +304,10 @@ export const prebuiltSavedObjectsBulkCreateUrl = (templateName: string) => export const PREBUILT_SAVED_OBJECTS_BULK_DELETE = `${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_delete/{template_name}`; export const prebuiltSavedObjectsBulkDeleteUrl = (templateName: string) => `${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_delete/${templateName}` as const; + +export const INTERNAL_DASHBOARDS_URL = `/internal/dashboards` as const; +export const INTERNAL_TAGS_URL = `/internal/tags`; + export const RISK_SCORE_CREATE_INDEX = `${INTERNAL_RISK_SCORE_URL}/indices/create`; export const RISK_SCORE_DELETE_INDICES = `${INTERNAL_RISK_SCORE_URL}/indices/delete`; export const RISK_SCORE_CREATE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/create`; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts index e27ad7cbfc20e..9359e93f7fe47 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_selection.cy.ts @@ -20,7 +20,8 @@ import { cleanKibana } from '../../tasks/common'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -describe('Rules selection', () => { +// TODO: See https://github.com/elastic/kibana/issues/154694 +describe.skip('Rules selection', () => { beforeEach(() => { cleanKibana(); login(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_table_auto_refresh.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_table_auto_refresh.cy.ts index b11ba39323507..38728635b63cf 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_table_auto_refresh.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rules_table_auto_refresh.cy.ts @@ -33,7 +33,8 @@ import { setRowsPerPageTo } from '../../tasks/table_pagination'; const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; -describe('Alerts detection rules table auto-refresh', () => { +// TODO: See https://github.com/elastic/kibana/issues/154694 +describe.skip('Alerts detection rules table auto-refresh', () => { before(() => { cleanKibana(); login(); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/__mocks__/utils.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/__mocks__/utils.ts new file mode 100644 index 0000000000000..eb8263c9f9adc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/__mocks__/utils.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MOCK_TAG_ID, DEFAULT_DASHBOARDS_RESPONSE } from '../api/__mocks__'; + +export const getSecurityTagIds = jest.fn().mockResolvedValue([MOCK_TAG_ID]); + +export const getSecurityDashboards = jest.fn().mockResolvedValue(DEFAULT_DASHBOARDS_RESPONSE); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/api/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/__mocks__/index.ts new file mode 100644 index 0000000000000..7dd6d2c9132ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/__mocks__/index.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. + */ + +import { SECURITY_TAG_NAME, SECURITY_TAG_DESCRIPTION } from '../../../../../../common/constants'; + +export const MOCK_TAG_ID = 'securityTagId'; + +export const DEFAULT_TAGS_RESPONSE = [ + { + id: MOCK_TAG_ID, + name: SECURITY_TAG_NAME, + description: SECURITY_TAG_DESCRIPTION, + color: '#2c7b82', + }, +]; + +export const DEFAULT_DASHBOARDS_RESPONSE = [ + { + type: 'dashboard', + id: 'c0ac2c00-c1c0-11e7-8995-936807a28b16-ecs', + namespaces: ['default'], + attributes: { + description: 'Summary of Linux kernel audit events.', + title: '[Auditbeat Auditd] Overview ECS', + version: 1, + }, + references: [ + { + name: 'tag-ref-ba964280-d211-11ed-890b-153ddf1a08e9', + id: 'ba964280-d211-11ed-890b-153ddf1a08e9', + type: 'tag', + }, + ], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.7.0', + updated_at: '2023-04-03T11:38:00.902Z', + created_at: '2023-04-03T11:20:50.603Z', + version: 'WzE4NzQsMV0=', + score: 0, + }, +]; + +export const getSecuritySolutionTags = jest + .fn() + .mockImplementation(() => Promise.resolve(DEFAULT_TAGS_RESPONSE)); + +export const getSecuritySolutionDashboards = jest + .fn() + .mockImplementation(() => Promise.resolve(DEFAULT_DASHBOARDS_RESPONSE)); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/api/index.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/index.ts new file mode 100644 index 0000000000000..841f348bd24ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/api/index.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 type { HttpSetup } from '@kbn/core/public'; +import type { Tag } from '@kbn/saved-objects-tagging-plugin/public'; +import { INTERNAL_TAGS_URL, INTERNAL_DASHBOARDS_URL } from '../../../../../common/constants'; +import type { DashboardTableItem } from '../types'; + +export const getSecuritySolutionTags = ({ http }: { http: HttpSetup }): Promise => + http.get(INTERNAL_TAGS_URL); + +export const getSecuritySolutionDashboards = ({ + http, +}: { + http: HttpSetup; +}): Promise => http.get(INTERNAL_DASHBOARDS_URL); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/types.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/types.ts new file mode 100644 index 0000000000000..e82c383192218 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/types.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. + */ + +export interface DashboardTableItem { + id: string; + type: string; + attributes: { + title: string; + description: string; + }; + references: Array<{ name: string; type: string; id: string }>; +} diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts index b92b303a7b77f..a41640d678262 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.test.ts @@ -9,26 +9,15 @@ import { renderHook, act } from '@testing-library/react-hooks'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock/test_providers'; -import type { Tag } from '@kbn/saved-objects-tagging-plugin/common'; import { useCreateSecurityDashboardLink } from './use_create_security_dashboard_link'; +import { MOCK_TAG_ID } from './api/__mocks__'; +import { getSecurityTagIds as mockGetSecurityTagIds } from './utils'; jest.mock('../../lib/kibana'); -const TAG_ID = 'securityTagId'; -const CREATED_TAG: Tag = { - id: TAG_ID, - name: 'tag title', - description: 'tag description', - color: '#999999', -}; const URL = '/path'; -const mockGetSecurityTagId = jest.fn(async (): Promise => null); -const mockCreateSecurityTag = jest.fn(async () => CREATED_TAG); -jest.mock('./utils', () => ({ - getSecurityTagId: () => mockGetSecurityTagId(), - createSecurityTag: () => mockCreateSecurityTag(), -})); +jest.mock('./utils'); const renderUseCreateSecurityDashboardLink = () => renderHook(() => useCreateSecurityDashboardLink(), { @@ -57,8 +46,7 @@ describe('useCreateSecurityDashboardLink', () => { it('should request when renders', async () => { await asyncRenderUseCreateSecurityDashboard(); - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).toHaveBeenCalledTimes(1); + expect(mockGetSecurityTagIds).toHaveBeenCalledTimes(1); }); it('should return a memoized value when rerendered', async () => { @@ -71,28 +59,18 @@ describe('useCreateSecurityDashboardLink', () => { expect(result1).toBe(result2); }); - it('should not request create tag if already exists', async () => { - mockGetSecurityTagId.mockResolvedValueOnce(TAG_ID); - await asyncRenderUseCreateSecurityDashboard(); - - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).not.toHaveBeenCalled(); - }); - it('should generate create url with tag', async () => { await asyncRenderUseCreateSecurityDashboard(); - expect(mockGetRedirectUrl).toHaveBeenCalledWith({ tags: [TAG_ID] }); + expect(mockGetRedirectUrl).toHaveBeenCalledWith({ tags: [MOCK_TAG_ID] }); }); it('should not re-request tag id when re-rendered', async () => { const { rerender } = await asyncRenderUseCreateSecurityDashboard(); - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).toHaveBeenCalledTimes(1); + expect(mockGetSecurityTagIds).toHaveBeenCalledTimes(1); act(() => rerender()); - expect(mockGetSecurityTagId).toHaveBeenCalledTimes(1); - expect(mockCreateSecurityTag).toHaveBeenCalledTimes(1); + expect(mockGetSecurityTagIds).toHaveBeenCalledTimes(1); }); it('should return isLoading while requesting', async () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts index 2c8c5d7315de8..7b5a46d9b78e6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_create_security_dashboard_link.ts @@ -7,30 +7,24 @@ import { useEffect, useMemo, useState } from 'react'; import { useKibana } from '../../lib/kibana'; -import { getSecurityTagId, createSecurityTag } from './utils'; +import { getSecurityTagIds } from './utils'; type UseCreateDashboard = () => { isLoading: boolean; url: string }; export const useCreateSecurityDashboardLink: UseCreateDashboard = () => { - const { - dashboard: { locator } = {}, - savedObjects: { client: savedObjectsClient }, - savedObjectsTagging, - } = useKibana().services; + const { dashboard: { locator } = {}, savedObjectsTagging, http } = useKibana().services; - const [securityTagId, setSecurityTagId] = useState(null); + const [securityTagId, setSecurityTagId] = useState(null); useEffect(() => { let ignore = false; const getOrCreateSecurityTag = async () => { - if (savedObjectsClient && savedObjectsTagging) { - let tagId = await getSecurityTagId(savedObjectsClient); - if (!tagId) { - const newTag = await createSecurityTag(savedObjectsTagging.client); - tagId = newTag.id; - } - if (!ignore) { - setSecurityTagId(tagId); + if (http && savedObjectsTagging) { + // getSecurityTagIds creates a tag if it coundn't find one + const tagIds = await getSecurityTagIds(http); + + if (!ignore && tagIds) { + setSecurityTagId(tagIds); } } }; @@ -40,12 +34,12 @@ export const useCreateSecurityDashboardLink: UseCreateDashboard = () => { return () => { ignore = true; }; - }, [savedObjectsClient, savedObjectsTagging]); + }, [http, savedObjectsTagging]); const result = useMemo( () => ({ isLoading: securityTagId == null, - url: securityTagId ? locator?.getRedirectUrl({ tags: [securityTagId] }) ?? '' : '', + url: securityTagId ? locator?.getRedirectUrl({ tags: [securityTagId[0]] }) ?? '' : '', }), [securityTagId, locator] ); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx index 3a6a12be604e9..143a1335d4de8 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.test.tsx @@ -12,12 +12,12 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { EuiBasicTable } from '@elastic/eui'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock/test_providers'; -import type { DashboardTableItem } from './use_security_dashboards_table'; import { useSecurityDashboardsTableColumns, useSecurityDashboardsTableItems, } from './use_security_dashboards_table'; import * as telemetry from '../../lib/telemetry'; +import type { DashboardTableItem } from './types'; jest.mock('../../lib/kibana'); const spyTrack = jest.spyOn(telemetry, 'track'); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx index 771e68fde0b78..9380ce31ea49c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/use_security_dashboards_table.tsx @@ -8,26 +8,18 @@ import React, { useEffect, useMemo, useCallback } from 'react'; import type { MouseEventHandler } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; -import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { SavedObject } from '@kbn/core/public'; import { getSecurityDashboards } from './utils'; import { LinkAnchor } from '../../components/links'; import { useKibana, useNavigateTo } from '../../lib/kibana'; import * as i18n from './translations'; import { useFetch, REQUEST_NAMES } from '../../hooks/use_fetch'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; - -export interface DashboardTableItem extends SavedObject { - title?: string; - description?: string; -} +import type { DashboardTableItem } from './types'; const EMPTY_DESCRIPTION = '-' as const; export const useSecurityDashboardsTableItems = () => { - const { - savedObjects: { client: savedObjectsClient }, - } = useKibana().services; + const { http } = useKibana().services; const { fetch, data, isLoading, error } = useFetch( REQUEST_NAMES.SECURITY_DASHBOARDS, @@ -35,10 +27,10 @@ export const useSecurityDashboardsTableItems = () => { ); useEffect(() => { - if (savedObjectsClient) { - fetch(savedObjectsClient); + if (http) { + fetch(http); } - }, [fetch, savedObjectsClient]); + }, [fetch, http]); const items = useMemo(() => { if (!data) { diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts index 24911a82a2e82..72c1057fb5e45 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.test.ts @@ -5,75 +5,82 @@ * 2.0. */ -import type { SavedObjectsClientContract } from '@kbn/core/public'; -import { SECURITY_TAG_NAME } from '../../../../common/constants'; -import { getSecurityTagId } from './utils'; +import type { HttpSetup } from '@kbn/core/public'; +import { + getSecuritySolutionTags as mockGetSecuritySolutionTags, + getSecuritySolutionDashboards as mockGetSecuritySolutionDashboards, +} from './api'; +import { getSecurityDashboards, getSecurityTagIds } from './utils'; -const TAG_ID = 'securityTagId'; -const DEFAULT_TAGS_RESPONSE = [ - { - id: TAG_ID, - attributes: { name: SECURITY_TAG_NAME }, - }, - { - id: `${TAG_ID}_2`, - attributes: { name: `${SECURITY_TAG_NAME}_2` }, - }, -]; - -const mockSavedObjectsFind = jest.fn(async () => ({ savedObjects: DEFAULT_TAGS_RESPONSE })); -const savedObjectsClient = { - find: mockSavedObjectsFind, -} as unknown as SavedObjectsClientContract; +jest.mock('./api'); +const mockHttp = {} as unknown as HttpSetup; describe('dashboards utils', () => { afterEach(() => { jest.clearAllMocks(); }); - describe('getSecurityTagId', () => { - it('should call saved objects find with security tag name', async () => { - await getSecurityTagId(savedObjectsClient); + describe('getSecurityTagIds', () => { + it('should call getSecuritySolutionTags with http', async () => { + await getSecurityTagIds(mockHttp); - expect(mockSavedObjectsFind).toHaveBeenCalledWith( - expect.objectContaining({ type: 'tag', search: SECURITY_TAG_NAME, searchFields: ['name'] }) + expect(mockGetSecuritySolutionTags).toHaveBeenCalledWith( + expect.objectContaining({ http: mockHttp }) ); }); - it('should find saved object with security tag name', async () => { - const result = await getSecurityTagId(savedObjectsClient); - - expect(result).toEqual(TAG_ID); - }); - - it('should not find saved object with wrong security tag name', async () => { - mockSavedObjectsFind.mockResolvedValueOnce({ savedObjects: [DEFAULT_TAGS_RESPONSE[1]] }); - const result = await getSecurityTagId(savedObjectsClient); + it('should find saved objects Ids with security tags', async () => { + const result = await getSecurityTagIds(mockHttp); - expect(result).toBeUndefined(); + expect(result).toMatchInlineSnapshot(` + Array [ + "securityTagId", + ] + `); }); }); - describe('createSecurityTag', () => { - it('should call saved objects find with security tag name', async () => { - await getSecurityTagId(savedObjectsClient); + describe('getSecurityDashboards', () => { + it('should call getSecuritySolutionDashboards with http', async () => { + await getSecurityDashboards(mockHttp); - expect(mockSavedObjectsFind).toHaveBeenCalledWith( - expect.objectContaining({ type: 'tag', search: SECURITY_TAG_NAME, searchFields: ['name'] }) + expect(mockGetSecuritySolutionDashboards).toHaveBeenCalledWith( + expect.objectContaining({ http: mockHttp }) ); }); - it('should find saved object with security tag name', async () => { - const result = await getSecurityTagId(savedObjectsClient); - - expect(result).toEqual(TAG_ID); - }); - - it('should not find saved object with wrong security tag name', async () => { - mockSavedObjectsFind.mockResolvedValueOnce({ savedObjects: [DEFAULT_TAGS_RESPONSE[1]] }); - const result = await getSecurityTagId(savedObjectsClient); - - expect(result).toBeUndefined(); + it('should find saved objects with security tags', async () => { + const result = await getSecurityDashboards(mockHttp); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object { + "description": "Summary of Linux kernel audit events.", + "title": "[Auditbeat Auditd] Overview ECS", + "version": 1, + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-04-03T11:20:50.603Z", + "id": "c0ac2c00-c1c0-11e7-8995-936807a28b16-ecs", + "namespaces": Array [ + "default", + ], + "references": Array [ + Object { + "id": "ba964280-d211-11ed-890b-153ddf1a08e9", + "name": "tag-ref-ba964280-d211-11ed-890b-153ddf1a08e9", + "type": "tag", + }, + ], + "score": 0, + "type": "dashboard", + "typeMigrationVersion": "8.7.0", + "updated_at": "2023-04-03T11:38:00.902Z", + "version": "WzE4NzQsMV0=", + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts index f5f4e56424513..97a40a7196b8a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts +++ b/x-pack/plugins/security_solution/public/common/containers/dashboards/utils.ts @@ -5,65 +5,26 @@ * 2.0. */ -import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { SavedObjectsClientContract, SavedObject } from '@kbn/core/public'; -import type { Tag, TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; -import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; -import { SECURITY_TAG_NAME } from '../../../../common/constants'; - -export const SECURITY_TAG_DESCRIPTION = 'Security Solution auto-generated tag' as const; - -/** - * Returns the hex representation of a random color (e.g `#F1B7E2`) - */ -const getRandomColor = (): string => { - return `#${String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0')}`; -}; +import type { HttpSetup } from '@kbn/core/public'; +import { getSecuritySolutionDashboards, getSecuritySolutionTags } from './api'; +import type { DashboardTableItem } from './types'; /** - * Request the security tag saved object and returns the id if exists + * Request the security tag saved object and returns the id if exists. + * It creates one if the tag doesn't exist. */ -export const getSecurityTagId = async ( - savedObjectsClient: SavedObjectsClientContract -): Promise => { - const tagResponse = await savedObjectsClient.find({ - type: 'tag', - searchFields: ['name'], - search: SECURITY_TAG_NAME, - }); - // The search query returns partial matches, we need to find the exact tag name - return tagResponse.savedObjects.find(({ attributes }) => attributes.name === SECURITY_TAG_NAME) - ?.id; -}; - -/** - * Creates the security tag saved object and returns its id - */ -export const createSecurityTag = async ( - tagsClient: SavedObjectsTaggingApi['client'] -): Promise => { - // We need to use the TaggingApi client to make sure the Dashboards app tags cache is refreshed - const tagResponse = await tagsClient.create({ - name: SECURITY_TAG_NAME, - description: SECURITY_TAG_DESCRIPTION, - color: getRandomColor(), - }); - return tagResponse; +export const getSecurityTagIds = async (http: HttpSetup): Promise => { + const tagResponse = await getSecuritySolutionTags({ http }); + return tagResponse?.map(({ id }: { id: string }) => id); }; /** * Requests the saved objects of the security tagged dashboards */ export const getSecurityDashboards = async ( - savedObjectsClient: SavedObjectsClientContract -): Promise>> => { - const tagId = await getSecurityTagId(savedObjectsClient); - if (!tagId) { - return []; - } - const dashboardsResponse = await savedObjectsClient.find({ - type: 'dashboard', - hasReference: { id: tagId, type: 'tag' }, - }); - return dashboardsResponse.savedObjects; + http: HttpSetup +): Promise => { + const dashboardsResponse = await getSecuritySolutionDashboards({ http }); + + return dashboardsResponse; }; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/__mocks__/index.ts new file mode 100644 index 0000000000000..85960cd4629fa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/__mocks__/index.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mockGetDashboardsResult = [ + { + type: 'dashboard', + id: 'd698d5f0-cd58-11ed-affc-fb75e701db4b', + namespaces: ['default'], + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, + description: '', + timeRestore: false, + optionsJSON: + '{"useMargins":true,"syncColors":false,"syncCursor":true,"syncTooltips":false,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"8.8.0","type":"lens","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b"},"panelIndex":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b","embeddableConfig":{"attributes":{"title":"my alerts chart","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"valuesInLegend":true},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"negate":true,"disabled":false,"type":"exists","key":"kibana.alert.building_block_type"},"query":{"exists":{"field":"kibana.alert.building_block_type"}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":[".alerts-security.alerts-default"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":".alerts-security.alerts-default"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of kibana.alert.rule.name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"kibana.alert.rule.name","isBucketed":true,"params":{"size":1000,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"},"secondaryFields":[]}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}},{"version":"8.8.0","type":"lens","gridData":{"x":24,"y":0,"w":24,"h":15,"i":"8bcec072-4cf2-417d-a740-2b7f34c69473"},"panelIndex":"8bcec072-4cf2-417d-a740-2b7f34c69473","embeddableConfig":{"attributes":{"title":"events","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"63ba54b1-ca7d-4fa3-b43b-d748723abad4","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true}},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"disabled":false,"key":"query","negate":false,"type":"custom"},"query":{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":["auditbeat-*","packetbeat-*"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":"auditbeat-*"}},{"match_phrase":{"_index":"packetbeat-*"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"63ba54b1-ca7d-4fa3-b43b-d748723abad4":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of event.action","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"event.action","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"}}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}}]', + title: 'my alerts dashboard', + version: 1, + }, + references: [ + { + type: 'index-pattern', + id: 'security-solution-default', + name: '46c2105e-0edd-460c-8ecf-aaf3777b0c9b:indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d', + }, + { + type: 'index-pattern', + id: 'security-solution-default', + name: '8bcec072-4cf2-417d-a740-2b7f34c69473:indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4', + }, + { + type: 'tag', + id: 'de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + name: 'tag-ref-de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + }, + ], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.7.0', + updated_at: '2023-03-28T11:27:28.365Z', + created_at: '2023-03-28T11:27:28.365Z', + version: 'WzE3NTIwLDFd', + score: 0, + }, + { + type: 'dashboard', + id: 'eee18bf0-cfc1-11ed-8380-f532c904188c', + namespaces: ['default'], + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"query":"","language":"kuery"},"filter":[{"meta":{"type":"phrase","key":"event.action","params":{"query":"process_stopped"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"},"query":{"match_phrase":{"event.action":"process_stopped"}},"$state":{"store":"appState"}}]}', + }, + description: '', + timeRestore: false, + optionsJSON: + '{"useMargins":true,"syncColors":false,"syncCursor":true,"syncTooltips":false,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"8.8.0","type":"lens","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b"},"panelIndex":"46c2105e-0edd-460c-8ecf-aaf3777b0c9b","embeddableConfig":{"attributes":{"title":"my alerts chart","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"valuesInLegend":true},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"negate":true,"disabled":false,"type":"exists","key":"kibana.alert.building_block_type"},"query":{"exists":{"field":"kibana.alert.building_block_type"}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":[".alerts-security.alerts-default"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":".alerts-security.alerts-default"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"eafb5cfd-bd7e-4c1f-a675-ef11a17c616d":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of kibana.alert.rule.name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"kibana.alert.rule.name","isBucketed":true,"params":{"size":1000,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"},"secondaryFields":[]}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}},{"version":"8.8.0","type":"lens","gridData":{"x":24,"y":0,"w":24,"h":15,"i":"8bcec072-4cf2-417d-a740-2b7f34c69473"},"panelIndex":"8bcec072-4cf2-417d-a740-2b7f34c69473","embeddableConfig":{"attributes":{"title":"events","description":"","visualizationType":"lnsXY","type":"lens","references":[{"type":"index-pattern","id":"security-solution-default","name":"indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4"}],"state":{"visualization":{"title":"Empty XY chart","legend":{"isVisible":true,"position":"left","legendSize":"xlarge"},"valueLabels":"hide","preferredSeriesType":"bar_stacked","layers":[{"layerId":"63ba54b1-ca7d-4fa3-b43b-d748723abad4","accessors":["e09e0380-0740-4105-becc-0a4ca12e3944"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"layerType":"data","xAccessor":"aac9d7d0-13a3-480a-892b-08207a787926","splitAccessor":"34919782-4546-43a5-b668-06ac934d3acd"}],"yRightExtent":{"mode":"full"},"yLeftExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true}},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"disabled":false,"key":"query","negate":false,"type":"custom"},"query":{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},"$state":{"store":"appState"}},{"meta":{"type":"phrases","key":"_index","params":["auditbeat-*","packetbeat-*"],"alias":null,"negate":false,"disabled":false},"query":{"bool":{"should":[{"match_phrase":{"_index":"auditbeat-*"}},{"match_phrase":{"_index":"packetbeat-*"}}],"minimum_should_match":1}},"$state":{"store":"appState"}}],"datasourceStates":{"formBased":{"layers":{"63ba54b1-ca7d-4fa3-b43b-d748723abad4":{"columns":{"aac9d7d0-13a3-480a-892b-08207a787926":{"label":"@timestamp","dataType":"date","operationType":"date_histogram","sourceField":"@timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"e09e0380-0740-4105-becc-0a4ca12e3944":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"___records___"},"34919782-4546-43a5-b668-06ac934d3acd":{"label":"Top values of event.action","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"event.action","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"e09e0380-0740-4105-becc-0a4ca12e3944"},"orderDirection":"desc","otherBucket":true,"missingBucket":false,"parentFormat":{"id":"terms"}}}},"columnOrder":["34919782-4546-43a5-b668-06ac934d3acd","aac9d7d0-13a3-480a-892b-08207a787926","e09e0380-0740-4105-becc-0a4ca12e3944"],"incompleteColumns":{}}}}},"internalReferences":[],"adHocDataViews":{}}},"enhancements":{}}}]', + title: 'my alerts dashboard - 2', + version: 1, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + id: 'security-solution-default', + }, + { + type: 'index-pattern', + id: 'security-solution-default', + name: '46c2105e-0edd-460c-8ecf-aaf3777b0c9b:indexpattern-datasource-layer-eafb5cfd-bd7e-4c1f-a675-ef11a17c616d', + }, + { + type: 'index-pattern', + id: 'security-solution-default', + name: '8bcec072-4cf2-417d-a740-2b7f34c69473:indexpattern-datasource-layer-63ba54b1-ca7d-4fa3-b43b-d748723abad4', + }, + { + type: 'tag', + id: 'de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + name: 'tag-ref-de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + }, + { + type: 'tag', + id: 'edb233b0-cfc1-11ed-8380-f532c904188c', + name: 'tag-ref-edb233b0-cfc1-11ed-8380-f532c904188c', + }, + ], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.7.0', + updated_at: '2023-03-31T12:45:36.175Z', + created_at: '2023-03-31T12:45:36.175Z', + version: 'WzE5MTQyLDFd', + score: 0, + }, +]; + +export const mockGetTagsResult = [ + { + type: 'tag', + id: 'de7ad1f0-ccc8-11ed-9175-1b0d4269ff48', + namespaces: ['default'], + attributes: { + name: 'Security Solution', + description: 'Security Solution auto-generated tag', + color: '#4bc922', + }, + references: [], + coreMigrationVersion: '8.8.0', + typeMigrationVersion: '8.0.0', + updated_at: '2023-03-27T17:57:41.647Z', + created_at: '2023-03-27T17:57:41.647Z', + version: 'WzE2Njc1LDFd', + score: null, + sort: [1679939861647], + }, +]; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/helpers.ts b/x-pack/plugins/security_solution/server/lib/dashboards/helpers.ts new file mode 100644 index 0000000000000..58bedbc4411d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/helpers.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import type { + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '@kbn/core-saved-objects-api-server'; +import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { DashboardAttributes } from '@kbn/dashboard-plugin/common'; +import type { OutputError } from '@kbn/securitysolution-es-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { SECURITY_TAG_NAME, SECURITY_TAG_DESCRIPTION } from '../../../common/constants'; +import { createTag, findTagsByName } from './saved_objects/tags'; + +/** + * Returns the hex representation of a random color (e.g `#F1B7E2`) + */ +const getRandomColor = (): string => { + return `#${String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0')}`; +}; + +export const getOrCreateSecurityTag = async ({ + logger, + savedObjectsClient, +}: { + logger: Logger; + savedObjectsClient: SavedObjectsClientContract; +}): Promise<{ + response: Array> | null; + error?: OutputError; +}> => { + const { response: existingTags } = await findTagsByName({ + savedObjectsClient, + search: SECURITY_TAG_NAME, + }); + + if (existingTags && existingTags.length > 0) { + return { response: existingTags }; + } else { + const { error, response: createdTag } = await createTag({ + savedObjectsClient, + tagName: SECURITY_TAG_NAME, + description: SECURITY_TAG_DESCRIPTION, + color: getRandomColor(), + }); + + if (createdTag && !error) { + return { response: [createdTag] }; + } else { + logger.error(`Failed to create ${SECURITY_TAG_NAME} tag - ${JSON.stringify(error?.message)}`); + return { + response: null, + error: error ?? transformError(new Error(`Failed to create ${SECURITY_TAG_NAME} tag`)), + }; + } + } +}; + +export const getSecuritySolutionDashboards = async ({ + logger, + savedObjectsClient, +}: { + logger: Logger; + savedObjectsClient: SavedObjectsClientContract; +}): Promise<{ + response: Array> | null; + error?: OutputError; +}> => { + const { response: foundTags } = await findTagsByName({ + savedObjectsClient, + search: SECURITY_TAG_NAME, + }); + + if (!foundTags || foundTags?.length === 0) { + return { response: [] }; + } + + try { + const dashboardsResponse = await savedObjectsClient.find({ + type: 'dashboard', + hasReference: foundTags.map(({ id: tagId }) => ({ id: tagId, type: 'tag' })), + }); + return { response: dashboardsResponse.saved_objects }; + } catch (e) { + logger.error(`Failed to get SecuritySolution Dashboards - ${JSON.stringify(e?.message)}`); + + return { response: null, error: transformError(e) }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.test.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.test.ts new file mode 100644 index 0000000000000..5bca5246e2592 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.test.ts @@ -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 type { Logger } from '@kbn/core/server'; +import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { INTERNAL_DASHBOARDS_URL } from '../../../../common/constants'; +import { + serverMock, + requestContextMock, + mockGetCurrentUser, + requestMock, +} from '../../detection_engine/routes/__mocks__'; +import { getSecuritySolutionDashboards } from '../helpers'; +import { mockGetDashboardsResult } from '../__mocks__'; +import { getSecuritySolutionDashboardsRoute } from './get_security_solution_dashboards'; +jest.mock('../helpers', () => ({ getSecuritySolutionDashboards: jest.fn() })); + +describe('getSecuritySolutionDashboardsRoute', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + const { context } = requestContextMock.createTools(); + const logger = { error: jest.fn() } as unknown as Logger; + const mockRequest = requestMock.create({ + method: 'get', + path: INTERNAL_DASHBOARDS_URL, + }); + beforeEach(() => { + jest.clearAllMocks(); + server = serverMock.create(); + + securitySetup = { + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown as SecurityPluginSetup; + + getSecuritySolutionDashboardsRoute(server.router, logger, securitySetup); + }); + + it('should return dashboards with Security Solution tags', async () => { + (getSecuritySolutionDashboards as jest.Mock).mockResolvedValue({ + response: mockGetDashboardsResult, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockGetDashboardsResult); + }); + + it('should return error', async () => { + const error = { + statusCode: 500, + message: 'Internal Server Error', + }; + (getSecuritySolutionDashboards as jest.Mock).mockResolvedValue({ + response: null, + error, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(error.statusCode); + expect(response.body.message).toEqual(`Failed to get dashboards - ${error.message}`); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.ts new file mode 100644 index 0000000000000..828a890f963c3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_dashboards.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { Logger } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; + +import { INTERNAL_DASHBOARDS_URL } from '../../../../common/constants'; +import type { SetupPlugins } from '../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../types'; +import { buildSiemResponse } from '../../detection_engine/routes/utils'; +import { buildFrameworkRequest } from '../../timeline/utils/common'; +import { getSecuritySolutionDashboards } from '../helpers'; + +export const getSecuritySolutionDashboardsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger, + security: SetupPlugins['security'] +) => { + router.get( + { + path: INTERNAL_DASHBOARDS_URL, + validate: false, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; + + const { response: dashboards, error } = await getSecuritySolutionDashboards({ + logger, + savedObjectsClient, + }); + if (!error && dashboards != null) { + return response.ok({ body: dashboards }); + } else { + return siemResponse.error({ + statusCode: error?.statusCode ?? 500, + body: i18n.translate( + 'xpack.securitySolution.dashboards.getSecuritySolutionDashboardsErrorTitle', + { + values: { message: error?.message }, + defaultMessage: `Failed to get dashboards - {message}`, + } + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.test.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.test.ts new file mode 100644 index 0000000000000..8f59087ff9ee6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { INTERNAL_TAGS_URL, SECURITY_TAG_NAME } from '../../../../common/constants'; +import { + serverMock, + requestContextMock, + mockGetCurrentUser, + requestMock, +} from '../../detection_engine/routes/__mocks__'; +import { getOrCreateSecurityTag } from '../helpers'; +import { mockGetTagsResult } from '../__mocks__'; +import { getSecuritySolutionTagsRoute } from './get_security_solution_tags'; +jest.mock('../helpers', () => ({ getOrCreateSecurityTag: jest.fn() })); + +describe('getSecuritySolutionTagsRoute', () => { + let server: ReturnType; + let securitySetup: SecurityPluginSetup; + const { context } = requestContextMock.createTools(); + const logger = { error: jest.fn() } as unknown as Logger; + const mockRequest = requestMock.create({ + method: 'get', + path: INTERNAL_TAGS_URL, + }); + beforeEach(() => { + jest.clearAllMocks(); + server = serverMock.create(); + + securitySetup = { + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown as SecurityPluginSetup; + + getSecuritySolutionTagsRoute(server.router, logger, securitySetup); + }); + + it('should return tags with Security Solution tags', async () => { + (getOrCreateSecurityTag as jest.Mock).mockResolvedValue({ + response: mockGetTagsResult, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toMatchInlineSnapshot(` + Array [ + Object { + "color": "#4bc922", + "description": "Security Solution auto-generated tag", + "id": "de7ad1f0-ccc8-11ed-9175-1b0d4269ff48", + "name": "Security Solution", + }, + ] + `); + }); + + it('should return error', async () => { + const error = { + statusCode: 500, + message: 'Internal Server Error', + }; + (getOrCreateSecurityTag as jest.Mock).mockResolvedValue({ + response: null, + error, + }); + + const response = await server.inject(mockRequest, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(error.statusCode); + expect(response.body.message).toEqual( + `Failed to create ${SECURITY_TAG_NAME} tag - ${error.message}` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.ts new file mode 100644 index 0000000000000..8c70a114ba647 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_security_solution_tags.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 type { Logger } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; + +import { INTERNAL_TAGS_URL, SECURITY_TAG_NAME } from '../../../../common/constants'; +import type { SetupPlugins } from '../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../types'; +import { buildSiemResponse } from '../../detection_engine/routes/utils'; +import { buildFrameworkRequest } from '../../timeline/utils/common'; +import { getOrCreateSecurityTag } from '../helpers'; + +export const getSecuritySolutionTagsRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger, + security: SetupPlugins['security'] +) => { + router.get( + { + path: INTERNAL_TAGS_URL, + validate: false, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const savedObjectsClient = (await frameworkRequest.context.core).savedObjects.client; + + const { response: tags, error } = await getOrCreateSecurityTag({ + logger, + savedObjectsClient, + }); + + if (tags && !error) { + return response.ok({ + body: tags.map(({ id, attributes: { name, description, color } }) => ({ + id, + name, + description, + color, + })), + }); + } else { + return siemResponse.error({ + statusCode: error?.statusCode ?? 500, + body: i18n.translate( + 'xpack.securitySolution.dashboards.getSecuritySolutionTagsErrorTitle', + { + values: { tagName: SECURITY_TAG_NAME, message: error?.message }, + defaultMessage: `Failed to create {tagName} tag - {message}`, + } + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/index.ts new file mode 100644 index 0000000000000..be21be7f3e24a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/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 { getSecuritySolutionDashboardsRoute } from './get_security_solution_dashboards'; +export { getSecuritySolutionTagsRoute } from './get_security_solution_tags'; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/create_tag.ts b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/create_tag.ts new file mode 100644 index 0000000000000..27646630b4f01 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/create_tag.ts @@ -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 type { + SavedObject, + SavedObjectReference, + SavedObjectsClientContract, +} from '@kbn/core/server'; +import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { OutputError } from '@kbn/securitysolution-es-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +interface CreateTagParams { + savedObjectsClient: SavedObjectsClientContract; + tagName: string; + description: string; + color: string; + references?: SavedObjectReference[]; +} + +interface CreateTagResponse { + error?: OutputError; + response: SavedObject | null; +} + +export const createTag = async ({ + savedObjectsClient, + tagName, + description, + color, + references, +}: CreateTagParams): Promise => { + const TYPE = 'tag'; + try { + const createdTag = await savedObjectsClient.create( + TYPE, + { + name: tagName, + description, + color, + }, + { references } + ); + + return { + response: createdTag, + }; + } catch (e) { + return { + error: transformError(e), + response: null, + }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/find_tags_by_name.ts b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/find_tags_by_name.ts new file mode 100644 index 0000000000000..76cc55a950e0e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/find_tags_by_name.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 type { SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server'; +import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { OutputError } from '@kbn/securitysolution-es-utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +export const findTagsByName = async ({ + savedObjectsClient, + search, +}: { + savedObjectsClient: SavedObjectsClientContract; + search: string; +}): Promise<{ + response: Array> | null; + error?: OutputError; +}> => { + try { + const tagResponse = await savedObjectsClient.find({ + type: 'tag', + search, + searchFields: ['name'], + sortField: 'updated_at', + sortOrder: 'desc', + }); + return { + response: tagResponse.saved_objects.filter(({ attributes: { name } }) => name === search), + }; + } catch (e) { + return { + error: transformError(e), + response: null, + }; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/index.ts b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/index.ts new file mode 100644 index 0000000000000..e9af2f18e5931 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/dashboards/saved_objects/tags/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { createTag } from './create_tag'; +export { findTagsByName } from './find_tags_by_name'; diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts index 7cd493cff2b74..96feae5a7c7b4 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/bulk_create_saved_objects.ts @@ -38,9 +38,10 @@ export const bulkCreateSavedObjects = async ({ spaceId, }); - const tagResult = tagResponse?.hostRiskScoreDashboards ?? tagResponse?.userRiskScoreDashboards; + const riskScoreTagResult = + tagResponse?.hostRiskScoreDashboards ?? tagResponse?.userRiskScoreDashboards; - if (!tagResult?.success) { + if (!riskScoreTagResult?.success) { return tagResponse; } @@ -79,7 +80,11 @@ export const bulkCreateSavedObjects = async ({ id: idReplaceMappings[so.id] ?? so.id, references: [ ...references, - { id: tagResult?.body?.id, name: tagResult?.body?.name, type: tagResult?.body?.type }, + { + id: riskScoreTagResult?.body?.id, + name: riskScoreTagResult?.body?.name, + type: riskScoreTagResult?.body?.type, + }, ], }; }); diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts index 9bb15266f2f71..fddb9dd0e47d0 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/find_or_create_tag.ts @@ -9,9 +9,9 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser import { transformError } from '@kbn/securitysolution-es-utils'; import { i18n } from '@kbn/i18n'; import type { RiskScoreEntity } from '../../../../../common/search_strategy'; -import type { Tag } from './utils'; import { RISK_SCORE_TAG_DESCRIPTION, getRiskScoreTagName } from './utils'; import type { BulkCreateSavedObjectsResult } from '../types'; +import { createTag, findTagsByName } from '../../../dashboards/saved_objects/tags'; export const findRiskScoreTag = async ({ savedObjectsClient, @@ -20,17 +20,9 @@ export const findRiskScoreTag = async ({ savedObjectsClient: SavedObjectsClientContract; search: string; }) => { - const tagResponse = await savedObjectsClient.find({ - type: 'tag', - search, - searchFields: ['name'], - sortField: 'updated_at', - sortOrder: 'desc', - }); + const { response: tagResponse } = await findTagsByName({ savedObjectsClient, search }); - const existingRiskScoreTag = tagResponse.saved_objects.find( - ({ attributes }) => attributes.name === search - ); + const existingRiskScoreTag = tagResponse?.find(({ attributes }) => attributes.name === search); return existingRiskScoreTag ? { @@ -85,23 +77,26 @@ export const findOrCreateRiskScoreTag = async ({ }, }; } else { - try { - const { id: tagId } = await savedObjectsClient.create('tag', { - name: tagName, - description: RISK_SCORE_TAG_DESCRIPTION, - color: '#6edb7f', - }); + const { error, response: createTagResponse } = await createTag({ + savedObjectsClient, + tagName, + description: RISK_SCORE_TAG_DESCRIPTION, + color: '#6edb7f', + }); + if (!error && createTagResponse?.id) { return { [savedObjectTemplate]: { success: true, error: null, - body: { ...tag, id: tagId }, + body: { ...tag, id: createTagResponse?.id }, }, }; - } catch (e) { + } else { logger.error( - `${savedObjectTemplate} cannot be installed as failed to create the tag: ${tagName}` + `${savedObjectTemplate} cannot be installed as failed to create the tag: ${tagName} - ${JSON.stringify( + error?.message + )}` ); return { [savedObjectTemplate]: { diff --git a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts index 74a84a49eb92d..9392b157e6d56 100644 --- a/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/helpers/utils.ts @@ -8,12 +8,6 @@ import { RiskScoreEntity } from '../../../../../common/search_strategy'; import type { SavedObjectTemplate } from '../types'; -export interface Tag { - id: string; - name: string; - description: string; -} - export const HOST_RISK_SCORE = 'Host Risk Score'; export const USER_RISK_SCORE = 'User Risk Score'; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index af26823416a2b..2c88194291af9 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -71,6 +71,10 @@ import { readPrebuiltDevToolContentRoute, } from '../lib/risk_score/routes'; import { registerManageExceptionsRoutes } from '../lib/exceptions/api/register_routes'; +import { + getSecuritySolutionDashboardsRoute, + getSecuritySolutionTagsRoute, +} from '../lib/dashboards/routes'; export const initRoutes = ( router: SecuritySolutionPluginRouter, @@ -157,6 +161,10 @@ export const initRoutes = ( deletePrebuiltSavedObjectsRoute(router, security); getRiskScoreIndexStatusRoute(router); installRiskScoresRoute(router, logger, security); + + // Dashboards + getSecuritySolutionDashboardsRoute(router, logger, security); + getSecuritySolutionTagsRoute(router, logger, security); const { previewTelemetryUrlEnabled } = config.experimentalFeatures; if (previewTelemetryUrlEnabled) { // telemetry preview endpoint for e2e integration tests only at the moment. 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 df7d3da5bd177..846f36f45542d 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 @@ -136,8 +136,10 @@ journey(`TestNowMode`, async ({ page, params }) => { await services.addTestSummaryDocument({ testRunId, docType: 'stepEnd', stepIndex: 1 }); await page.waitForSelector('text=1 step completed'); - await page.waitForSelector('text=Go to https://www.google.com'); - await page.waitForSelector('text=1.42 s'); + await page.waitForSelector( + '.euiTableRowCell--hideForMobile :has-text("Go to https://www.google.com")' + ); + await page.waitForSelector('.euiTableRowCell--hideForMobile :has-text("1.42 s")'); await page.waitForSelector('text=Complete'); }); @@ -146,7 +148,7 @@ journey(`TestNowMode`, async ({ page, params }) => { await retry.tryForTime(90 * 1000, async () => { await page.waitForSelector('text=2 steps completed'); await page.waitForSelector('text="Go to step 2"'); - await page.waitForSelector('text=788 ms'); + await page.waitForSelector('div:has-text("788 ms")'); await page.waitForSelector('text=IN PROGRESS'); }); }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_run_details.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_run_details.journey.ts index 080ca78d71358..5db5a55a4759d 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_run_details.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/test_run_details.journey.ts @@ -65,6 +65,6 @@ journey(`TestRunDetailsPage`, async ({ page, params }) => { await page.waitForSelector('text=Test run details'); await page.waitForSelector('text=Go to https://www.google.com'); - await page.waitForSelector('text=After 2.12 s'); + await page.waitForSelector('div:has-text("After 2.12 s")'); }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx index 3115597135a96..8ba6fb3bc54ff 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx @@ -38,9 +38,18 @@ export const LocationStatusBadges = ({ const locationsToDisplay = locations.slice(0, toDisplay); return ( - + {locationsToDisplay.map((loc) => ( - + = ({ title, hasBorder = true, hasShadow = false, children, titleLeftAlign, ...props }) => { + { title?: string; titleLeftAlign?: boolean; margin?: string } & EuiPanelProps +> = ({ + title, + hasBorder = true, + hasShadow = false, + children, + titleLeftAlign, + margin, + ...props +}) => { const { euiTheme } = useEuiTheme(); return ( @@ -19,7 +27,7 @@ export const PanelWithTitle: React.FC<

{label ?? VIEW_DETAILS} @@ -44,10 +50,11 @@ export const StepDetailsLinkIcon = ({ return ( 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 17a9c4077e372..508b9d29c04a9 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 @@ -6,7 +6,14 @@ */ import { i18n } from '@kbn/i18n'; -import React, { CSSProperties, ReactElement, useCallback, useEffect, useState } from 'react'; +import React, { + CSSProperties, + ReactElement, + PropsWithChildren, + useCallback, + useEffect, + useState, +} from 'react'; import { EuiBasicTable, EuiBasicTableColumn, @@ -14,7 +21,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, + EuiTextProps, + EuiTitle, useEuiTheme, + useIsWithinMinBreakpoint, } from '@elastic/eui'; import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types'; @@ -22,7 +32,11 @@ import { StepTabs } from '../../test_run_details/step_tabs'; import { ResultDetails } from './result_details'; import { JourneyStep } from '../../../../../../common/runtime_types'; import { JourneyStepScreenshotContainer } from '../screenshot/journey_step_screenshot_container'; -import { ScreenshotImageSize, THUMBNAIL_SCREENSHOT_SIZE } from '../screenshot/screenshot_size'; +import { + ScreenshotImageSize, + THUMBNAIL_SCREENSHOT_SIZE, + THUMBNAIL_SCREENSHOT_SIZE_MOBILE, +} from '../screenshot/screenshot_size'; import { StepDetailsLinkIcon } from '../links/step_details_link'; import { parseBadgeStatus, getTextColorForMonitorStatus } from './status_badge'; @@ -61,6 +75,7 @@ export const BrowserStepsList = ({ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< Record >({}); + const isTabletOrGreater = useIsWithinMinBreakpoint('s'); const toggleDetails = useCallback( (item: JourneyStep) => { @@ -118,8 +133,13 @@ export const BrowserStepsList = ({ field: 'synthetics.step.index', name: '#', render: (stepIndex: number, item: JourneyStep) => ( - + + {stepIndex} + ), + mobileOptions: { + show: false, + }, }, ] : []), @@ -139,15 +159,20 @@ export const BrowserStepsList = ({ /> ), mobileOptions: { - render: (item: JourneyStep) => ( - - - {item.synthetics?.step?.index!}. {item.synthetics?.step?.name} - - + render: (step: JourneyStep) => ( + ), - header: SCREENSHOT_LABEL, + header: false, enlarge: true, + width: '100%', }, }, { @@ -166,6 +191,9 @@ export const BrowserStepsList = ({ ); }, + mobileOptions: { + show: false, + }, }, { field: 'synthetics.step.status', @@ -177,6 +205,9 @@ export const BrowserStepsList = ({ isExpanded={Boolean(itemIdToExpandedRowMap[item._id]) && !testNowMode} /> ), + mobileOptions: { + show: false, + }, }, ...(showLastSuccessful ? [ @@ -189,6 +220,9 @@ export const BrowserStepsList = ({ isExpanded={Boolean(itemIdToExpandedRowMap[item._id])} /> ), + mobileOptions: { + show: false, + }, }, ] : [ @@ -208,7 +242,6 @@ export const BrowserStepsList = ({ align: 'right', field: 'timestamp', name: '', - mobileOptions: { show: false }, render: (_val: string, item) => ( ), + mobileOptions: { show: false }, }, ]; return ( <> { if (itemIdToExpandedRowMap[row._id]) { return { @@ -234,8 +269,8 @@ export const BrowserStepsList = ({ loading={loading} columns={columns} error={error?.message} - isExpandable={true} - hasActions={true} + isExpandable={showExpand} + hasActions={false} items={stepEnds} noItemsMessage={ loading @@ -254,15 +289,16 @@ export const BrowserStepsList = ({ ); }; -const StepNumber = ({ - stepIndex, +const StyleForStepStatus = ({ step, + textSize = 's', euiTheme, -}: { - stepIndex: number; + children, +}: PropsWithChildren<{ step: JourneyStep; + textSize?: EuiTextProps['size']; euiTheme: EuiThemeComputed; -}) => { +}>) => { const status = parseBadgeStatus(step.synthetics?.step?.status ?? ''); return ( @@ -270,14 +306,107 @@ const StepNumber = ({ css={{ fontWeight: euiTheme.font.weight.bold, }} - size="s" + size={textSize} color={euiTheme.colors[getTextColorForMonitorStatus(status)] as CSSProperties['color']} > - {stepIndex} + {children} ); }; +const MobileRowDetails = ({ + journeyStep, + showStepNumber, + showLastSuccessful, + stepsLoading, + isExpanded, + isTestNowMode, + euiTheme, +}: { + journeyStep: JourneyStep; + showStepNumber: boolean; + showLastSuccessful: boolean; + stepsLoading: boolean; + isExpanded: boolean; + isTestNowMode: boolean; + euiTheme: EuiThemeComputed; +}) => { + return ( + + +

+ + {showStepNumber && journeyStep.synthetics?.step?.index + ? `${journeyStep.synthetics.step.index}. ` + : null}{' '} + {journeyStep.synthetics?.step?.name} + +

+
+ + +
+ + {[ + { + title: RESULT_LABEL, + description: ( + + ), + }, + ...[ + showLastSuccessful + ? { + title: LAST_SUCCESSFUL, + description: ( + + ), + } + : { + title: STEP_DURATION, + description: , + }, + ], + ].map(({ title, description }) => ( + + {title} + {description} + + ))} + +
+
+ +
+ ); +}; + const RESULT_LABEL = i18n.translate('xpack.synthetics.monitor.result.label', { defaultMessage: 'Result', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/status_badge.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/status_badge.tsx index acfca86c2dea0..38b853d0e8f25 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/status_badge.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/status_badge.tsx @@ -16,7 +16,7 @@ export const StatusBadge = ({ status }: { status: MonitorStatus }) => { } return ( - + {status === 'succeeded' ? COMPLETE_LABEL : status === 'failed' ? FAILED_LABEL : SKIPPED_LABEL} ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/page_template/synthetics_page_template.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/page_template/synthetics_page_template.tsx new file mode 100644 index 0000000000000..b9ce0e722f24e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/page_template/synthetics_page_template.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 { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; +import React from 'react'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +import { ClientPluginsStart } from '../../../../../plugin'; + +export const WrappedPageTemplate = (props: LazyObservabilityPageTemplateProps) => { + const { observability } = useKibana().services; + const PageTemplateComponent = observability.navigation.PageTemplate; + + return ; +}; + +export const SyntheticsPageTemplateComponent = euiStyled(WrappedPageTemplate)` + &&& { + .euiPageHeaderContent__top { + flex-wrap: wrap; + .euiTitle { + min-width: 160px; + } + } + } +`; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx index 818541b1f73ea..f24ba4d5e9f4a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx @@ -27,6 +27,7 @@ import { EuiOutsideClickDetector, useIsWithinMaxBreakpoint, } from '@elastic/eui'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { SYNTHETICS_API_URLS } from '../../../../../../common/constants'; import { SyntheticsSettingsContext } from '../../../contexts'; @@ -118,7 +119,7 @@ export const JourneyScreenshotDialog = ({ }} onKeyDown={onKeyDown} > - + - + ) : null} - + - + {stepCountLabel} @@ -206,6 +207,17 @@ export const JourneyScreenshotDialog = ({ ) : null; }; +const ModalBodyStyled = euiStyled(EuiModalBody)` + &&& { + & > div { + display: flex; + justify-content: center; + align-items: center; + margin-top: 24px; + } + } +`; + export const getScreenshotUrl = ({ basePath, checkGroup, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/screenshot_size.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/screenshot_size.ts index 0d1e09949b188..c735ae59a0308 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/screenshot_size.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/screenshot_size.ts @@ -16,6 +16,7 @@ export type ScreenshotImageSize = | 'full'; export const THUMBNAIL_SCREENSHOT_SIZE: ScreenshotImageSize = [96, 64]; +export const THUMBNAIL_SCREENSHOT_SIZE_MOBILE: ScreenshotImageSize = [180, 112]; export const POPOVER_SCREENSHOT_SIZE: ScreenshotImageSize = [640, 360]; export function getConfinedScreenshotSize( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx index ac6ebcb7ef360..4b69da4ad70c3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/advanced/index.tsx @@ -41,8 +41,8 @@ export const AdvancedConfig = ({ readOnly }: { readOnly: boolean }) => { title={

{configGroup.title}

} fullWidth key={configGroup.title} - descriptionFlexItemProps={{ style: { minWidth: 200 } }} - fieldFlexItemProps={{ style: { minWidth: 500 } }} + descriptionFlexItemProps={{ style: { minWidth: 208 } }} + fieldFlexItemProps={{ style: { minWidth: 208 } }} style={{ flexWrap: 'wrap' }} > {configGroup.components.map((field) => { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx index 976a7ea1c666a..34a696389717f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx @@ -7,12 +7,14 @@ import React from 'react'; import styled from 'styled-components'; +import useThrottle from 'react-use/lib/useThrottle'; import { EuiPanel } from '@elastic/eui'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { CodeEditor as MonacoCodeEditor } from '@kbn/kibana-react-plugin/public'; import { MonacoEditorLangId } from '../types'; +import { useDimensions } from '../../../hooks'; const CodeEditorContainer = styled(EuiPanel)` padding: 0; @@ -39,29 +41,39 @@ export const CodeEditor = ({ height = '250px', readOnly, }: CodeEditorProps) => { + const { elementRef: containerRef, width: containerWidth } = useDimensions(); + const containerWidthThrottled = useThrottle(containerWidth, 500); + return ( - - + } + borderRadius="none" + hasShadow={false} + hasBorder={true} > - - - + + + + + ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx index 90e4d457764c1..a472c3231053b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/request_body_field.test.tsx @@ -10,10 +10,13 @@ import 'jest-canvas-mock'; import React, { useState, useCallback } from 'react'; import userEvent from '@testing-library/user-event'; import { fireEvent, waitFor } from '@testing-library/react'; +import { mockGlobals } from '../../../utils/testing'; import { render } from '../../../utils/testing/rtl_helpers'; import { RequestBodyField } from './request_body_field'; import { Mode } from '../types'; +mockGlobals(); + jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx index b52cfe346a80d..f661d0f25b4af 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.test.tsx @@ -7,9 +7,12 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; +import { mockGlobals } from '../../../utils/testing'; import { render } from '../../../utils/testing/rtl_helpers'; import { SourceField } from './source_field'; +mockGlobals(); + jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), htmlIdGenerator: () => () => `id-${Math.random()}`, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index 934a895c47d34..fe2035efb481f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -1118,7 +1118,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({ { value: DEFAULT_BROWSER_ADVANCED_FIELDS[ConfigKey.THROTTLING_CONFIG], inputDisplay: ( - + {i18n.translate('xpack.synthetics.monitorConfig.throttling.options.default', { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx index 12c382fd94147..27d5f699c3b51 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx @@ -56,7 +56,7 @@ export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => { ) : ( <> - + {isEdit && defaultValues && (
@@ -84,7 +84,7 @@ export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => { - + { const original = jest.requireActual('@kbn/kibana-react-plugin/public'); return { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx index 4052840f21d95..bd9bc9d57c4aa 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; +import { mockGlobals } from '../../utils/testing'; import { render } from '../../utils/testing/rtl_helpers'; import { MonitorEditPage } from './monitor_edit_page'; import { ConfigKey } from '../../../../../common/runtime_types'; import * as observabilityPublic from '@kbn/observability-plugin/public'; +mockGlobals(); + jest.mock('@kbn/observability-plugin/public'); jest.mock('@kbn/kibana-react-plugin/public', () => { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx index c2a7578cdaade..48ea642366a78 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step.tsx @@ -16,10 +16,10 @@ interface Props { export const Step = ({ description, children }: Props) => { return ( - + {description} - {children} + {children} ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx index c8d162393299f..d8f74ba2eeb85 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/step_config.tsx @@ -49,7 +49,7 @@ const MONITOR_DETAILS_STEP = (readOnly: boolean = false): Step => ({ }); const SCRIPT_RECORDER_BTNS = ( - + + {link} - + {ACTIVE_LABEL} ); }, + mobileOptions: { + header: false, + }, }, ...(isBrowserType ? [ @@ -169,6 +175,7 @@ export const ErrorsList = ({
- + - - + + {monitorId && ( - - + + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx index 4d9fb7bc6e0d9..ce0c35152957c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_history/monitor_history.tsx @@ -55,8 +55,8 @@ export const MonitorHistory = () => { - - + + {/* @ts-expect-error Current @elastic/eui has the wrong types for the ref */} @@ -127,7 +127,7 @@ export const MonitorHistory = () => { - +

{DURATION_TREND_LABEL}

diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_header.tsx index c2df7ae05d7a9..78cf664de2f4b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_header.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_header.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; +import { useSyntheticsSettingsContext } from '../../../contexts'; +import { useSelectedLocation } from '../hooks/use_selected_location'; import { ConfigKey } from '../../../../../../common/runtime_types'; import { MONITOR_HISTORY_ROUTE } from '../../../../../../common/constants'; import { stringifyUrlParams } from '../../../utils/url_params'; -import { useGetUrlParams } from '../../../hooks'; import { useSelectedMonitor } from '../hooks/use_selected_monitor'; @@ -25,9 +25,18 @@ export const MonitorStatusHeader = ({ periodCaption, showViewHistoryButton, }: MonitorStatusPanelProps) => { - const history = useHistory(); - const params = useGetUrlParams(); + const { basePath } = useSyntheticsSettingsContext(); const { monitor } = useSelectedMonitor(); + const selectedLocation = useSelectedLocation(); + const search = stringifyUrlParams({ + locationId: selectedLocation?.id, + dateRangeStart: 'now-24h', + dateRangeEnd: 'now', + }); + const viewDetailsUrl = `${basePath}/app/synthetics${MONITOR_HISTORY_ROUTE.replace( + ':monitorId', + monitor?.[ConfigKey.CONFIG_ID] ?? '' + )}${search}`; const isLast24Hours = from === 'now-24h' && to === 'now'; const periodCaptionText = !!periodCaption @@ -43,45 +52,33 @@ export const MonitorStatusHeader = ({ css={{ marginBottom: 0, }} + wrap={true} > - - -

{labels.STATUS_LABEL}

-
-
- {periodCaptionText ? ( + - - {periodCaptionText} - + +

{labels.STATUS_LABEL}

+
- ) : null} - + {periodCaptionText ? ( + + + {periodCaptionText} + + + ) : null} +
{showViewHistoryButton ? ( - - - {labels.VIEW_HISTORY_LABEL} - - + + {labels.VIEW_HISTORY_LABEL} + ) : null}
); 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 d09f6672e73bd..1a7446625a769 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 @@ -60,6 +60,7 @@ export const LastTestRun = () => { latestPing={latestPing} loading={loading} stepsLoading={stepsLoading} + isErrorDetails={false} /> ); }; @@ -99,9 +100,7 @@ export const LastTestRunComponent = ({ color="danger" iconType="warning" > - {isErrorDetails ? ( - <> - ) : ( + {isErrorDetails ? null : ( - + {TitleNode} - + 0 ? 'fail' : 'success')} /> - + {lastRunTimestamp} {isBrowserMonitor ? ( - + - +

@@ -98,7 +98,7 @@ export const MonitorAlerts = ({ - + - + - + { const { from, to } = useMonitorRangeFrom(); const monitorId = useMonitorQueryId(); + const isFlyoutOpen = !!useTestFlyoutOpen(); const dateLabel = from === 'now-30d/d' ? LAST_30_DAYS_LABEL : TO_DATE_LABEL; @@ -42,12 +44,12 @@ export const MonitorSummary = () => { return ( - - + + - - + + @@ -60,39 +62,45 @@ export const MonitorSummary = () => { - - - - - - - - - - - - - - - {monitorId && ( - - )} - - - {monitorId && ( - - )} - + + + + + + + + + + + + + + + + + + + + {monitorId && ( + + )} + + + {monitorId && ( + + )} + + @@ -125,11 +133,11 @@ export const MonitorSummary = () => { showViewHistoryButton={true} /> - - + + - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx index ade5f47376804..854e600ddae90 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx @@ -9,9 +9,20 @@ import React, { MouseEvent, useMemo, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiBasicTableColumn, EuiPanel, EuiText } from '@elastic/eui'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiText, + useIsWithinMinBreakpoint, +} from '@elastic/eui'; import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; +import { THUMBNAIL_SCREENSHOT_SIZE_MOBILE } from '../../common/screenshot/screenshot_size'; +import { getErrorDetailsUrl } from '../monitor_errors/errors_list'; import { TestRunsTableHeader } from './test_runs_table_header'; import { MONITOR_TYPES } from '../../../../../../common/constants'; @@ -29,7 +40,7 @@ import { useSelectedMonitor } from '../hooks/use_selected_monitor'; import { useSelectedLocation } from '../hooks/use_selected_location'; import { useMonitorPings } from '../hooks/use_monitor_pings'; import { JourneyLastScreenshot } from '../../common/screenshot/journey_last_screenshot'; -import { useSyntheticsRefreshContext } from '../../../contexts'; +import { useSyntheticsRefreshContext, useSyntheticsSettingsContext } from '../../../contexts'; type SortableField = 'timestamp' | 'monitor.status' | 'monitor.duration.us'; @@ -47,6 +58,7 @@ export const TestRunsTable = ({ showViewHistoryButton = true, }: TestRunsTableProps) => { const history = useHistory(); + const { basePath } = useSyntheticsSettingsContext(); const { monitorId } = useParams<{ monitorId: string }>(); const [page, setPage] = useState({ index: 0, size: 10 }); @@ -71,6 +83,7 @@ export const TestRunsTable = ({ const pingsError = useSelector(selectPingsError); const { monitor } = useSelectedMonitor(); const selectedLocation = useSelectedLocation(); + const isTabletOrGreater = useIsWithinMinBreakpoint('s'); const isBrowserMonitor = monitor?.[ConfigKey.MONITOR_TYPE] === DataStream.BROWSER; @@ -105,6 +118,18 @@ export const TestRunsTable = ({ timestamp={timestamp} /> ), + mobileOptions: { + header: false, + render: (item) => ( + + + + ), + }, }, ] : []) as Array>), @@ -117,6 +142,17 @@ export const TestRunsTable = ({ render: (timestamp: string, ping: Ping) => ( ), + mobileOptions: { + header: false, + render: (item) => ( + + ), + }, }, { align: 'left', @@ -125,6 +161,9 @@ export const TestRunsTable = ({ name: RESULT_LABEL, sortable: true, render: (status: string) => , + mobileOptions: { + show: false, + }, }, { align: 'left', @@ -134,6 +173,9 @@ export const TestRunsTable = ({ render: (errorMessage: string) => ( {errorMessage?.length > 0 ? errorMessage : '-'} ), + mobileOptions: { + show: false, + }, }, { align: 'right', @@ -142,6 +184,9 @@ export const TestRunsTable = ({ name: DURATION_LABEL, sortable: true, render: (durationUs: number) => {formatTestDuration(durationUs)}, + mobileOptions: { + show: false, + }, }, ]; @@ -150,7 +195,6 @@ export const TestRunsTable = ({ return {}; } return { - height: '85px', 'data-test-subj': `row-${item.monitor.check_group}`, onClick: (evt: MouseEvent) => { const targetElem = evt.target as HTMLElement; @@ -180,6 +224,7 @@ export const TestRunsTable = ({ pings={pings} /> { + return ( + + + + + + + + {ping?.state?.id! && + ping.config_id && + locationId && + parseBadgeStatus(ping?.monitor?.status ?? 'skipped') === 'failed' ? ( + + {i18n.translate('xpack.synthetics.monitorDetails.summary.viewErrorDetails', { + defaultMessage: 'View error details', + })} + + ) : null} + + + + + {[ + { + title: DURATION_LABEL, + description: formatTestDuration(ping?.monitor?.duration?.us), + }, + ].map(({ title, description }) => ( + + {title} + {description} + + ))} + + + ); +}; + export const LAST_10_TEST_RUNS = i18n.translate( 'xpack.synthetics.monitorDetails.summary.lastTenTestRuns', { 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 3d94f0c5a43bf..67ce536fc3c8e 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 @@ -35,7 +35,7 @@ export const MonitorStats = ({ @@ -56,7 +56,13 @@ export const MonitorStats = ({ diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 8183cf363595a..53f14deb0dab5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -68,7 +68,12 @@ export const OverviewGrid = memo(() => { return ( <> - + - 0 ? 'danger' : 'success'}> + 0 ? 'danger' : 'success'} + css={{ maxWidth: 'max-content' }} + > {summaryDoc?.summary?.down! > 0 ? FAILED_LABEL : COMPLETED_LABEL} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_info.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_info.tsx index 62d94a52138c2..0e13c7efa251d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_info.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_info.tsx @@ -44,17 +44,17 @@ export const StepMetaInfo = ({ const isFailed = step.synthetics.step?.status === 'failed'; return ( - +

{STEP_NAME}

{step?.synthetics.step?.name} - + - + {AFTER_LABEL} {formatTestDuration(step?.synthetics.step?.duration.us)} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx index d3239e34617f1..ad3d18683ee27 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx @@ -25,8 +25,8 @@ export const StepScreenshotDetails = ({ return ( - - + + {step ? ( { <> {!hasNoSteps && ( - - + + @@ -86,7 +86,7 @@ export const TestRunDetails = () => { - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index d60c4c25d95a1..487e82e5538d9 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -21,6 +21,7 @@ import { useInspectorContext } from '@kbn/observability-plugin/public'; import { useSyntheticsPrivileges } from './hooks/use_synthetics_priviliges'; import { ClientPluginsStart } from '../../plugin'; import { getMonitorsRoute } from './components/monitors_page/route_config'; +import { SyntheticsPageTemplateComponent } from './components/common/page_template/synthetics_page_template'; import { getMonitorDetailsRoute } from './components/monitor_details/route_config'; import { getStepDetailsRoute } from './components/step_details_page/route_config'; import { getTestRunDetailsRoute } from './components/test_run_details/route_config'; @@ -54,13 +55,6 @@ const baseTitle = i18n.translate('xpack.synthetics.routes.baseTitle', { defaultMessage: 'Synthetics - Kibana', }); -export const MONITOR_MANAGEMENT_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.heading', - { - defaultMessage: 'Monitor Management', - } -); - const getRoutes = ( euiTheme: EuiThemeComputed, history: ReturnType, @@ -177,7 +171,7 @@ const RouteInit: React.FC> = ({ path, title } }; export const PageRouter: FC = () => { - const { application, observability } = useKibana().services; + const { application } = useKibana().services; const { addInspectorRequest } = useInspectorContext(); const { euiTheme } = useEuiTheme(); const history = useHistory(); @@ -189,7 +183,6 @@ export const PageRouter: FC = () => { location, application.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ); - const PageTemplateComponent = observability.navigation.PageTemplate; apiService.addInspectorRequest = addInspectorRequest; @@ -209,21 +202,21 @@ export const PageRouter: FC = () => {
- {isUnPrivileged || } - +
) )} ( - + { , ]} /> - + )} /> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/resize_observer.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/resize_observer.mock.ts new file mode 100644 index 0000000000000..b13767609f76f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/resize_observer.mock.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. + */ + +export class MockResizeObserver implements ResizeObserver { + private elements: Set = new Set(); + + observe(target: Element) { + this.elements.add(target); + } + unobserve(target: Element) { + this.elements.delete(target); + } + disconnect() { + this.elements.clear(); + } +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts index cebc9f5030293..8ba89472a8cf3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export * from './mock_globals'; export * from './rtl_helpers'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/mock_globals.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/mock_globals.ts new file mode 100644 index 0000000000000..7c987d9afb402 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/mock_globals.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockResizeObserver } from './__mocks__/resize_observer.mock'; + +export function mockGlobals() { + global.ResizeObserver = MockResizeObserver; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5c9625589e89f..76a4b553c062a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35342,7 +35342,6 @@ "xpack.synthetics.monitorManagement.getAPIKeyReducedPermissions.description": "Utilisez une clé d’API pour transmettre des moniteurs à distance à partir d'un pipeline CLI ou CD. Pour générer une clé d’API, vous devez disposer des autorisations de gérer les clés d’API et d’un accès en écriture à Uptime. Veuillez contacter votre administrateur.", "xpack.synthetics.monitorManagement.getProjectApiKey.label": "Générer une clé d'API de projet", "xpack.synthetics.monitorManagement.getProjectAPIKeyLabel.generate": "Générer une clé d'API de projet", - "xpack.synthetics.monitorManagement.heading": "Gestion des moniteurs", "xpack.synthetics.monitorManagement.hostFieldLabel": "Hôte", "xpack.synthetics.monitorManagement.inProgress": "EN COURS", "xpack.synthetics.monitorManagement.invalidLabel": "Non valide", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ca3dba7c283fe..4cc051fe6abd3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35321,7 +35321,6 @@ "xpack.synthetics.monitorManagement.getAPIKeyReducedPermissions.description": "APIキーを使用して、CLIまたはCDパイプラインからリモートでモニターをプッシュします。APIキーを生成するには、APIキーを管理する権限とアップタイム書き込み権限が必要です。管理者にお問い合わせください。", "xpack.synthetics.monitorManagement.getProjectApiKey.label": "プロジェクトAPIキーを生成", "xpack.synthetics.monitorManagement.getProjectAPIKeyLabel.generate": "プロジェクトAPIキーを生成", - "xpack.synthetics.monitorManagement.heading": "モニター管理", "xpack.synthetics.monitorManagement.hostFieldLabel": "ホスト", "xpack.synthetics.monitorManagement.inProgress": "進行中", "xpack.synthetics.monitorManagement.invalidLabel": "無効", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ead15654b7fe1..675a2ebc482ac 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35336,7 +35336,6 @@ "xpack.synthetics.monitorManagement.getAPIKeyReducedPermissions.description": "使用 API 密钥从 CLI 或 CD 管道远程推送监测。要生成 API 密钥,您必须有权管理 API 密钥并具有 Uptime 写入权限。请联系您的管理员。", "xpack.synthetics.monitorManagement.getProjectApiKey.label": "生成项目 API 密钥", "xpack.synthetics.monitorManagement.getProjectAPIKeyLabel.generate": "生成项目 API 密钥", - "xpack.synthetics.monitorManagement.heading": "监测管理", "xpack.synthetics.monitorManagement.hostFieldLabel": "主机", "xpack.synthetics.monitorManagement.inProgress": "进行中", "xpack.synthetics.monitorManagement.invalidLabel": "无效",