Skip to content

Commit

Permalink
[ResponseOps][Window Maintenance] Add window maintenance to alerting …
Browse files Browse the repository at this point in the history
…plugin public folder and add create form (#153445)

Resolves #152272

## Summary

- This PR adds maintenance windows to the alerting plugin and also adds
the create form.
- The Maintenance Windows feature is hidden by a feature flag by
default.
- The create form calls the create api, but it's expected to fail
because the api hasn't been merged.

<img width="1709" alt="Screen Shot 2023-04-03 at 2 56 45 PM"
src="https://user-images.githubusercontent.com/109488926/229601948-13a46847-bebc-4674-9446-55163a6a6589.png">

<img width="1706" alt="Screen Shot 2023-04-03 at 2 56 56 PM"
src="https://user-images.githubusercontent.com/109488926/229601997-03b232cf-0d50-4047-a92b-79ef0c1e9568.png">

<img width="1706" alt="Screen Shot 2023-04-03 at 2 57 12 PM"
src="https://user-images.githubusercontent.com/109488926/229602015-7e0110e6-dd3d-41c4-8601-64943fe01323.png">


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios


### To verify
- Enable maintenance windows by adding `ENABLE_MAINTENANCE_WINDOWS =
true` to `x-pack/plugins/alerting/common/index.ts`
- Go to
http://localhost:5601/app/management/insightsAndAlerting/maintenanceWindows
or select the Maintenance Windows link on the side nav
- Click the `Create a maintenance window` button and verify it takes you
to the create from
- Verify the bread crumbs and the return link work correctly
- Once on the create form, try to submit without entering any values and
verify that the form doesn't submit and errors are shown on the `name`
and `duration` fields
- Submit a maintenance window that is not recurring. You'll see an error
popup but verify that the request body is correct and matches what
you've selected in the form.
- Create a maintenance window that is recurring and try choosing the
different options on the recurring form. Verify that the summary at the
bottom of the form accurately reflects what you have selected.
- Submit a maintenance window that is recurring. Again, you'll see an
error popup but verify that the request body is correct and matches what
you've selected in the form.

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
doakalexi and kibanamachine authored Apr 13, 2023
1 parent 3b07f96 commit 752147e
Show file tree
Hide file tree
Showing 63 changed files with 3,999 additions and 16 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ export const BASE_ALERTING_API_PATH = '/api/alerting';
export const INTERNAL_BASE_ALERTING_API_PATH = '/internal/alerting';
export const ALERTS_FEATURE_ID = 'alerts';
export const MONITORING_HISTORY_LIMIT = 200;
export const ENABLE_MAINTENANCE_WINDOWS = false;
5 changes: 4 additions & 1 deletion x-pack/plugins/alerting/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
"features",
"kibanaUtils",
"licensing",
"taskManager"
"taskManager",
"kibanaReact",
"management",
"esUiShared",
],
"optionalPlugins": [
"usageCollection",
Expand Down
90 changes: 90 additions & 0 deletions x-pack/plugins/alerting/public/application/maintenance_windows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { Router, Switch } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Route } from '@kbn/shared-ux-router';
import { CoreStart } from '@kbn/core/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { EuiLoadingSpinner } from '@elastic/eui';
import { AlertingPluginStart } from '../plugin';
import { paths } from '../config';

const MaintenanceWindowsLazy: React.FC = React.lazy(() => import('../pages/maintenance_windows'));
const MaintenanceWindowsCreateLazy: React.FC = React.lazy(
() => import('../pages/maintenance_windows/maintenance_window_create_page')
);

const App = React.memo(() => {
return (
<>
<Switch>
<Route path="/" exact>
<Suspense fallback={<EuiLoadingSpinner />}>
<MaintenanceWindowsLazy />
</Suspense>
</Route>
<Route path={paths.alerting.maintenanceWindowsCreate} exact>
<Suspense fallback={<EuiLoadingSpinner />}>
<MaintenanceWindowsCreateLazy />
</Suspense>
</Route>
</Switch>
</>
);
});
App.displayName = 'App';

