diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx index 7c817a1d70809..f321871628fb0 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx @@ -45,6 +45,7 @@ const COLUMNS: Array> = [ { field: 'status', name: i18n.TABLE_STATUS, + 'data-test-subj': 'maintenance-windows-column-status', render: (status: MaintenanceWindowStatus) => { return ( {STATUS_DISPLAY[status].label} @@ -168,7 +169,7 @@ export const MaintenanceWindowsList = React.memo( return ( = React.mem closePopover={closePopover} panelPaddingSize="none" anchorPosition="downCenter" + data-test-subj="table-actions-popover" > diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/index.test.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/index.test.tsx index 31efde4db1378..c0fa96a8e1637 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/index.test.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/index.test.tsx @@ -78,7 +78,7 @@ describe('Maintenance windows page', () => { }; appMockRenderer = createAppMockRenderer({ capabilities, license }); const result = appMockRenderer.render(); - expect(result.queryByTestId('mw-table')).toBeInTheDocument(); + expect(result.queryByTestId('maintenance-windows-table')).toBeInTheDocument(); expect(appMockRenderer.mocked.setBadge).toBeCalledTimes(1); }); }); diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 959245fd56caa..da6a867319d7c 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -45,6 +45,7 @@ import { SearchSessionsPageProvider } from './search_sessions_management_page'; import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections'; import { BannersPageObject } from './banners_page'; import { InfraHostsViewProvider } from './infra_hosts_view'; +import { MaintenanceWindowsPageProvider } from './maintenance_windows_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -88,4 +89,5 @@ export const pageObjects = { banners: BannersPageObject, detections: DetectionsPageObject, observability: ObservabilityPageProvider, + maintenanceWindows: MaintenanceWindowsPageProvider, }; diff --git a/x-pack/test/functional/page_objects/maintenance_windows_page.ts b/x-pack/test/functional/page_objects/maintenance_windows_page.ts new file mode 100644 index 0000000000000..64874db9b0a4f --- /dev/null +++ b/x-pack/test/functional/page_objects/maintenance_windows_page.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +const ENTER_KEY = '\uE007'; + +export function MaintenanceWindowsPageProvider({ getService }: FtrProviderContext) { + const find = getService('find'); + + return { + async getMaintenanceWindowsList() { + const table = await find.byCssSelector('[data-test-subj="maintenance-windows-table"] table'); + const $ = await table.parseDomContent(); + return $.findTestSubjects('list-item') + .toArray() + .map((row) => { + return { + status: $(row) + .findTestSubject('maintenance-windows-column-status') + .find('.euiTableCellContent') + .text(), + }; + }); + }, + async searchMaintenanceWindows(searchText: string) { + const searchBox = await find.byCssSelector( + '.euiFieldSearch:not(.euiSelectableTemplateSitewide__search)' + ); + await searchBox.click(); + await searchBox.clearValue(); + await searchBox.type(searchText); + await searchBox.pressKeys(ENTER_KEY); + await find.byCssSelector( + '.euiBasicTable[data-test-subj="maintenance-windows-table"]:not(.euiBasicTable-loading)' + ); + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index fbe6db9810cf9..395f8eb722321 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -16,5 +16,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./logs_list')); loadTestFile(require.resolve('./rules_settings')); + loadTestFile(require.resolve('./maintenance_windows')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/index.ts new file mode 100644 index 0000000000000..c0488c21adb21 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Maintenance Windows', function () { + loadTestFile(require.resolve('./maintenance_windows_table')); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts new file mode 100644 index 0000000000000..6a23cc5486469 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ObjectRemover } from '../../../lib/object_remover'; +import { generateUniqueKey } from '../../../lib/get_test_data'; +import { createMaintenanceWindow, createObjectRemover } from './utils'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'maintenanceWindows', 'header']); + const retry = getService('retry'); + + let objectRemover: ObjectRemover; + const browser = getService('browser'); + + describe('Maintenance windows table', function () { + before(async () => { + objectRemover = await createObjectRemover({ getService }); + }); + + beforeEach(async () => { + await pageObjects.common.navigateToApp('maintenanceWindows'); + }); + + after(async () => { + await objectRemover.removeAll(); + }); + + it('should should cancel a running maintenance window', async () => { + const name = generateUniqueKey(); + const createdMaintenanceWindow = await createMaintenanceWindow({ + name, + getService, + }); + objectRemover.add(createdMaintenanceWindow.id, 'rules/maintenance_window', 'alerting', true); + await browser.refresh(); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + let list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Running'); + + await testSubjects.click('table-actions-popover'); + await testSubjects.click('table-actions-cancel'); + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Cancelled running maintenance window '${name}'`); + }); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.not.eql('Running'); + }); + + it('should should archive finished maintenance window', async () => { + const name = generateUniqueKey(); + const createdMaintenanceWindow = await createMaintenanceWindow({ + name, + startDate: new Date('05-01-2023'), + notRecurring: true, + getService, + }); + objectRemover.add(createdMaintenanceWindow.id, 'rules/maintenance_window', 'alerting', true); + await browser.refresh(); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + let list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Finished'); + + await testSubjects.click('table-actions-popover'); + await testSubjects.click('table-actions-archive'); + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Archived maintenance window '${name}'`); + }); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Archived'); + }); + + it('should should cancel and archive a running maintenance window', async () => { + const name = generateUniqueKey(); + const createdMaintenanceWindow = await createMaintenanceWindow({ + name, + getService, + }); + objectRemover.add(createdMaintenanceWindow.id, 'rules/maintenance_window', 'alerting', true); + await browser.refresh(); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + let list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Running'); + + await testSubjects.click('table-actions-popover'); + await testSubjects.click('table-actions-cancel-and-archive'); + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Cancelled and archived running maintenance window '${name}'`); + }); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Archived'); + }); + + it('should should unarchive a maintenance window', async () => { + const name = generateUniqueKey(); + const createdMaintenanceWindow = await createMaintenanceWindow({ + name, + startDate: new Date('05-01-2023'), + notRecurring: true, + getService, + }); + objectRemover.add(createdMaintenanceWindow.id, 'rules/maintenance_window', 'alerting', true); + await browser.refresh(); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + let list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Finished'); + + await testSubjects.click('table-actions-popover'); + await testSubjects.click('table-actions-archive'); + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Archived maintenance window '${name}'`); + }); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Archived'); + + await testSubjects.click('table-actions-popover'); + await testSubjects.click('table-actions-unarchive'); + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Unarchived maintenance window '${name}'`); + }); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(1); + expect(list[0].status).to.eql('Finished'); + }); + + it('should filter maintenance windows by the status', async () => { + const running = await createMaintenanceWindow({ + name: 'running-mw', + getService, + }); + objectRemover.add(running.id, 'rules/maintenance_window', 'alerting', true); + const finished = await createMaintenanceWindow({ + name: 'finished-mw', + startDate: new Date('05-01-2023'), + notRecurring: true, + getService, + }); + objectRemover.add(finished.id, 'rules/maintenance_window', 'alerting', true); + + const date = new Date(); + date.setDate(date.getDate() + 1); + const upcoming = await createMaintenanceWindow({ + name: 'upcoming-mw', + startDate: date, + getService, + }); + objectRemover.add(upcoming.id, 'rules/maintenance_window', 'alerting', true); + await browser.refresh(); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows('mw'); + + const list = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(list.length).to.eql(3); + + await testSubjects.click('status-filter-button'); + await testSubjects.click('status-filter-upcoming'); // select Upcoming status filter + await retry.try(async () => { + const upcomingList = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(upcomingList.length).to.equal(1); + expect(upcomingList[0].status).to.equal('Upcoming'); + }); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/utils.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/utils.ts new file mode 100644 index 0000000000000..f2bf6334d6b29 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/utils.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ObjectRemover } from '../../../lib/object_remover'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export const createObjectRemover = async ({ + getService, +}: { + getService: FtrProviderContext['getService']; +}) => { + const supertest = getService('supertest'); + const objectRemover = new ObjectRemover(supertest); + + return objectRemover; +}; + +export const createMaintenanceWindow = async ({ + name, + startDate, + notRecurring, + getService, +}: { + name: string; + startDate?: Date; + notRecurring?: boolean; + getService: FtrProviderContext['getService']; +}) => { + const supertest = getService('supertest'); + const dtstart = startDate ? startDate : new Date(); + const createParams = { + title: name, + duration: 60 * 60 * 1000, + r_rule: { + dtstart: dtstart.toISOString(), + tzid: 'UTC', + ...(notRecurring ? { freq: 1, count: 1 } : { freq: 2 }), + }, + }; + + const { body } = await supertest + .post(`/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send(createParams) + .expect(200); + + return body; +}; diff --git a/x-pack/test/functional_with_es_ssl/config.base.ts b/x-pack/test/functional_with_es_ssl/config.base.ts index 533fec1944b67..29db8f6b5a89b 100644 --- a/x-pack/test/functional_with_es_ssl/config.base.ts +++ b/x-pack/test/functional_with_es_ssl/config.base.ts @@ -61,6 +61,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { triggersActionsConnectors: { pathname: '/app/management/insightsAndAlerting/triggersActionsConnectors', }, + maintenanceWindows: { + pathname: '/app/management/insightsAndAlerting/maintenanceWindows', + }, }, esTestCluster: { ...xpackFunctionalConfig.get('esTestCluster'), diff --git a/x-pack/test/functional_with_es_ssl/lib/object_remover.ts b/x-pack/test/functional_with_es_ssl/lib/object_remover.ts index 24620e4836121..5827ad082da3e 100644 --- a/x-pack/test/functional_with_es_ssl/lib/object_remover.ts +++ b/x-pack/test/functional_with_es_ssl/lib/object_remover.ts @@ -9,6 +9,7 @@ interface ObjectToRemove { id: string; type: string; plugin: string; + isInternal?: boolean; } export class ObjectRemover { @@ -19,15 +20,20 @@ export class ObjectRemover { this.supertest = supertest; } - add(id: ObjectToRemove['id'], type: ObjectToRemove['type'], plugin: ObjectToRemove['plugin']) { - this.objectsToRemove.push({ id, type, plugin }); + add( + id: ObjectToRemove['id'], + type: ObjectToRemove['type'], + plugin: ObjectToRemove['plugin'], + isInternal?: ObjectToRemove['isInternal'] + ) { + this.objectsToRemove.push({ id, type, plugin, isInternal }); } async removeAll() { await Promise.all( - this.objectsToRemove.map(({ id, type, plugin }) => { + this.objectsToRemove.map(({ id, type, plugin, isInternal }) => { return this.supertest - .delete(`/api/${plugin}/${type}/${id}`) + .delete(`/${isInternal ? 'internal' : 'api'}/${plugin}/${type}/${id}`) .set('kbn-xsrf', 'foo') .expect(204); })