diff --git a/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx b/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx index 9872e35d90c88..e1cd59f9a1378 100644 --- a/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx +++ b/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx @@ -47,23 +47,23 @@ export const TabbedTableListView = ({ [activeTabId, tabs] ); + const onFetchSuccess = useCallback(() => { + setHasInitialFetchReturned(true); + }, []); + const [tableList, setTableList] = useState(null); useEffect(() => { async function loadTableList() { const newTableList = await getActiveTab().getTableList({ - onFetchSuccess: () => { - if (!hasInitialFetchReturned) { - setHasInitialFetchReturned(true); - } - }, + onFetchSuccess, setPageDataTestSubject, }); setTableList(newTableList); } loadTableList(); - }, [hasInitialFetchReturned, activeTabId, tabs, getActiveTab]); + }, [activeTabId, tabs, getActiveTab, onFetchSuccess]); return ( diff --git a/packages/content-management/table_list_view/src/table_list_view.tsx b/packages/content-management/table_list_view/src/table_list_view.tsx index 71b08f82174bb..71e9f95bbf581 100644 --- a/packages/content-management/table_list_view/src/table_list_view.tsx +++ b/packages/content-management/table_list_view/src/table_list_view.tsx @@ -82,10 +82,8 @@ export const TableListView = ({ const [pageDataTestSubject, setPageDataTestSubject] = useState(); const onFetchSuccess = useCallback(() => { - if (!hasInitialFetchReturned) { - setHasInitialFetchReturned(true); - } - }, [hasInitialFetchReturned]); + setHasInitialFetchReturned(true); + }, []); return ( diff --git a/packages/content-management/table_list_view_table/src/reducer.tsx b/packages/content-management/table_list_view_table/src/reducer.tsx index c8486d92caced..0a8a89db80d92 100644 --- a/packages/content-management/table_list_view_table/src/reducer.tsx +++ b/packages/content-management/table_list_view_table/src/reducer.tsx @@ -39,11 +39,21 @@ export function getReducer() { } } + let hasNoItems = state.hasNoItems; + + const hasQuery = state.searchQuery.text !== ''; + if (hasQuery) { + hasNoItems = undefined; + } else { + hasNoItems = items.length === 0; + } + return { ...state, hasInitialFetchReturned: true, isFetchingItems: false, items, + hasNoItems, totalItems: action.data.response.total, hasUpdatedAtMetadata, tableSort: tableSort ?? state.tableSort, diff --git a/packages/content-management/table_list_view_table/src/table_list_view.test.helpers.ts b/packages/content-management/table_list_view_table/src/table_list_view.test.helpers.ts new file mode 100644 index 0000000000000..cbfa96198cc9d --- /dev/null +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.helpers.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { TestBed } from '@kbn/test-jest-helpers'; +import { act } from 'react-dom/test-utils'; + +export const getActions = ({ find, form, component }: TestBed) => { + /** Open the sort select drop down menu */ + const openSortSelect = () => { + find('tableSortSelectBtn').at(0).simulate('click'); + }; + + // --- Search Box --- + + /** Set the search box value */ + const updateSearchText = async (value: string) => { + await act(async () => { + find('tableListSearchBox').simulate('keyup', { + key: 'Enter', + target: { value }, + }); + }); + component.update(); + }; + + /** Get the Search box value */ + const getSearchBoxValue = () => find('tableListSearchBox').props().defaultValue; + + // --- Row Actions --- + const selectRow = (rowId: string) => { + act(() => { + form.selectCheckBox(`checkboxSelectRow-${rowId}`); + }); + component.update(); + }; + + const clickDeleteSelectedItemsButton = () => { + act(() => { + find('deleteSelectedItems').simulate('click'); + }); + component.update(); + }; + + const clickConfirmModalButton = async () => { + await act(async () => { + find('confirmModalConfirmButton').simulate('click'); + }); + component.update(); + }; + + return { + openSortSelect, + updateSearchText, + getSearchBoxValue, + selectRow, + clickDeleteSelectedItemsButton, + clickConfirmModalButton, + }; +}; diff --git a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx index 0fc1840f26613..94c1ca5ab6753 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx @@ -22,6 +22,7 @@ import { type TableListViewTableProps, type UserContentCommonSchema, } from './table_list_view_table'; +import { getActions } from './table_list_view.test.helpers'; const mockUseEffect = useEffect; @@ -54,12 +55,6 @@ const twoDaysAgoToString = new Date(twoDaysAgo.getTime()).toDateString(); const yesterday = new Date(new Date().setDate(new Date().getDate() - 1)); const yesterdayToString = new Date(yesterday.getTime()).toDateString(); -const getActions = (testBed: TestBed) => ({ - openSortSelect() { - testBed.find('tableSortSelectBtn').at(0).simulate('click'); - }, -}); - describe('TableListView', () => { const requiredProps: TableListViewTableProps = { entityName: 'test', @@ -91,50 +86,102 @@ describe('TableListView', () => { } ); - test('render default empty prompt', async () => { - let testBed: TestBed; + describe('empty prompt', () => { + test('render default empty prompt', async () => { + let testBed: TestBed; - await act(async () => { - testBed = await setup(); + await act(async () => { + testBed = await setup(); + }); + + const { component, exists } = testBed!; + component.update(); + + expect(component.find(EuiEmptyPrompt).length).toBe(1); + expect(exists('newItemButton')).toBe(false); }); - const { component, exists } = testBed!; - component.update(); + // avoid trapping users in empty prompt that can not create new items + test('render default empty prompt with create action when createItem supplied', async () => { + let testBed: TestBed; - expect(component.find(EuiEmptyPrompt).length).toBe(1); - expect(exists('newItemButton')).toBe(false); - }); + await act(async () => { + testBed = await setup({ createItem: () => undefined }); + }); - // avoid trapping users in empty prompt that can not create new items - test('render default empty prompt with create action when createItem supplied', async () => { - let testBed: TestBed; + const { component, exists } = testBed!; + component.update(); - await act(async () => { - testBed = await setup({ createItem: () => undefined }); + expect(component.find(EuiEmptyPrompt).length).toBe(1); + expect(exists('newItemButton')).toBe(true); }); - const { component, exists } = testBed!; - component.update(); + test('render custom empty prompt', async () => { + let testBed: TestBed; - expect(component.find(EuiEmptyPrompt).length).toBe(1); - expect(exists('newItemButton')).toBe(true); - }); + const CustomEmptyPrompt = () => { + return Table empty} />; + }; - test('render custom empty prompt', async () => { - let testBed: TestBed; + await act(async () => { + testBed = await setup({ emptyPrompt: }); + }); - const CustomEmptyPrompt = () => { - return Table empty} />; - }; + const { component, exists } = testBed!; + component.update(); - await act(async () => { - testBed = await setup({ emptyPrompt: }); + expect(exists('custom-empty-prompt')).toBe(true); }); - const { component, exists } = testBed!; - component.update(); + test('render empty prompt after deleting all items from table', async () => { + // NOTE: this test is using helpers that are being tested in the + // "should allow select items to be deleted" test below. + // If this test fails, check that one first. + + const hits: UserContentCommonSchema[] = [ + { + id: 'item-1', + type: 'dashboard', + updatedAt: '2020-01-01T00:00:00Z', + attributes: { + title: 'Item 1', + }, + references: [], + }, + ]; - expect(exists('custom-empty-prompt')).toBe(true); + const findItems = jest.fn().mockResolvedValue({ total: 1, hits }); + const deleteItems = jest.fn(); + + let testBed: TestBed; + + const EmptyPrompt = () => { + return Table empty} />; + }; + + await act(async () => { + testBed = await setup({ emptyPrompt: , findItems, deleteItems }); + }); + + const { component, exists, table } = testBed!; + const { selectRow, clickConfirmModalButton, clickDeleteSelectedItemsButton } = getActions( + testBed! + ); + component.update(); + + expect(exists('custom-empty-prompt')).toBe(false); + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + const [row] = tableCellsValues; + expect(row[1]).toBe('Item 1'); // Note: row[0] is the checkbox + + // We delete the item in the table and expect the empty prompt to show + findItems.mockResolvedValue({ total: 0, hits: [] }); + selectRow('item-1'); + clickDeleteSelectedItemsButton(); + await clickConfirmModalButton(); + + expect(exists('custom-empty-prompt')).toBe(true); + }); }); describe('default columns', () => { @@ -798,7 +845,20 @@ describe('TableListView', () => { let testBed: TestBed; const initialFilter = 'tag:(tag-1)'; - const findItems = jest.fn().mockResolvedValue({ total: 0, hits: [] }); + const findItems = jest.fn().mockResolvedValue({ + total: 1, + hits: [ + { + id: 'item-1', + type: 'dashboard', + updatedAt: new Date('2023-07-15').toISOString(), + attributes: { + title: 'Item 1', + }, + references: [], + }, + ], + }); await act(async () => { testBed = await setupInitialFilter({ @@ -824,6 +884,173 @@ describe('TableListView', () => { }); }); + describe('search', () => { + const updatedAt = new Date('2023-07-15').toISOString(); + + const hits: UserContentCommonSchema[] = [ + { + id: 'item-1', + type: 'dashboard', + updatedAt, + attributes: { + title: 'Item 1', + }, + references: [], + }, + { + id: 'item-2', + type: 'dashboard', + updatedAt, + attributes: { + title: 'Item 2', + }, + references: [], + }, + ]; + + const findItems = jest.fn(); + + const setupSearch = (...args: Parameters>) => { + const testBed = registerTestBed( + WithServices(TableListViewTable), + { + defaultProps: { + ...requiredProps, + findItems, + urlStateEnabled: false, + entityName: 'Foo', + entityNamePlural: 'Foos', + }, + memoryRouter: { wrapComponent: true }, + } + )(...args); + + const { updateSearchText, getSearchBoxValue } = getActions(testBed); + + return { + testBed, + updateSearchText, + getSearchBoxValue, + getLastCallArgsFromFindItems: () => findItems.mock.calls[findItems.mock.calls.length - 1], + }; + }; + + beforeEach(() => { + findItems.mockReset().mockResolvedValue({ total: hits.length, hits }); + }); + + test('should search the table items', async () => { + let testBed: TestBed; + let updateSearchText: (value: string) => Promise; + let getLastCallArgsFromFindItems: () => Parameters; + let getSearchBoxValue: () => string; + + await act(async () => { + ({ testBed, getLastCallArgsFromFindItems, getSearchBoxValue, updateSearchText } = + await setupSearch()); + }); + + const { component, table } = testBed!; + component.update(); + + let searchTerm = ''; + let expected = ''; + [searchTerm] = getLastCallArgsFromFindItems!(); + expect(getSearchBoxValue!()).toBe(expected); + expect(searchTerm).toBe(expected); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues).toMatchInlineSnapshot(` + Array [ + Array [ + "Item 1", + "Sat Jul 15 2023", + ], + Array [ + "Item 2", + "Sat Jul 15 2023", + ], + ] + `); + + findItems.mockResolvedValueOnce({ + total: 1, + hits: [ + { + id: 'item-from-search', + type: 'dashboard', + updatedAt: new Date('2023-07-01').toISOString(), + attributes: { + title: 'Item from search', + }, + references: [], + }, + ], + }); + + expected = 'foo'; + await updateSearchText!(expected); + [searchTerm] = getLastCallArgsFromFindItems!(); + expect(getSearchBoxValue!()).toBe(expected); + expect(searchTerm).toBe(expected); + + expect(table.getMetaData('itemsInMemTable').tableCellsValues).toMatchInlineSnapshot(` + Array [ + Array [ + "Item from search", + "July 1, 2023", + ], + ] + `); + }); + + test('should search and render empty list if no result', async () => { + let testBed: TestBed; + let updateSearchText: (value: string) => Promise; + + await act(async () => { + ({ testBed, updateSearchText } = await setupSearch()); + }); + + const { component, table, find } = testBed!; + component.update(); + + findItems.mockResolvedValueOnce({ + total: 0, + hits: [], + }); + + await updateSearchText!('unknown items'); + + expect(table.getMetaData('itemsInMemTable').tableCellsValues).toMatchInlineSnapshot(` + Array [ + Array [ + "No Foos matched your search.", + ], + ] + `); + + await act(async () => { + find('clearSearchButton').simulate('click'); + }); + component.update(); + + // We should get back the initial 2 items (Item 1 and Item 2) + expect(table.getMetaData('itemsInMemTable').tableCellsValues).toMatchInlineSnapshot(` + Array [ + Array [ + "Item 1", + "Sat Jul 15 2023", + ], + Array [ + "Item 2", + "Sat Jul 15 2023", + ], + ] + `); + }); + }); + describe('url state', () => { let router: Router | undefined; @@ -1153,10 +1380,11 @@ describe('TableListView', () => { }; test('should allow select items to be deleted', async () => { - const { - testBed: { table, find, exists, component, form }, - deleteItems, - } = await setupTest(); + const { testBed, deleteItems } = await setupTest(); + + const { table, exists, component } = testBed; + const { selectRow, clickDeleteSelectedItemsButton, clickConfirmModalButton } = + getActions(testBed); const { tableCellsValues } = table.getMetaData('itemsInMemTable'); @@ -1165,28 +1393,21 @@ describe('TableListView', () => { ['', 'Item 1Item 1 description', twoDaysAgoToString], ]); + // Select the second item const selectedHit = hits[1]; expect(exists('deleteSelectedItems')).toBe(false); - act(() => { - // Select the second item - form.selectCheckBox(`checkboxSelectRow-${selectedHit.id}`); - }); - component.update(); + + selectRow(selectedHit.id); // Delete button is now visible expect(exists('deleteSelectedItems')).toBe(true); // Click delete and validate that confirm modal opens expect(component.exists('.euiModal--confirmation')).toBe(false); - act(() => { - find('deleteSelectedItems').simulate('click'); - }); - component.update(); + clickDeleteSelectedItemsButton(); expect(component.exists('.euiModal--confirmation')).toBe(true); - await act(async () => { - find('confirmModalConfirmButton').simulate('click'); - }); + await clickConfirmModalButton(); expect(deleteItems).toHaveBeenCalledWith([selectedHit]); }); diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 33ef91dc65e4b..9d7613c6a122d 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -108,6 +108,7 @@ export interface TableListViewTableProps< contentEditor?: ContentEditorConfig; tableCaption: string; + /** Flag to force a new fetch of the table items. Whenever it changes, the `findItems()` will be called. */ refreshListBouncer?: boolean; onFetchSuccess: () => void; setPageDataTestSubject: (subject: string) => void; @@ -115,6 +116,12 @@ export interface TableListViewTableProps< export interface State { items: T[]; + /** + * Flag to indicate if there aren't any item when **no filteres are applied**. + * When there are no item we render an empty prompt. + * Default to `undefined` to indicate that we don't know yet if there are items or not. + */ + hasNoItems: boolean | undefined; hasInitialFetchReturned: boolean; isFetchingItems: boolean; isDeletingItems: boolean; @@ -293,6 +300,7 @@ function TableListViewTableComp({ const isMounted = useRef(false); const fetchIdx = useRef(0); + /** * The "onTableSearchChange()" handler has an async behavior. We want to be able to discard * previsous search changes and only handle the last one. For that we keep a counter of the changes. @@ -335,9 +343,10 @@ function TableListViewTableComp({ const initialState = useMemo>( () => ({ items: [], + hasNoItems: undefined, totalItems: 0, hasInitialFetchReturned: false, - isFetchingItems: false, + isFetchingItems: true, isDeletingItems: false, showDeleteModal: false, hasUpdatedAtMetadata: false, @@ -364,6 +373,7 @@ function TableListViewTableComp({ hasInitialFetchReturned, isFetchingItems, items, + hasNoItems, fetchError, showDeleteModal, isDeletingItems, @@ -374,8 +384,6 @@ function TableListViewTableComp({ tableSort, } = state; - const hasQuery = searchQuery.text !== ''; - const hasNoItems = hasInitialFetchReturned && items.length === 0 && !hasQuery; const showFetchError = Boolean(fetchError); const showLimitError = !showFetchError && totalItems > listingLimit; @@ -857,17 +865,7 @@ function TableListViewTableComp({ // ------------ // Effects // ------------ - useDebounce( - () => { - // Do not call fetchItems on dependency changes when initial fetch does not load any items - // to avoid flashing between empty table and no items view - if (!hasNoItems) { - fetchItems(); - } - }, - 300, - [fetchItems, refreshListBouncer] - ); + useDebounce(fetchItems, 300, [fetchItems, refreshListBouncer]); useEffect(() => { if (!urlStateEnabled) { diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.test.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.test.ts index 03c8c118edcf2..af9470d18e892 100644 --- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.test.ts +++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.test.ts @@ -91,6 +91,23 @@ describe('getEcsResponseLog', () => { `); }); + test('redacts es-client-authentication headers by default', () => { + const event = createResponseEvent({ + requestParams: { + headers: { 'es-client-authentication': 'ae3fda37-xxx', 'user-agent': 'world' }, + }, + response: { headers: { 'content-length': '123' } }, + }); + const log = getEcsResponseLog(event); + // @ts-expect-error ECS custom field + expect(log.http.request.headers).toMatchInlineSnapshot(` + Object { + "es-client-authentication": "[REDACTED]", + "user-agent": "world", + } + `); + }); + test('does not mutate original headers', () => { const reqHeaders = { a: 'foo', b: ['hello', 'world'] }; const resHeaders = { c: 'bar' }; diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.ts index fa122166ada08..6b44c48925438 100644 --- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.ts +++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/get_ecs_response_log.ts @@ -12,7 +12,13 @@ import { type LogMeta } from '@kbn/logging'; // If you are updating these, consider whether they should also be updated in the // http service `getResponseLog` -const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'set-cookie', 'x-elastic-app-auth']; +const FORBIDDEN_HEADERS = [ + 'authorization', + 'cookie', + 'set-cookie', + 'x-elastic-app-auth', + 'es-client-authentication', +]; const REDACTED_HEADER_TEXT = '[REDACTED]'; // We are excluding sensitive headers by default, until we have a log filtering mechanism. diff --git a/packages/core/http/core-http-server-internal/src/logging/get_response_log.test.ts b/packages/core/http/core-http-server-internal/src/logging/get_response_log.test.ts index a8f607c5c6865..a77283b6575ff 100644 --- a/packages/core/http/core-http-server-internal/src/logging/get_response_log.test.ts +++ b/packages/core/http/core-http-server-internal/src/logging/get_response_log.test.ts @@ -211,6 +211,21 @@ describe('getEcsResponseLog', () => { `); }); + test('redacts es-client-authentication headers by default', () => { + const req = createMockHapiRequest({ + headers: { 'es-client-authentication': 'ae3fda37-xxx', 'user-agent': 'world' }, + response: { headers: { 'content-length': '123' } }, + }); + const result = getEcsResponseLog(req, logger); + // @ts-expect-error ECS custom field + expect(result.meta.http.request.headers).toMatchInlineSnapshot(` + Object { + "es-client-authentication": "[REDACTED]", + "user-agent": "world", + } + `); + }); + test('does not mutate original headers', () => { const reqHeaders = { a: 'foo', b: ['hello', 'world'] }; const resHeaders = { headers: { c: 'bar' } }; diff --git a/packages/core/http/core-http-server-internal/src/logging/get_response_log.ts b/packages/core/http/core-http-server-internal/src/logging/get_response_log.ts index f356bf077095a..2cc9c50a1b319 100644 --- a/packages/core/http/core-http-server-internal/src/logging/get_response_log.ts +++ b/packages/core/http/core-http-server-internal/src/logging/get_response_log.ts @@ -16,7 +16,13 @@ import { getResponsePayloadBytes } from './get_payload_size'; // If you are updating these, consider whether they should also be updated in the // elasticsearch service `getEcsResponseLog` -const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'set-cookie', 'x-elastic-app-auth']; +const FORBIDDEN_HEADERS = [ + 'authorization', + 'cookie', + 'set-cookie', + 'x-elastic-app-auth', + 'es-client-authentication', +]; const REDACTED_HEADER_TEXT = '[REDACTED]'; type HapiHeaders = Record; diff --git a/src/plugins/event_annotation/public/components/table_list.tsx b/src/plugins/event_annotation/public/components/table_list.tsx index afc94b11e8fc6..2e172c4a9daed 100644 --- a/src/plugins/event_annotation/public/components/table_list.tsx +++ b/src/plugins/event_annotation/public/components/table_list.tsx @@ -77,8 +77,8 @@ export const EventAnnotationGroupTableList = ({ const [refreshListBouncer, setRefreshListBouncer] = useState(false); const refreshList = useCallback(() => { - setRefreshListBouncer(!refreshListBouncer); - }, [refreshListBouncer]); + setRefreshListBouncer((prev) => !prev); + }, []); const fetchItems = useCallback( ( diff --git a/src/plugins/visualizations/common/content_management/v1/cm_services.ts b/src/plugins/visualizations/common/content_management/v1/cm_services.ts index 62ae0afc97830..4b1165d357203 100644 --- a/src/plugins/visualizations/common/content_management/v1/cm_services.ts +++ b/src/plugins/visualizations/common/content_management/v1/cm_services.ts @@ -29,7 +29,7 @@ const referencesSchema = schema.arrayOf(referenceSchema); const visualizeAttributesSchema = schema.object( { title: schema.string(), - description: schema.maybe(schema.string()), + description: schema.maybe(schema.nullable(schema.string())), version: schema.maybe(schema.number()), kibanaSavedObjectMeta: schema.maybe(schema.object({ searchSourceJSON: schema.string() })), uiStateJSON: schema.maybe(schema.string()), diff --git a/test/functional/apps/dashboard/group1/embeddable_rendering.ts b/test/functional/apps/dashboard/group1/embeddable_rendering.ts index 93b111f072443..d7addf89ac404 100644 --- a/test/functional/apps/dashboard/group1/embeddable_rendering.ts +++ b/test/functional/apps/dashboard/group1/embeddable_rendering.ts @@ -102,6 +102,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/158529 describe.skip('dashboard embeddable rendering', function describeIndexTests() { + const from = 'Jan 1, 2018 @ 00:00:00.000'; + const to = 'Apr 13, 2018 @ 00:00:00.000'; before(async () => { await security.testUser.setRoles(['kibana_admin', 'animals', 'test_logstash_reader']); await kibanaServer.savedObjects.cleanStandardList(); @@ -111,14 +113,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); + await PageObjects.common.setTime({ from, to }); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.clickNewDashboard(); await elasticChart.setNewChartUiDebugFlag(true); - - const fromTime = 'Jan 1, 2018 @ 00:00:00.000'; - const toTime = 'Apr 13, 2018 @ 00:00:00.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); after(async () => { @@ -127,6 +126,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const newUrl = currentUrl.replace(/\?.*$/, ''); await browser.get(newUrl, false); await security.testUser.restoreDefaults(); + await PageObjects.common.unsetTime(); await kibanaServer.savedObjects.cleanStandardList(); }); diff --git a/test/functional/apps/dashboard/group5/share.ts b/test/functional/apps/dashboard/group5/share.ts index b6a2e9811e51f..a6d9289313e62 100644 --- a/test/functional/apps/dashboard/group5/share.ts +++ b/test/functional/apps/dashboard/group5/share.ts @@ -106,19 +106,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); + const from = 'Sep 19, 2017 @ 06:31:44.000'; + const to = 'Sep 23, 2018 @ 18:31:44.000'; + await PageObjects.common.setTime({ from, to }); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('few panels'); - await PageObjects.dashboard.switchToEditMode(); - const from = 'Sep 19, 2017 @ 06:31:44.000'; - const to = 'Sep 23, 2018 @ 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(from, to); await PageObjects.dashboard.waitForRenderComplete(); }); after(async () => { await kibanaServer.savedObjects.cleanStandardList(); + await PageObjects.common.unsetTime(); }); describe('snapshot share', async () => { diff --git a/test/functional/apps/dashboard_elements/controls/common/range_slider.ts b/test/functional/apps/dashboard_elements/controls/common/range_slider.ts index d6cd7388987fe..9140c23573c3f 100644 --- a/test/functional/apps/dashboard_elements/controls/common/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/common/range_slider.ts @@ -19,9 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); - const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([ + const { dashboardControls, common, dashboard, header } = getPageObjects([ 'dashboardControls', - 'timePicker', 'dashboard', 'common', 'header', @@ -46,16 +45,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); + await common.setTime({ + from: 'Oct 22, 2018 @ 00:00:00.000', + to: 'Dec 3, 2018 @ 00:00:00.000', + }); await common.navigateToApp('dashboard'); await dashboardControls.enableControlsLab(); await common.navigateToApp('dashboard'); await dashboard.preserveCrossAppState(); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); - await timePicker.setAbsoluteRange( - 'Oct 22, 2018 @ 00:00:00.000', - 'Dec 3, 2018 @ 00:00:00.000' - ); await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); }); @@ -66,6 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await esArchiver.unload('test/functional/fixtures/es_archiver/kibana_sample_data_flights'); await kibanaServer.uiSettings.unset('defaultIndex'); + await common.unsetTime(); await security.testUser.restoreDefaults(); }); diff --git a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts index 1bfdc9143ad4c..8961af57a4ad2 100644 --- a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts +++ b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts @@ -32,10 +32,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); + await PageObjects.common.setTime({ + from: 'Sep 22, 2015 @ 00:00:00.000', + to: 'Sep 23, 2015 @ 00:00:00.000', + }); }); after(async () => { await kibanaServer.savedObjects.cleanStandardList(); + await PageObjects.common.unsetTime(); }); beforeEach(async () => { @@ -43,10 +48,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.ensureFieldEditorModalIsClosed(); await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 22, 2015 @ 00:00:00.000', - 'Sep 23, 2015 @ 00:00:00.000' - ); }); const addSearchEmbeddableToDashboard = async () => { diff --git a/test/functional/apps/management/_files.ts b/test/functional/apps/management/_files.ts index 54b22028275d7..deebdb18f374b 100644 --- a/test/functional/apps/management/_files.ts +++ b/test/functional/apps/management/_files.ts @@ -13,8 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'filesManagement']); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/160178 - describe.skip('Files management', () => { + describe('Files management', () => { before(async () => { await PageObjects.filesManagement.navigateTo(); }); diff --git a/test/functional/apps/visualize/group3/_linked_saved_searches.ts b/test/functional/apps/visualize/group3/_linked_saved_searches.ts index c27d5725e50ac..f97a4c86e4560 100644 --- a/test/functional/apps/visualize/group3/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/group3/_linked_saved_searches.ts @@ -31,19 +31,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await PageObjects.visualize.initTests(); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); await filterBar.addFilter({ field: 'extension.raw', operation: 'is', value: 'jpg' }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.saveSearch(savedSearchName); discoverSavedSearchUrlPath = (await browser.getCurrentUrl()).split('?')[0]; }); + after(async () => { + await PageObjects.common.unsetTime(); + }); + it('should create a visualization from a saved search', async () => { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickSavedSearch(savedSearchName); - await PageObjects.timePicker.setDefaultAbsoluteRange(); await retry.waitFor('wait for count to equal 9,109', async () => { const data = await PageObjects.visChart.getTableVisContent(); return data[0][0] === '9,109'; diff --git a/test/functional/apps/visualize/group6/_tsvb_markdown.ts b/test/functional/apps/visualize/group6/_tsvb_markdown.ts index 9f259ff005364..f015a0a010748 100644 --- a/test/functional/apps/visualize/group6/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/group6/_tsvb_markdown.ts @@ -11,9 +11,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualBuilder, timePicker, visualize, visChart } = getPageObjects([ + const { visualBuilder, common, visualize, visChart } = getPageObjects([ 'visualBuilder', - 'timePicker', + 'common', 'visualize', 'visChart', ]); @@ -37,20 +37,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('markdown', () => { before(async () => { await visualize.initTests(); + await common.setTime({ + to: 'Sep 22, 2015 @ 06:00:00.000', + from: 'Sep 22, 2015 @ 11:00:00.000', + }); await visualize.navigateToNewVisualization(); await visualize.clickVisualBuilder(); await visualBuilder.checkVisualBuilderIsPresent(); await visualBuilder.clickMarkdown(); - await timePicker.setAbsoluteRange( - 'Sep 22, 2015 @ 06:00:00.000', - 'Sep 22, 2015 @ 11:00:00.000' - ); await visualBuilder.markdownSwitchSubTab('options'); await visualBuilder.setMetricsDataTimerangeMode('Last value'); await visualBuilder.setDropLastBucket(true); await visualBuilder.markdownSwitchSubTab('markdown'); }); + after(async () => { + await common.unsetTime(); + }); + it('should render subtabs and table variables markdown components', async () => { const tabs = await visualBuilder.getSubTabs(); expect(tabs).to.have.length(3); diff --git a/x-pack/plugins/cloud_integrations/cloud_data_migration/server/config.ts b/x-pack/plugins/cloud_integrations/cloud_data_migration/server/config.ts index d1b9842857d47..e6a7fce2541f2 100644 --- a/x-pack/plugins/cloud_integrations/cloud_data_migration/server/config.ts +++ b/x-pack/plugins/cloud_integrations/cloud_data_migration/server/config.ts @@ -9,7 +9,14 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from '@kbn/core-plugins-server'; const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // cloud_data_migration is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }); export type CloudDataMigrationConfig = TypeOf; diff --git a/x-pack/plugins/cross_cluster_replication/server/config.ts b/x-pack/plugins/cross_cluster_replication/server/config.ts index 4cba6d0707abb..1d83f53920576 100644 --- a/x-pack/plugins/cross_cluster_replication/server/config.ts +++ b/x-pack/plugins/cross_cluster_replication/server/config.ts @@ -26,7 +26,14 @@ const schemaLatest = schema.object( * Disables the plugin. * Added back in 8.8. */ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // CCR is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts index 9db799997fad3..52ce24634886e 100644 --- a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts +++ b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts @@ -6,7 +6,7 @@ */ import type { AgentPolicy } from '../types'; -import { FLEET_SERVER_PACKAGE, FLEET_APM_PACKAGE } from '../constants'; +import { FLEET_SERVER_PACKAGE, FLEET_APM_PACKAGE, FLEET_SYNTHETICS_PACKAGE } from '../constants'; export function policyHasFleetServer(agentPolicy: AgentPolicy) { if (!agentPolicy.package_policies) { @@ -19,8 +19,17 @@ export function policyHasFleetServer(agentPolicy: AgentPolicy) { } export function policyHasAPMIntegration(agentPolicy: AgentPolicy) { + return policyHasIntegration(agentPolicy, FLEET_APM_PACKAGE); +} + +export function policyHasSyntheticsIntegration(agentPolicy: AgentPolicy) { + return policyHasIntegration(agentPolicy, FLEET_SYNTHETICS_PACKAGE); +} + +function policyHasIntegration(agentPolicy: AgentPolicy, packageName: string) { if (!agentPolicy.package_policies) { return false; } - return agentPolicy.package_policies?.some((p) => p.package?.name === FLEET_APM_PACKAGE); + + return agentPolicy.package_policies?.some((p) => p.package?.name === packageName); } diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 6b392f506b4c6..abf06ac54b07c 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -63,7 +63,11 @@ export { export { getAllowedOutputTypeForPolicy } from './output_helpers'; export { agentStatusesToSummary } from './agent_statuses_to_summary'; -export { policyHasFleetServer, policyHasAPMIntegration } from './agent_policies_helpers'; +export { + policyHasFleetServer, + policyHasAPMIntegration, + policyHasSyntheticsIntegration, +} from './agent_policies_helpers'; export { generateNewAgentPolicyWithDefaults, diff --git a/x-pack/plugins/fleet/common/services/output_helpers.ts b/x-pack/plugins/fleet/common/services/output_helpers.ts index d0d8c826878fd..00b480c378b61 100644 --- a/x-pack/plugins/fleet/common/services/output_helpers.ts +++ b/x-pack/plugins/fleet/common/services/output_helpers.ts @@ -6,7 +6,12 @@ */ import type { AgentPolicy } from '../types'; -import { FLEET_APM_PACKAGE, FLEET_SERVER_PACKAGE, outputType } from '../constants'; +import { + FLEET_APM_PACKAGE, + FLEET_SERVER_PACKAGE, + FLEET_SYNTHETICS_PACKAGE, + outputType, +} from '../constants'; /** * Return allowed output type for a given agent policy, @@ -16,7 +21,10 @@ export function getAllowedOutputTypeForPolicy(agentPolicy: AgentPolicy) { const isRestrictedToSameClusterES = agentPolicy.package_policies && agentPolicy.package_policies.some( - (p) => p.package?.name === FLEET_APM_PACKAGE || p.package?.name === FLEET_SERVER_PACKAGE + (p) => + p.package?.name === FLEET_APM_PACKAGE || + p.package?.name === FLEET_SERVER_PACKAGE || + p.package?.name === FLEET_SYNTHETICS_PACKAGE ); if (isRestrictedToSameClusterES) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx index 1a415e51e23a6..2d7eef6b864bf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/hooks.test.tsx @@ -130,7 +130,7 @@ describe('useOutputOptions', () => { size="s" > { size="s" > { size="s" > { size="s" > { size="s" > { size="s" > { size="s" > { size="s" > getAllowedOutputTypeForPolicy(agentPolicy as AgentPolicy), [agentPolicy] @@ -89,7 +90,7 @@ export function useOutputOptions(agentPolicy: Partial ) : undefined ), - disabled: !isLicenceAllowingPolicyPerOutput || isOutputTypeUnsupported, + disabled: !isPolicyPerOutputAllowed || isOutputTypeUnsupported, }; }), ]; - }, [outputsRequest, isLicenceAllowingPolicyPerOutput, allowedOutputTypes]); + }, [outputsRequest, isPolicyPerOutputAllowed, allowedOutputTypes]); const monitoringOutputOptions = useMemo(() => { if (outputsRequest.isLoading || !outputsRequest.data) { @@ -135,11 +136,11 @@ export function useOutputOptions(agentPolicy: Partial ({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx index 8d96c5f314547..4157c6964f79a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/confirm_update.tsx @@ -73,13 +73,13 @@ const ConfirmDescription: React.FunctionComponent = ({ title={ } > {' '} diff --git a/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap b/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap deleted file mode 100644 index 6a9de18956e43..0000000000000 --- a/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap +++ /dev/null @@ -1,258 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Fleet preconfiguration reset Preconfigured cloud policy With a full preconfigured cloud policy Create correct .fleet-policies 1`] = ` -Object { - "agent": Object { - "download": Object { - "sourceURI": "https://artifacts.elastic.co/downloads/", - }, - "features": Object {}, - "monitoring": Object { - "enabled": false, - "logs": false, - "metrics": false, - }, - "protection": Object { - "enabled": false, - "signing_key": "", - "uninstall_token_hash": "", - }, - }, - "id": "policy-elastic-agent-on-cloud", - "inputs": Array [ - Object { - "data_stream": Object { - "namespace": "default", - }, - "id": "fleet-server-fleet_server-elastic-cloud-fleet-server", - "meta": Object { - "package": Object { - "name": "fleet_server", - }, - }, - "name": "Fleet Server", - "package_policy_id": "elastic-cloud-fleet-server", - "revision": 1, - "server.runtime": Object { - "gc_percent": 20, - }, - "type": "fleet-server", - "unused_key": "not_used", - "use_output": "es-containerhost", - }, - Object { - "apm-server": Object { - "agent": Object { - "config": Object { - "elasticsearch": Object { - "api_key": "", - }, - }, - }, - "agent_config": Array [], - "auth": Object { - "anonymous": Object { - "allow_agent": Array [ - "rum-js", - "js-base", - "iOS/swift", - ], - "allow_service": null, - "enabled": true, - "rate_limit": Object { - "event_limit": 300, - "ip_limit": 1000, - }, - }, - "api_key": Object { - "enabled": true, - "limit": 100, - }, - "secret_token": "CLOUD_SECRET_TOKEN", - }, - "capture_personal_data": true, - "default_service_environment": null, - "expvar.enabled": false, - "host": "0.0.0.0:8200", - "idle_timeout": "45s", - "java_attacher": Object { - "discovery-rules": null, - "download-agent-version": null, - "enabled": false, - }, - "max_connections": 0, - "max_event_size": 307200, - "max_header_size": 1048576, - "pprof.enabled": false, - "read_timeout": "3600s", - "response_headers": null, - "rum": Object { - "allow_headers": null, - "allow_origins": Array [ - "*", - ], - "enabled": true, - "exclude_from_grouping": "^/webpack", - "library_pattern": "node_modules|bower_components|~", - "response_headers": null, - "source_mapping": Object { - "elasticsearch": Object { - "api_key": "", - }, - "metadata": Array [], - }, - }, - "sampling": Object { - "tail": Object { - "enabled": false, - "interval": "1m", - "policies": Array [ - Object { - "sample_rate": 0.1, - }, - ], - "storage_limit": "3GB", - }, - }, - "shutdown_timeout": "30s", - "ssl": Object { - "certificate": "/app/config/certs/node.crt", - "cipher_suites": null, - "curve_types": null, - "enabled": true, - "key": "/app/config/certs/node.key", - "key_passphrase": null, - "supported_protocols": Array [ - "TLSv1.1", - "TLSv1.2", - "TLSv1.3", - ], - }, - "write_timeout": "30s", - }, - "data_stream": Object { - "namespace": "default", - }, - "id": "elastic-cloud-apm", - "meta": Object { - "package": Object { - "name": "apm", - }, - }, - "name": "Elastic APM", - "package_policy_id": "elastic-cloud-apm", - "revision": 2, - "type": "apm", - "use_output": "es-containerhost", - }, - ], - "output_permissions": Object { - "es-containerhost": Object { - "_elastic_agent_checks": Object { - "cluster": Array [ - "monitor", - ], - }, - "_elastic_agent_monitoring": Object { - "indices": Array [], - }, - "elastic-cloud-apm": Object { - "cluster": Array [ - "cluster:monitor/main", - ], - "indices": Array [ - Object { - "names": Array [ - "logs-apm.app-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - Object { - "names": Array [ - "metrics-apm.app.*-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - Object { - "names": Array [ - "logs-apm.error-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - Object { - "names": Array [ - "metrics-apm.internal-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - Object { - "names": Array [ - "metrics-apm.profiling-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - Object { - "names": Array [ - "traces-apm.rum-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - Object { - "names": Array [ - "traces-apm.sampled-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - "maintenance", - "monitor", - "read", - ], - }, - Object { - "names": Array [ - "traces-apm-default", - ], - "privileges": Array [ - "auto_configure", - "create_doc", - ], - }, - ], - }, - }, - }, - "outputs": Object { - "es-containerhost": Object { - "hosts": Array [ - "https://cloudinternales:9200", - ], - "type": "elasticsearch", - }, - }, - "revision": 5, - "secret_references": Array [], - "signed": Object { - "data": "", - "signature": "", - }, -} -`; diff --git a/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts index 7c18ef95997f6..7da8edb68f9f7 100644 --- a/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts @@ -164,7 +164,201 @@ describe('Fleet preconfiguration reset', () => { data.signed.data = ''; data.signed.signature = ''; - expect(data).toMatchSnapshot(); + expect(data).toEqual({ + agent: { + download: { + sourceURI: 'https://artifacts.elastic.co/downloads/', + }, + features: {}, + monitoring: { + enabled: false, + logs: false, + metrics: false, + }, + protection: { + enabled: false, + signing_key: '', + uninstall_token_hash: '', + }, + }, + id: 'policy-elastic-agent-on-cloud', + inputs: [ + { + data_stream: { + namespace: 'default', + }, + id: 'fleet-server-fleet_server-elastic-cloud-fleet-server', + meta: { + package: { + name: 'fleet_server', + }, + }, + name: 'Fleet Server', + package_policy_id: 'elastic-cloud-fleet-server', + revision: 1, + 'server.runtime': { + gc_percent: 20, + }, + type: 'fleet-server', + unused_key: 'not_used', + use_output: 'es-containerhost', + }, + { + 'apm-server': { + agent: { + config: { + elasticsearch: { + api_key: '', + }, + }, + }, + agent_config: [], + auth: { + anonymous: { + allow_agent: ['rum-js', 'js-base', 'iOS/swift'], + allow_service: null, + enabled: true, + rate_limit: { + event_limit: 300, + ip_limit: 1000, + }, + }, + api_key: { + enabled: true, + limit: 100, + }, + secret_token: 'CLOUD_SECRET_TOKEN', + }, + capture_personal_data: true, + default_service_environment: null, + 'expvar.enabled': false, + host: '0.0.0.0:8200', + idle_timeout: '45s', + java_attacher: { + 'discovery-rules': null, + 'download-agent-version': null, + enabled: false, + }, + max_connections: 0, + max_event_size: 307200, + max_header_size: 1048576, + 'pprof.enabled': false, + read_timeout: '3600s', + response_headers: null, + rum: { + allow_headers: null, + allow_origins: ['*'], + enabled: true, + exclude_from_grouping: '^/webpack', + library_pattern: 'node_modules|bower_components|~', + response_headers: null, + source_mapping: { + elasticsearch: { + api_key: '', + }, + metadata: [], + }, + }, + sampling: { + tail: { + enabled: false, + interval: '1m', + policies: [ + { + sample_rate: 0.1, + }, + ], + storage_limit: '3GB', + }, + }, + shutdown_timeout: '30s', + ssl: { + certificate: '/app/config/certs/node.crt', + cipher_suites: null, + curve_types: null, + enabled: true, + key: '/app/config/certs/node.key', + key_passphrase: null, + supported_protocols: ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + }, + write_timeout: '30s', + }, + data_stream: { + namespace: 'default', + }, + id: 'elastic-cloud-apm', + meta: { + package: { + name: 'apm', + }, + }, + name: 'Elastic APM', + package_policy_id: 'elastic-cloud-apm', + revision: 2, + type: 'apm', + use_output: 'es-containerhost', + }, + ], + output_permissions: { + 'es-containerhost': { + _elastic_agent_checks: { + cluster: ['monitor'], + }, + _elastic_agent_monitoring: { + indices: [], + }, + 'elastic-cloud-apm': { + cluster: ['cluster:monitor/main'], + indices: [ + { + names: ['logs-apm.app-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['metrics-apm.app.*-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['logs-apm.error-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['metrics-apm.internal-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['metrics-apm.profiling-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['traces-apm.rum-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['traces-apm.sampled-default'], + privileges: ['auto_configure', 'create_doc', 'maintenance', 'monitor', 'read'], + }, + { + names: ['traces-apm-default'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }, + }, + outputs: { + 'es-containerhost': { + hosts: ['https://cloudinternales:9200'], + type: 'elasticsearch', + }, + }, + revision: 5, + secret_references: [], + signed: { + data: '', + signature: '', + }, + }); }); it('Create correct package policies', async () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts index 29d48d5d595ef..e43bc275a9c4d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts @@ -196,7 +196,7 @@ describe('validateOutputForPolicy', () => { ); }); - it('should not allow logstash output to be used with a policy using fleet server or APM', async () => { + it('should not allow logstash output to be used with a policy using fleet server, synthetics or APM', async () => { mockHasLicence(true); mockedOutputService.get.mockResolvedValue({ type: 'logstash', @@ -217,7 +217,7 @@ describe('validateOutputForPolicy', () => { ); }); - it('should allow elasticsearch output to be used with a policy using fleet server or APM', async () => { + it('should allow elasticsearch output to be used with a policy using fleet server, synthetics or APM', async () => { mockHasLicence(true); mockedOutputService.get.mockResolvedValue({ type: 'elasticsearch', diff --git a/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts b/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts index d93bb965295ef..bbe00c49b414f 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts @@ -9,7 +9,7 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { AgentPolicySOAttributes, AgentPolicy } from '../../types'; import { LICENCE_FOR_PER_POLICY_OUTPUT, outputType } from '../../../common/constants'; -import { policyHasFleetServer } from '../../../common/services'; +import { policyHasFleetServer, policyHasSyntheticsIntegration } from '../../../common/services'; import { appContextService } from '..'; import { outputService } from '../output'; import { OutputInvalidError, OutputLicenceError } from '../../errors'; @@ -80,6 +80,9 @@ export async function validateOutputForPolicy( // Validate output when the policy has fleet server if (policyHasFleetServer(data as AgentPolicy)) return; + // Validate output when the policy has synthetics integration + if (policyHasSyntheticsIntegration(data as AgentPolicy)) return; + const hasLicence = appContextService .getSecurityLicense() .hasAtLeast(LICENCE_FOR_PER_POLICY_OUTPUT); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index d32737e0eb5c1..6958ea80c00d6 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -48,6 +48,7 @@ import { packageToPackagePolicy, policyHasFleetServer, policyHasAPMIntegration, + policyHasSyntheticsIntegration, } from '../../common/services'; import { agentPolicyStatuses, @@ -211,6 +212,10 @@ class AgentPolicyService { return policyHasFleetServer(agentPolicy); } + public hasSyntheticsIntegration(agentPolicy: AgentPolicy) { + return policyHasSyntheticsIntegration(agentPolicy); + } + public async create( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index 7c0076abfc653..5ecd3a4a29a40 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -201,7 +201,7 @@ function getMockedSoClient( describe('Output Service', () => { const esClientMock = elasticsearchServiceMock.createElasticsearchClient(); - const mockedAgentPolicyResolvedValue = { + const mockedAgentPolicyWithFleetServerResolvedValue = { items: [ { name: 'fleet server policy', @@ -232,18 +232,50 @@ describe('Output Service', () => { ], } as unknown as ReturnType; + const mockedAgentPolicyWithSyntheticsResolvedValue = { + items: [ + { + name: 'synthetics policy', + id: 'synthetics_policy', + package_policies: [ + { + name: 'synthetics-123', + package: { + name: 'synthetics', + }, + }, + ], + }, + { + name: 'agent policy 1', + id: 'agent_policy_1', + is_managed: false, + package_policies: [ + { + name: 'nginx', + package: { + name: 'nginx', + }, + }, + ], + }, + ], + } as unknown as ReturnType; + beforeEach(() => { mockedAgentPolicyService.list.mockClear(); mockedAgentPolicyService.hasAPMIntegration.mockClear(); mockedAgentPolicyService.hasFleetServerIntegration.mockClear(); + mockedAgentPolicyService.hasSyntheticsIntegration.mockClear(); mockedAgentPolicyService.removeOutputFromAll.mockReset(); mockedAppContextService.getInternalUserSOClient.mockReset(); mockedAppContextService.getEncryptedSavedObjectsSetup.mockReset(); mockedAuditLoggingService.writeCustomSoAuditLog.mockReset(); mockedAgentPolicyService.update.mockReset(); }); + describe('create', () => { - it('work with a predefined id', async () => { + it('works with a predefined id', async () => { const soClient = getMockedSoClient(); await outputService.create( @@ -446,7 +478,9 @@ describe('Output Service', () => { mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ canEncrypt: true, } as any); - mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyResolvedValue); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.create( @@ -470,6 +504,37 @@ describe('Output Service', () => { ); }); + it('should update synthetics policies with data_output_id=default_output_id if a new default logstash output is created', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ + canEncrypt: true, + } as any); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await outputService.create( + soClient, + esClientMock, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'logstash', + }, + { id: 'output-1' } + ); + + expect(mockedAgentPolicyService.update).toBeCalledWith( + expect.anything(), + expect.anything(), + 'synthetics_policy', + { data_output_id: 'output-test' }, + { force: false } + ); + }); + it('Should allow to create a new logstash output with no errors if is not set as default', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', @@ -477,7 +542,9 @@ describe('Output Service', () => { mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ canEncrypt: true, } as any); - mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyResolvedValue); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.create( @@ -523,7 +590,9 @@ describe('Output Service', () => { mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ canEncrypt: true, } as any); - mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyResolvedValue); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.create( @@ -547,6 +616,37 @@ describe('Output Service', () => { ); }); + it('Should update synthetics policies with data_output_id=default_output_id if a new default kafka output is created', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ + canEncrypt: true, + } as any); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await outputService.create( + soClient, + esClientMock, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'kafka', + }, + { id: 'output-1' } + ); + + expect(mockedAgentPolicyService.update).toBeCalledWith( + expect.anything(), + expect.anything(), + 'synthetics_policy', + { data_output_id: 'output-test' }, + { force: false } + ); + }); + it('Should allow to create a new kafka output with no errors if is not set as default', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', @@ -554,7 +654,9 @@ describe('Output Service', () => { mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({ canEncrypt: true, } as any); - mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyResolvedValue); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.create( @@ -878,6 +980,7 @@ describe('Output Service', () => { } as unknown as ReturnType); mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(false); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(false); await outputService.update(soClient, esClientMock, 'existing-es-output', { type: 'logstash', @@ -936,36 +1039,9 @@ describe('Output Service', () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', }); - mockedAgentPolicyService.list.mockResolvedValue({ - items: [ - { - name: 'fleet server policy', - id: 'fleet_server_policy', - is_default_fleet_server: true, - package_policies: [ - { - name: 'fleet-server-123', - package: { - name: 'fleet_server', - }, - }, - ], - }, - { - name: 'agent policy 1', - id: 'agent_policy_1', - is_managed: false, - package_policies: [ - { - name: 'nginx', - package: { - name: 'nginx', - }, - }, - ], - }, - ], - } as unknown as ReturnType); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.update(soClient, esClientMock, 'output-test', { @@ -994,36 +1070,9 @@ describe('Output Service', () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', }); - mockedAgentPolicyService.list.mockResolvedValue({ - items: [ - { - name: 'fleet server policy', - id: 'fleet_server_policy', - is_default_fleet_server: true, - package_policies: [ - { - name: 'fleet-server-123', - package: { - name: 'fleet_server', - }, - }, - ], - }, - { - name: 'agent policy 1', - id: 'agent_policy_1', - is_managed: false, - package_policies: [ - { - name: 'nginx', - package: { - name: 'nginx', - }, - }, - ], - }, - ], - } as unknown as ReturnType); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.update( @@ -1056,38 +1105,77 @@ describe('Output Service', () => { ); }); + it('should update synthetics policies with data_output_id=default_output_id if a default ES output is changed to logstash', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await outputService.update(soClient, esClientMock, 'output-test', { + type: 'logstash', + hosts: ['test:4343'], + is_default: true, + }); + + expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { + type: 'logstash', + hosts: ['test:4343'], + is_default: true, + ca_sha256: null, + ca_trusted_fingerprint: null, + }); + expect(mockedAgentPolicyService.update).toBeCalledWith( + expect.anything(), + expect.anything(), + 'synthetics_policy', + { data_output_id: 'output-test' }, + { force: false } + ); + }); + + it('should update synthetics policies with data_output_id=default_output_id and force=true if a default ES output is changed to logstash, from preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await outputService.update( + soClient, + esClientMock, + 'output-test', + { + type: 'logstash', + hosts: ['test:4343'], + is_default: true, + }, + { + fromPreconfiguration: true, + } + ); + + expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { + type: 'logstash', + hosts: ['test:4343'], + is_default: true, + ca_sha256: null, + ca_trusted_fingerprint: null, + }); + expect(mockedAgentPolicyService.update).toBeCalledWith( + expect.anything(), + expect.anything(), + 'synthetics_policy', + { data_output_id: 'output-test' }, + { force: true } + ); + }); + it('Should return an error if trying to change the output to logstash for fleet server policy', async () => { const soClient = getMockedSoClient({}); - mockedAgentPolicyService.list.mockResolvedValue({ - items: [ - { - name: 'fleet server policy', - id: 'fleet_server_policy', - is_default_fleet_server: true, - package_policies: [ - { - name: 'fleet-server-123', - package: { - name: 'fleet_server', - }, - }, - ], - }, - { - name: 'agent policy 1', - id: 'agent_policy_1', - is_managed: false, - package_policies: [ - { - name: 'nginx', - package: { - name: 'nginx', - }, - }, - ], - }, - ], - } as unknown as ReturnType); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await expect( @@ -1100,6 +1188,22 @@ describe('Output Service', () => { ); }); + it('Should return an error if trying to change the output to logstash for synthetics policy', async () => { + const soClient = getMockedSoClient({}); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(false); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await expect( + outputService.update(soClient, esClientMock, 'existing-es-output', { + type: 'logstash', + hosts: ['test:4343'], + }) + ).rejects.toThrowError( + 'Logstash output cannot be used with Synthetics integration in synthetics policy. Please create a new ElasticSearch output.' + ); + }); + it('should call audit logger', async () => { const soClient = getMockedSoClient({ defaultOutputId: 'existing-es-output' }); @@ -1115,7 +1219,6 @@ describe('Output Service', () => { }); // With Kafka output - it('Should delete ES specific fields if the output type changes to kafka', async () => { const soClient = getMockedSoClient({}); mockedAgentPolicyService.list.mockResolvedValue({ @@ -1123,6 +1226,7 @@ describe('Output Service', () => { } as unknown as ReturnType); mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(false); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(false); await outputService.update(soClient, esClientMock, 'existing-es-output', { type: 'kafka', @@ -1180,7 +1284,9 @@ describe('Output Service', () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', }); - mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyResolvedValue); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.update(soClient, esClientMock, 'output-test', { @@ -1218,7 +1324,9 @@ describe('Output Service', () => { const soClient = getMockedSoClient({ defaultOutputId: 'output-test', }); - mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyResolvedValue); + mockedAgentPolicyService.list.mockResolvedValue( + mockedAgentPolicyWithFleetServerResolvedValue + ); mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(true); await outputService.update( @@ -1259,6 +1367,90 @@ describe('Output Service', () => { { force: true } ); }); + + it('should update synthetics policies with data_output_id=default_output_id if a default ES output is changed to kafka', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await outputService.update(soClient, esClientMock, 'output-test', { + type: 'kafka', + hosts: ['http://test:4343'], + is_default: true, + }); + + expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { + type: 'kafka', + hosts: ['http://test:4343'], + is_default: true, + ca_sha256: null, + ca_trusted_fingerprint: null, + client_id: 'Elastic Agent', + compression: 'gzip', + compression_level: 4, + partition: 'hash', + timeout: 30, + version: '1.0.0', + broker_timeout: 10, + broker_ack_reliability: 'Wait for local commit', + broker_buffer_size: 256, + }); + expect(mockedAgentPolicyService.update).toBeCalledWith( + expect.anything(), + expect.anything(), + 'synthetics_policy', + { data_output_id: 'output-test' }, + { force: false } + ); + }); + + it('should update synthetics policies with data_output_id=default_output_id and force=true if a default ES output is changed to kafka, from preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + mockedAgentPolicyService.list.mockResolvedValue(mockedAgentPolicyWithSyntheticsResolvedValue); + mockedAgentPolicyService.hasSyntheticsIntegration.mockReturnValue(true); + + await outputService.update( + soClient, + esClientMock, + 'output-test', + { + type: 'kafka', + hosts: ['http://test:4343'], + is_default: true, + }, + { + fromPreconfiguration: true, + } + ); + + expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), { + type: 'kafka', + hosts: ['http://test:4343'], + is_default: true, + ca_sha256: null, + ca_trusted_fingerprint: null, + client_id: 'Elastic Agent', + compression: 'gzip', + compression_level: 4, + partition: 'hash', + timeout: 30, + version: '1.0.0', + broker_timeout: 10, + broker_ack_reliability: 'Wait for local commit', + broker_buffer_size: 256, + }); + expect(mockedAgentPolicyService.update).toBeCalledWith( + expect.anything(), + expect.anything(), + 'synthetics_policy', + { data_output_id: 'output-test' }, + { force: true } + ); + }); }); describe('delete', () => { diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index a9e546d878fb5..ec5f63cbe3566 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -148,7 +148,7 @@ async function validateLogstashOutputNotUsedInAPMPolicy( } } -async function findPoliciesWithFleetServer( +async function findPoliciesWithFleetServerOrSynthetics( soClient: SavedObjectsClientContract, outputId?: string, isDefault?: boolean @@ -159,23 +159,24 @@ async function findPoliciesWithFleetServer( ? await getAgentPoliciesPerOutput(soClient, outputId, isDefault) : (await agentPolicyService.list(soClient, { withPackagePolicies: true }))?.items; - if (agentPolicies) { - const policiesWithFleetServer = agentPolicies.filter((policy) => - agentPolicyService.hasFleetServerIntegration(policy) - ); - return policiesWithFleetServer; - } - return []; + const policiesWithFleetServer = + agentPolicies?.filter((policy) => agentPolicyService.hasFleetServerIntegration(policy)) || []; + const policiesWithSynthetics = + agentPolicies?.filter((policy) => agentPolicyService.hasSyntheticsIntegration(policy)) || []; + return { policiesWithFleetServer, policiesWithSynthetics }; } -function validateOutputNotUsedInFleetServerPolicy( +function validateOutputNotUsedInPolicy( agentPolicies: AgentPolicy[], - dataOutputType: OutputType['Logstash'] | OutputType['Kafka'] + dataOutputType: OutputType['Logstash'] | OutputType['Kafka'], + integrationName: string ) { - // Validate no policy with fleet server use that policy + // Validate no policy with this integration uses that output for (const agentPolicy of agentPolicies) { throw new OutputInvalidError( - `${_.capitalize(dataOutputType)} output cannot be used with Fleet Server integration in ${ + `${_.capitalize( + dataOutputType + )} output cannot be used with ${integrationName} integration in ${ agentPolicy.name }. Please create a new ElasticSearch output.` ); @@ -192,44 +193,46 @@ async function validateTypeChanges( fromPreconfiguration: boolean ) { const mergedIsDefault = data.is_default ?? originalOutput.is_default; - const fleetServerPolicies = await findPoliciesWithFleetServer(soClient, id, mergedIsDefault); + const { policiesWithFleetServer, policiesWithSynthetics } = + await findPoliciesWithFleetServerOrSynthetics(soClient, id, mergedIsDefault); if (data.type === outputType.Logstash || originalOutput.type === outputType.Logstash) { await validateLogstashOutputNotUsedInAPMPolicy(soClient, id, mergedIsDefault); } - // prevent changing an ES output to logstash or kafka if it's used by fleet server policies + // prevent changing an ES output to logstash or kafka if it's used by fleet server or synthetics policies if ( originalOutput.type === outputType.Elasticsearch && (data?.type === outputType.Logstash || data?.type === outputType.Kafka) ) { // Validate no policy with fleet server use that policy - validateOutputNotUsedInFleetServerPolicy(fleetServerPolicies, data.type); + validateOutputNotUsedInPolicy(policiesWithFleetServer, data.type, 'Fleet Server'); + validateOutputNotUsedInPolicy(policiesWithSynthetics, data.type, 'Synthetics'); } - await updateFleetServerPoliciesDataOutputId( + await updateAgentPoliciesDataOutputId( soClient, esClient, data, mergedIsDefault, defaultDataOutputId, - fleetServerPolicies, + _.uniq([...policiesWithFleetServer, ...policiesWithSynthetics]), fromPreconfiguration ); } -async function updateFleetServerPoliciesDataOutputId( +async function updateAgentPoliciesDataOutputId( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, data: Nullable>, isDefault: boolean, defaultDataOutputId: string | null, - fleetServerPolicies: AgentPolicy[], + agentPolicies: AgentPolicy[], fromPreconfiguration: boolean ) { // if a logstash output is updated to become default // if fleet server policies don't have data_output_id // update them to use the default output if ((data?.type === outputType.Logstash || data?.type === outputType.Kafka) && isDefault) { - for (const policy of fleetServerPolicies) { + for (const policy of agentPolicies) { if (!policy.data_output_id) { await agentPolicyService.update( soClient, @@ -415,14 +418,15 @@ class OutputService { ); } } - const fleetServerPolicies = await findPoliciesWithFleetServer(soClient); - await updateFleetServerPoliciesDataOutputId( + const { policiesWithFleetServer, policiesWithSynthetics } = + await findPoliciesWithFleetServerOrSynthetics(soClient); + await updateAgentPoliciesDataOutputId( soClient, esClient, data, data.is_default, defaultDataOutputId, - fleetServerPolicies, + _.uniq([...policiesWithFleetServer, ...policiesWithSynthetics]), options?.fromPreconfiguration ?? false ); diff --git a/x-pack/plugins/index_lifecycle_management/server/config.ts b/x-pack/plugins/index_lifecycle_management/server/config.ts index 7fdec20bbb050..5a02e404ad2b7 100644 --- a/x-pack/plugins/index_lifecycle_management/server/config.ts +++ b/x-pack/plugins/index_lifecycle_management/server/config.ts @@ -28,7 +28,14 @@ const schemaLatest = schema.object( * Disables the plugin. * Added back in 8.8. */ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // ILM is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/lens/common/content_management/v1/cm_services.ts b/x-pack/plugins/lens/common/content_management/v1/cm_services.ts index c0e09f5041110..22e1af6272978 100644 --- a/x-pack/plugins/lens/common/content_management/v1/cm_services.ts +++ b/x-pack/plugins/lens/common/content_management/v1/cm_services.ts @@ -28,7 +28,7 @@ const referencesSchema = schema.arrayOf(referenceSchema); const lensAttributesSchema = schema.object( { title: schema.string(), - description: schema.maybe(schema.string()), + description: schema.maybe(schema.nullable(schema.string())), visualizationType: schema.maybe(schema.string()), state: schema.maybe(schema.any()), uiStateJSON: schema.maybe(schema.string()), diff --git a/x-pack/plugins/license_management/server/config.ts b/x-pack/plugins/license_management/server/config.ts index 23449bc19e793..f413335cc23ef 100644 --- a/x-pack/plugins/license_management/server/config.ts +++ b/x-pack/plugins/license_management/server/config.ts @@ -26,7 +26,14 @@ const schemaLatest = schema.object( * Disables the plugin. * Added back in 8.8. */ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // License Management is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/remote_clusters/server/config.ts b/x-pack/plugins/remote_clusters/server/config.ts index 4f6c56191cd89..12275985a3aa9 100644 --- a/x-pack/plugins/remote_clusters/server/config.ts +++ b/x-pack/plugins/remote_clusters/server/config.ts @@ -26,7 +26,14 @@ const schemaLatest = schema.object( * Disables the plugin. * Added back in 8.8. */ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // Remote Clusters is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/rollup/server/config.ts b/x-pack/plugins/rollup/server/config.ts index 953cd4b283f97..c0d5c1fbe92ee 100644 --- a/x-pack/plugins/rollup/server/config.ts +++ b/x-pack/plugins/rollup/server/config.ts @@ -26,7 +26,14 @@ const schemaLatest = schema.object( * Disables the plugin. * Added back in 8.8. */ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // Rollups is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); diff --git a/x-pack/plugins/security_solution/cypress/e2e/explore/dashboards/entity_analytics.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/explore/dashboards/entity_analytics.cy.ts index a8d2146e4433b..4829a8d939331 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/explore/dashboards/entity_analytics.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/explore/dashboards/entity_analytics.cy.ts @@ -10,7 +10,12 @@ import { login, visit } from '../../../tasks/login'; import { ALERTS_URL, ENTITY_ANALYTICS_URL } from '../../../urls/navigation'; import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; -import { cleanKibana, deleteAlertsAndRules } from '../../../tasks/common'; +import { + cleanKibana, + deleteAlertsAndRules, + waitForPageToBeLoaded, + waitForTableToLoad, +} from '../../../tasks/common'; import { ANOMALIES_TABLE, ANOMALIES_TABLE_ROWS, @@ -313,11 +318,15 @@ describe('Entity Analytics Dashboard', () => { beforeEach(() => { login(); visit(ENTITY_ANALYTICS_URL); + waitForPageToBeLoaded(); }); it('renders table with pagination', () => { cy.get(ANOMALIES_TABLE).should('be.visible'); - cy.get(ANOMALIES_TABLE_ROWS).should('have.length', 10); + waitForTableToLoad(); + + // Increase default timeout because anomalies table takes a while to load + cy.get(ANOMALIES_TABLE_ROWS, { timeout: 20000 }).should('have.length', 10); // navigates to next page cy.get(ANOMALIES_TABLE_NEXT_PAGE_BUTTON).click(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/explore/host_details/risk_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/explore/host_details/risk_tab.cy.ts index 6bcb5cd9532ee..a57d49a602c53 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/explore/host_details/risk_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/explore/host_details/risk_tab.cy.ts @@ -7,7 +7,7 @@ import { login, visitHostDetailsPage } from '../../../tasks/login'; -import { cleanKibana } from '../../../tasks/common'; +import { cleanKibana, waitForTableToLoad } from '../../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; import { TABLE_CELL, TABLE_ROWS } from '../../../screens/alerts_details'; @@ -49,8 +49,3 @@ describe('risk tab', () => { ); }); }); - -export const waitForTableToLoad = () => { - cy.get('.euiBasicTable-loading').should('exist'); - cy.get('.euiBasicTable-loading').should('not.exist'); -}; diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts index fb21d9d5a445d..44a2326aebc8e 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts @@ -43,6 +43,7 @@ import { DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE, DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY, DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE, + DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS, DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE, DOCUMENT_DETAILS_FLYOUT_JSON_TAB, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB, @@ -85,6 +86,8 @@ describe( cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE).should('be.visible'); cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE) .should('be.visible') diff --git a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel.ts b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel.ts index 50de0eb348264..60ebbe7fdd071 100644 --- a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel.ts +++ b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel.ts @@ -20,6 +20,7 @@ import { FLYOUT_HEADER_RISK_SCORE_VALUE_TEST_ID, FLYOUT_HEADER_SEVERITY_TITLE_TEST_ID, FLYOUT_HEADER_SEVERITY_VALUE_TEST_ID, + FLYOUT_HEADER_STATUS_BUTTON_TEST_ID, FLYOUT_HEADER_TITLE_TEST_ID, } from '../../../public/flyout/right/components/test_ids'; @@ -42,6 +43,9 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB = getDataTestSubjectSelector(OVERVIEW_TAB_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB = getDataTestSubjectSelector(TABLE_TAB_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_JSON_TAB = getDataTestSubjectSelector(JSON_TAB_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS = getDataTestSubjectSelector( + FLYOUT_HEADER_STATUS_BUTTON_TEST_ID +); export const DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE = getDataTestSubjectSelector( FLYOUT_HEADER_RISK_SCORE_TITLE_TEST_ID ); diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index c91a331323ff5..716dc27c2e3be 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -12,6 +12,7 @@ import { LOADING_INDICATOR, LOADING_INDICATOR_HIDDEN, } from '../screens/security_header'; +import { EUI_BASIC_TABLE_LOADING } from '../screens/common/controls'; const primaryButton = 0; @@ -282,3 +283,8 @@ export const waitForWelcomePanelToBeLoaded = () => { cy.get(KIBANA_LOADING_ICON).should('exist'); cy.get(KIBANA_LOADING_ICON).should('not.exist'); }; + +export const waitForTableToLoad = () => { + cy.get(EUI_BASIC_TABLE_LOADING).should('exist'); + cy.get(EUI_BASIC_TABLE_LOADING).should('not.exist'); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap index 6ee4557d2ee22..c676809c3c2a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap @@ -91,6 +91,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `