[RAM][Maintenance Window] Add categories to banner title (#169704)

## Summary

Fixes #169591

Adds category names to the Maintenance Window banner title. This will
make it clearer why the banner is showing up, and which rules it
affects.

The banner will still **always** appear in Stack Management if there's
an active maintenance window, because Stack Management displays all
rules.

<img width="818" alt="Screenshot 2023-10-24 at 1 13 05 PM"
src="e5ad6486-4afd-42fa-b507-63de9374710b">
<img width="993" alt="Screenshot 2023-10-24 at 1 12 51 PM"
src="10b8cc13-618d-4ff2-9100-c007ebc637c7">
<img width="922" alt="Screenshot 2023-10-24 at 1 33 23 PM"
src="12502962-2c13-4094-96dd-54e03e5d5d8d">
<img width="949" alt="Screenshot 2023-10-24 at 1 00 31 PM"
src="9d2853e9-7481-40e8-9ece-d17849d33e75">


### 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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2023-10-25 18:34:31 -05:00 committed by GitHub
parent e09e14f3b8
commit 444e080fe1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 19 deletions

View file

@ -79,7 +79,7 @@ describe('MaintenanceWindowCallout', () => {
{ wrapper: TestProviders }
);
expect(await findAllByText('Maintenance window is running')).toHaveLength(1);
expect(await findAllByText('One or more maintenance windows are running')).toHaveLength(1);
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
@ -94,7 +94,7 @@ describe('MaintenanceWindowCallout', () => {
{ wrapper: TestProviders }
);
expect(await findAllByText('Maintenance window is running')).toHaveLength(1);
expect(await findAllByText('One or more maintenance windows are running')).toHaveLength(1);
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
@ -106,7 +106,7 @@ describe('MaintenanceWindowCallout', () => {
{ wrapper: TestProviders }
);
expect(await findByText('Maintenance window is running')).toBeInTheDocument();
expect(await findByText('One or more maintenance windows are running')).toBeInTheDocument();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
@ -134,6 +134,85 @@ describe('MaintenanceWindowCallout', () => {
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
it('should be visible if there is a "running" maintenance window that matches the specified category', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
{
...RUNNING_MAINTENANCE_WINDOW_1,
categoryIds: ['observability'],
},
]);
const { findByText } = render(
<MaintenanceWindowCallout
kibanaServices={kibanaServicesMock}
categories={['observability']}
/>,
{ wrapper: TestProviders }
);
expect(
await findByText('A maintenance window is running for Observability rules')
).toBeInTheDocument();
});
it('should NOT be visible if there is a "running" maintenance window that does not match the specified category', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
{
...RUNNING_MAINTENANCE_WINDOW_1,
categoryIds: ['observability'],
},
]);
const { container } = render(
<MaintenanceWindowCallout
kibanaServices={kibanaServicesMock}
categories={['securitySolution']}
/>,
{ wrapper: TestProviders }
);
expect(container).toBeEmptyDOMElement();
});
it('should be visible if there is a "running" maintenance window with a category, and no categories are specified', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
{
...RUNNING_MAINTENANCE_WINDOW_1,
categoryIds: ['observability'],
},
]);
const { findByText } = render(
<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />,
{ wrapper: TestProviders }
);
expect(
await findByText('A maintenance window is running for Observability rules')
).toBeInTheDocument();
});
it('should only display the specified categories in the callout title for a maintenance window that matches muliple categories', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
{
...RUNNING_MAINTENANCE_WINDOW_1,
categoryIds: ['observability', 'securitySolution', 'management'],
},
]);
const { findByText } = render(
<MaintenanceWindowCallout
kibanaServices={kibanaServicesMock}
categories={['observability', 'management']}
/>,
{ wrapper: TestProviders }
);
expect(
await findByText('A maintenance window is running for Observability and Stack rules')
).toBeInTheDocument();
});
it('should see an error toast if there was an error while fetching maintenance windows', async () => {
const createReactQueryWrapper = () => {
const queryClient = new QueryClient({
@ -169,7 +248,7 @@ describe('MaintenanceWindowCallout', () => {
expect(kibanaServicesMock.notifications.toasts.addError).toHaveBeenCalledTimes(1);
expect(kibanaServicesMock.notifications.toasts.addError).toHaveBeenCalledWith(mockError, {
title: 'Failed to check if maintenance windows are active',
toastMessage: 'Rule notifications are stopped while the maintenance window is running.',
toastMessage: 'Rule notifications are stopped while maintenance windows are running.',
});
});
});
@ -195,7 +274,7 @@ describe('MaintenanceWindowCallout', () => {
expect(container).toBeEmptyDOMElement();
});
it('should work as expected if window maintenance privilege is READ ', async () => {
it('should work as expected if window maintenance privilege is READ', async () => {
const servicesMock = {
...kibanaServicesMock,
application: {
@ -213,7 +292,7 @@ describe('MaintenanceWindowCallout', () => {
wrapper: TestProviders,
});
expect(await findByText('Maintenance window is running')).toBeInTheDocument();
expect(await findByText('One or more maintenance windows are running')).toBeInTheDocument();
});
it('should display the callout if the category ids contains the specified category', async () => {

View file

@ -9,22 +9,44 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
import { MaintenanceWindowStatus, KibanaServices } from './types';
import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows';
const MAINTENANCE_WINDOW_FEATURE_ID = 'maintenanceWindow';
const MAINTENANCE_WINDOW_RUNNING = i18n.translate(
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActive',
{
defaultMessage: 'Maintenance window is running',
}
);
const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate(
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveDescription',
{
defaultMessage: 'Rule notifications are stopped while the maintenance window is running.',
defaultMessage: 'Rule notifications are stopped while maintenance windows are running.',
}
);
const MAINTENANCE_WINDOW_NO_CATEGORY_TITLE = i18n.translate(
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveNoCategories',
{
defaultMessage: 'One or more maintenance windows are running',
}
);
const maintenanceWindowTwoCategoryNames = (names: string[]) =>
i18n.translate('alertsUIShared.maintenanceWindowCallout.maintenanceWindowTwoCategoryNames', {
defaultMessage: '{first} and {second}',
values: { first: names[0], second: names[1] },
});
const maintenanceWindowMultipleCategoryNames = (names: string[]) =>
i18n.translate('alertsUIShared.maintenanceWindowCallout.maintenanceWindowMultipleCategoryNames', {
defaultMessage: '{commaSeparatedList}, and {last}', // Oxford comma, e.g. "First, second, and third"
values: { commaSeparatedList: names.slice(0, -1).join(', '), last: names.slice(-1).join('') },
});
const APP_CATEGORIES = {
...DEFAULT_APP_CATEGORIES,
management: {
...DEFAULT_APP_CATEGORIES.management,
label: i18n.translate('alertsUIShared.maintenanceWindowCallout.managementCategoryLabel', {
defaultMessage: 'Stack',
}),
},
};
export function MaintenanceWindowCallout({
kibanaServices,
@ -51,6 +73,8 @@ export function MaintenanceWindowCallout({
if (activeMaintenanceWindows.length === 0) {
return false;
}
// If categories is omitted, always display the callout
if (!Array.isArray(categories)) {
return true;
}
@ -66,6 +90,32 @@ export function MaintenanceWindowCallout({
});
}, [categories, activeMaintenanceWindows]);
const categoryNames = useMemo(() => {
const activeCategoryIds = Array.from(
new Set(
activeMaintenanceWindows
.map(({ categoryIds }) =>
// If the categories array is provided, only display category names that are included in it
categoryIds?.filter((categoryId) => !categories || categories.includes(categoryId))
)
.flat()
)
);
const activeCategories = activeCategoryIds
.map(
(categoryId) =>
Object.values(APP_CATEGORIES).find((c) => c.id === categoryId)?.label ?? categoryId
)
.filter(Boolean) as string[];
return activeCategories.length === 0
? null
: activeCategories.length === 1
? `${activeCategories}`
: activeCategories.length === 2
? maintenanceWindowTwoCategoryNames(activeCategories)
: maintenanceWindowMultipleCategoryNames(activeCategories);
}, [activeMaintenanceWindows, categories]);
if (isMaintenanceWindowDisabled) {
return null;
}
@ -76,7 +126,18 @@ export function MaintenanceWindowCallout({
return (
<EuiCallOut
title={MAINTENANCE_WINDOW_RUNNING}
title={
!categoryNames
? MAINTENANCE_WINDOW_NO_CATEGORY_TITLE
: i18n.translate('alertsUIShared.maintenanceWindowCallout.maintenanceWindowActive', {
defaultMessage:
'{activeWindowCount, plural, one {A maintenance window is} other {Maintenance windows are}} running for {categories} rules',
values: {
categories: categoryNames,
activeWindowCount: activeMaintenanceWindows.length,
},
})
}
color="warning"
iconType="iInCircle"
data-test-subj="maintenanceWindowCallout"

View file

@ -37,6 +37,6 @@ const FETCH_ERROR = i18n.translate('alertsUIShared.maintenanceWindowCallout.fetc
const FETCH_ERROR_DESCRIPTION = i18n.translate(
'alertsUIShared.maintenanceWindowCallout.fetchErrorDescription',
{
defaultMessage: 'Rule notifications are stopped while the maintenance window is running.',
defaultMessage: 'Rule notifications are stopped while maintenance windows are running.',
}
);

View file

@ -22,6 +22,7 @@
"@kbn/i18n-react",
"@kbn/alerting-plugin",
"@kbn/rrule",
"@kbn/actions-plugin"
"@kbn/actions-plugin",
"@kbn/core-application-common"
]
}

View file

@ -271,7 +271,22 @@ describe('rules_list component empty', () => {
it('renders MaintenanceWindowCallout if one exists', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
renderWithProviders(<RulesList />);
expect(await screen.findByText('Maintenance window is running')).toBeInTheDocument();
expect(
await screen.findByText(
'Rule notifications are stopped while maintenance windows are running.'
)
).toBeInTheDocument();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
it("hides MaintenanceWindowCallout if filterConsumers does not match the running maintenance window's category", async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
{ ...RUNNING_MAINTENANCE_WINDOW_1, categoryIds: ['securitySolution'] },
]);
renderWithProviders(<RulesList filterConsumers={['observability']} />);
await expect(
screen.findByText('Rule notifications are stopped while maintenance windows are running.')
).rejects.toThrow();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});

View file

@ -749,7 +749,7 @@ export const RulesList = ({
{showSearchBar && !isEmpty(filters.ruleParams) ? (
<RulesListClearRuleFilterBanner onClickClearFilter={handleClearRuleParamFilter} />
) : null}
<MaintenanceWindowCallout kibanaServices={kibanaServices} />
<MaintenanceWindowCallout kibanaServices={kibanaServices} categories={filterConsumers} />
<RulesListPrompts
showNoAuthPrompt={showNoAuthPrompt}
showCreateFirstRulePrompt={showCreateFirstRulePrompt}

View file

@ -68,7 +68,7 @@ describe(
it('Displays the callout when there are running maintenance windows', () => {
visit(RULES_MANAGEMENT_URL);
cy.contains('Maintenance window is running');
cy.contains('A maintenance window is running for Security rules');
});
}
);