export const renderApp = ({
core,
plugins,
mountParams,
kibanaVersion,
}: {
core: CoreStart;
plugins: AlertingPluginStart;
mountParams: ManagementAppMountParams;
kibanaVersion: string;
}) => {
const { element, history, theme$ } = mountParams;
const i18nCore = core.i18n;
const isDarkMode = core.uiSettings.get('theme:darkMode');

const queryClient = new QueryClient();

ReactDOM.render(
<KibanaThemeProvider theme$={theme$}>
<KibanaContextProvider
services={{
...core,
...plugins,
storage: new Storage(localStorage),
kibanaVersion,
}}
>
<Router history={history}>
<EuiThemeProvider darkMode={isDarkMode}>
<i18nCore.Context>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</i18nCore.Context>
</EuiThemeProvider>
</Router>
</KibanaContextProvider>
</KibanaThemeProvider>,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
10 changes: 10 additions & 0 deletions x-pack/plugins/alerting/public/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { paths, AlertingDeepLinkId, APP_ID, MAINTENANCE_WINDOWS_APP_ID } from './paths';

export type { IAlertingDeepLinkId } from './paths';
23 changes: 23 additions & 0 deletions x-pack/plugins/alerting/public/config/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const MAINTENANCE_WINDOWS_APP_ID = 'maintenanceWindows';
export const APP_ID = 'management';

export const paths = {
alerting: {
maintenanceWindows: `/${MAINTENANCE_WINDOWS_APP_ID}`,
maintenanceWindowsCreate: '/create',
},
};

export const AlertingDeepLinkId = {
maintenanceWindows: MAINTENANCE_WINDOWS_APP_ID,
maintenanceWindowsCreate: 'create',
};

export type IAlertingDeepLinkId = typeof AlertingDeepLinkId[keyof typeof AlertingDeepLinkId];
74 changes: 74 additions & 0 deletions x-pack/plugins/alerting/public/hooks/use_breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { renderHook } from '@testing-library/react-hooks';
import { useBreadcrumbs } from './use_breadcrumbs';
import { AlertingDeepLinkId } from '../config';
import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils';

const mockSetBreadcrumbs = jest.fn();
const mockSetTitle = jest.fn();

jest.mock('../utils/kibana_react', () => {
const originalModule = jest.requireActual('../utils/kibana_react');
return {
...originalModule,
useKibana: () => {
const { services } = originalModule.useKibana();
return {
services: {
...services,
chrome: { setBreadcrumbs: mockSetBreadcrumbs, docTitle: { change: mockSetTitle } },
},
};
},
};
});

jest.mock('./use_navigation', () => {
const originalModule = jest.requireActual('./use_navigation');
return {
...originalModule,
useNavigation: jest.fn().mockReturnValue({
getAppUrl: jest.fn((params?: { deepLinkId: string }) => params?.deepLinkId ?? '/test'),
}),
};
});

let appMockRenderer: AppMockRenderer;

describe('useBreadcrumbs', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRenderer = createAppMockRenderer();
});

test('set maintenance windows breadcrumbs', () => {
renderHook(() => useBreadcrumbs(AlertingDeepLinkId.maintenanceWindows), {
wrapper: appMockRenderer.AppWrapper,
});
expect(mockSetBreadcrumbs).toHaveBeenCalledWith([
{ href: '/test', onClick: expect.any(Function), text: 'Stack Management' },
{ text: 'Maintenance Windows' },
]);
});

test('set create maintenance windows breadcrumbs', () => {
renderHook(() => useBreadcrumbs(AlertingDeepLinkId.maintenanceWindowsCreate), {
wrapper: appMockRenderer.AppWrapper,
});
expect(mockSetBreadcrumbs).toHaveBeenCalledWith([
{ href: '/test', onClick: expect.any(Function), text: 'Stack Management' },
{
href: AlertingDeepLinkId.maintenanceWindows,
onClick: expect.any(Function),
text: 'Maintenance Windows',
},
{ text: 'Create' },
]);
});
});
95 changes: 95 additions & 0 deletions x-pack/plugins/alerting/public/hooks/use_breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';
import { ChromeBreadcrumb } from '@kbn/core/public';
import { MouseEvent, useEffect } from 'react';
import { useKibana } from '../utils/kibana_react';
import { useNavigation } from './use_navigation';
import { APP_ID, AlertingDeepLinkId, IAlertingDeepLinkId } from '../config';

const breadcrumbTitle: Record<IAlertingDeepLinkId, string> = {
[AlertingDeepLinkId.maintenanceWindows]: i18n.translate(
'xpack.alerting.breadcrumbs.maintenanceWindowsLinkText',
{
defaultMessage: 'Maintenance Windows',
}
),
[AlertingDeepLinkId.maintenanceWindowsCreate]: i18n.translate(
'xpack.alerting.breadcrumbs.createMaintenanceWindowsLinkText',
{
defaultMessage: 'Create',
}
),
};

const topLevelBreadcrumb: Record<string, IAlertingDeepLinkId> = {
[AlertingDeepLinkId.maintenanceWindowsCreate]: AlertingDeepLinkId.maintenanceWindows,
};

function addClickHandlers(
breadcrumbs: ChromeBreadcrumb[],
navigateToHref?: (url: string) => Promise<void>
) {
return breadcrumbs.map((bc) => ({
...bc,
...(bc.href
? {
onClick: (event: MouseEvent) => {
if (navigateToHref && bc.href) {
event.preventDefault();
navigateToHref(bc.href);
}
},
}
: {}),
}));
}

function getTitleFromBreadCrumbs(breadcrumbs: ChromeBreadcrumb[]) {
return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse();
}

export const useBreadcrumbs = (pageDeepLink: IAlertingDeepLinkId) => {
const {
services: {
chrome: { docTitle, setBreadcrumbs },
application: { navigateToUrl },
},
} = useKibana();
const setTitle = docTitle.change;
const { getAppUrl } = useNavigation(APP_ID);

useEffect(() => {
const breadcrumbs = [
{
text: i18n.translate('xpack.alerting.breadcrumbs.stackManagementLinkText', {
defaultMessage: 'Stack Management',
}),
href: getAppUrl(),
},
...(topLevelBreadcrumb[pageDeepLink]
? [
{
text: breadcrumbTitle[topLevelBreadcrumb[pageDeepLink]],
href: getAppUrl({ deepLinkId: topLevelBreadcrumb[pageDeepLink] }),
},
]
: []),
{
text: breadcrumbTitle[pageDeepLink],
},
];

if (setBreadcrumbs) {
setBreadcrumbs(addClickHandlers(breadcrumbs, navigateToUrl));
}
if (setTitle) {
setTitle(getTitleFromBreadCrumbs(breadcrumbs));
}
}, [pageDeepLink, getAppUrl, navigateToUrl, setBreadcrumbs, setTitle]);
};
Loading

0 comments on commit 752147e

Please sign in to comment.