[8.18] [ResponseOps][MW] Use date format from settings in MW UI (#211576) (#214543)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[ResponseOps][MW] Use date format from settings in MW UI
(#211576)](https://github.com/elastic/kibana/pull/211576)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT
[{"author":{"name":"Antonio","email":"antonio.coelho@elastic.co"},"sourceCommit":{"committedDate":"2025-03-07T12:45:48Z","message":"[ResponseOps][MW]
Use date format from settings in MW UI (#211576)\n\nCloses #199315\n\n##
Summary\n\nThis PR changes the Maintenance Window UI to respect the date
format\nconfigured in Kibana's advanced settings.\n\n3 places needed
changing:\n- Maintenance window list.\n- Maintenance window creation
page.\n- Event popover in the maintenance window list(for recurring
MWs).","sha":"2ead636ebd3d8dc911dd870111c8e016035169c1","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","backport:version","v9.1.0","v8.19.0"],"title":"[ResponseOps][MW]
Use date format from settings in MW
UI","number":211576,"url":"https://github.com/elastic/kibana/pull/211576","mergeCommit":{"message":"[ResponseOps][MW]
Use date format from settings in MW UI (#211576)\n\nCloses #199315\n\n##
Summary\n\nThis PR changes the Maintenance Window UI to respect the date
format\nconfigured in Kibana's advanced settings.\n\n3 places needed
changing:\n- Maintenance window list.\n- Maintenance window creation
page.\n- Event popover in the maintenance window list(for recurring
MWs).","sha":"2ead636ebd3d8dc911dd870111c8e016035169c1"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/211576","number":211576,"mergeCommit":{"message":"[ResponseOps][MW]
Use date format from settings in MW UI (#211576)\n\nCloses #199315\n\n##
Summary\n\nThis PR changes the Maintenance Window UI to respect the date
format\nconfigured in Kibana's advanced settings.\n\n3 places needed
changing:\n- Maintenance window list.\n- Maintenance window creation
page.\n- Event popover in the maintenance window list(for recurring
MWs).","sha":"2ead636ebd3d8dc911dd870111c8e016035169c1"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/214438","number":214438,"state":"MERGED","mergeCommit":{"sha":"e74dfe03009087cf65fce16bf43c4053120aff46","message":"[8.x]
[ResponseOps][MW] Use date format from settings in MW UI (#211576)
(#214438)\n\n# Backport\n\nThis will backport the following commits from
`main` to `8.x`:\n- [[ResponseOps][MW] Use date format from settings in
MW
UI\n(#211576)](https://github.com/elastic/kibana/pull/211576)\n\n<!---
Backport version: 9.6.6 -->\n\n### Questions ?\nPlease refer to the
[Backport
tool\ndocumentation](https://github.com/sorenlouv/backport)\n\n<!--BACKPORT\n[{\"author\":{\"name\":\"Antonio\",\"email\":\"antonio.coelho@elastic.co\"},\"sourceCommit\":{\"committedDate\":\"2025-03-07T12:45:48Z\",\"message\":\"[ResponseOps][MW]\nUse
date format from settings in MW UI (#211576)\\n\\nCloses
#199315\\n\\n##\nSummary\\n\\nThis PR changes the Maintenance Window UI
to respect the date\nformat\\nconfigured in Kibana's advanced
settings.\\n\\n3 places needed\nchanging:\\n- Maintenance window
list.\\n- Maintenance window creation\npage.\\n- Event popover in the
maintenance window list(for
recurring\nMWs).\",\"sha\":\"2ead636ebd3d8dc911dd870111c8e016035169c1\",\"branchLabelMapping\":{\"^v9.1.0$\":\"main\",\"^v8.19.0$\":\"8.x\",\"^v(\\\\d+).(\\\\d+).\\\\d+$\":\"$1.$2\"}},\"sourcePullRequest\":{\"labels\":[\"release_note:skip\",\"Team:ResponseOps\",\"backport\nmissing\",\"backport:version\",\"v9.1.0\",\"v8.19.0\"],\"title\":\"[ResponseOps][MW]\nUse
date format from settings in
MW\nUI\",\"number\":211576,\"url\":\"https://github.com/elastic/kibana/pull/211576\",\"mergeCommit\":{\"message\":\"[ResponseOps][MW]\nUse
date format from settings in MW UI (#211576)\\n\\nCloses
#199315\\n\\n##\nSummary\\n\\nThis PR changes the Maintenance Window UI
to respect the date\nformat\\nconfigured in Kibana's advanced
settings.\\n\\n3 places needed\nchanging:\\n- Maintenance window
list.\\n- Maintenance window creation\npage.\\n- Event popover in the
maintenance window list(for
recurring\nMWs).\",\"sha\":\"2ead636ebd3d8dc911dd870111c8e016035169c1\"}},\"sourceBranch\":\"main\",\"suggestedTargetBranches\":[\"8.x\"],\"targetPullRequestStates\":[{\"branch\":\"main\",\"label\":\"v9.1.0\",\"branchLabelMappingKey\":\"^v9.1.0$\",\"isSourceBranch\":true,\"state\":\"MERGED\",\"url\":\"https://github.com/elastic/kibana/pull/211576\",\"number\":211576,\"mergeCommit\":{\"message\":\"[ResponseOps][MW]\nUse
date format from settings in MW UI (#211576)\\n\\nCloses
#199315\\n\\n##\nSummary\\n\\nThis PR changes the Maintenance Window UI
to respect the date\nformat\\nconfigured in Kibana's advanced
settings.\\n\\n3 places needed\nchanging:\\n- Maintenance window
list.\\n- Maintenance window creation\npage.\\n- Event popover in the
maintenance window list(for
recurring\nMWs).\",\"sha\":\"2ead636ebd3d8dc911dd870111c8e016035169c1\"}},{\"branch\":\"8.x\",\"label\":\"v8.19.0\",\"branchLabelMappingKey\":\"^v8.19.0$\",\"isSourceBranch\":false,\"state\":\"NOT_CREATED\"}]}]\nBACKPORT-->\n\n---------\n\nCo-authored-by:
kibanamachine <42973632+kibanamachine@users.noreply.github.com>"}}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Antonio 2025-03-14 14:09:28 +01:00 committed by GitHub
parent fe127ae72e
commit 09fec93596
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 172 additions and 40 deletions

View file

@ -111,6 +111,11 @@ describe('CreateMaintenanceWindowForm', () => {
});
it('should prefill the form when provided with initialValue', async () => {
useUiSetting.mockImplementation((key: string) => {
if (key === 'dateFormat') return 'YYYY.MM.DD, h:mm:ss';
return 'America/Los_Angeles';
});
const result = appMockRenderer.render(
<CreateMaintenanceWindowForm
{...formProps}
@ -155,8 +160,8 @@ describe('CreateMaintenanceWindowForm', () => {
expect(securityInput).toBeChecked();
expect(managementInput).toBeChecked();
expect(titleInput).toHaveValue('test');
expect(dateInputs[0]).toHaveValue('03/23/2023 09:00 PM');
expect(dateInputs[1]).toHaveValue('03/25/2023 09:00 PM');
expect(dateInputs[0]).toHaveValue('2023.03.23, 9:00:00');
expect(dateInputs[1]).toHaveValue('2023.03.25, 9:00:00');
expect(recurringInput).toBeChecked();
expect(timezoneInput).toHaveValue('America/Los_Angeles');
});

View file

@ -0,0 +1,80 @@
/*
* 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, { FC, PropsWithChildren } from 'react';
import moment from 'moment';
import { screen } from '@testing-library/react';
import { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { AppMockRenderer, createAppMockRenderer } from '../../../../lib/test_utils';
import type { FormProps } from '../schema';
import { DatePickerRangeField } from './date_picker_range_field';
jest.mock('../../../../utils/kibana_react');
jest.mock('../../helpers/get_selected_for_date_picker');
const { useUiSetting } = jest.requireMock('../../../../utils/kibana_react');
const { getSelectedForDatePicker } = jest.requireMock('../../helpers/get_selected_for_date_picker');
describe('DatePickerRangeField', () => {
let appMockRenderer: AppMockRenderer;
const MockHookWrapperComponent: FC<PropsWithChildren<unknown>> = ({ children }) => {
const { form } = useForm<FormProps>({
defaultValue: {},
schema: {
startDate: {},
endDate: {},
},
onSubmit: jest.fn(),
});
return <Form form={form}>{children}</Form>;
};
beforeEach(() => {
jest.clearAllMocks();
appMockRenderer = createAppMockRenderer();
});
test('it renders the dates in the settings format', async () => {
// ISO format
const startDate = '2023-03-24T07:28:00.000Z';
const endDate = '2023-03-26T07:31:00.000Z';
// some random format
useUiSetting.mockReturnValue('YYYY.MM.DD, h:mm:ss');
getSelectedForDatePicker
.mockReturnValueOnce({ selected: moment(startDate), utcOffset: 0 })
.mockReturnValueOnce({ selected: moment(endDate), utcOffset: 0 });
const fields = {
startDate: {
value: startDate,
label: 'startDate',
path: 'startDate',
} as FieldHook<string, string>,
endDate: {
value: endDate,
label: 'endDate',
path: 'endDate',
} as FieldHook<string, string>,
};
appMockRenderer.render(
<MockHookWrapperComponent>
<DatePickerRangeField fields={fields} data-test-subj={'datePicker'} />
</MockHookWrapperComponent>
);
expect(await screen.findByTestId('datePicker')).toBeInTheDocument();
expect(await screen.findByDisplayValue('2023.03.24, 3:28:00')).toBeInTheDocument();
expect(await screen.findByDisplayValue('2023.03.26, 3:31:00')).toBeInTheDocument();
});
});

View file

@ -13,8 +13,12 @@ import {
useFormContext,
FieldHook,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import * as i18n from '../../translations';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { MAINTENANCE_WINDOW_DATE_FORMAT } from '../../../../../common';
import { useUiSetting } from '../../../../utils/kibana_react';
import { getSelectedForDatePicker as getSelected } from '../../helpers/get_selected_for_date_picker';
import * as i18n from '../../translations';
interface DatePickerRangeFieldProps {
fields: { startDate: FieldHook<string, string>; endDate: FieldHook<string, string> };
@ -26,6 +30,10 @@ interface DatePickerRangeFieldProps {
export const DatePickerRangeField: React.FC<DatePickerRangeFieldProps> = React.memo(
({ fields, timezone, showTimeSelect = true, ...rest }) => {
const [today] = useState<Moment>(moment());
const systemDateFormat = useUiSetting<string>(
UI_SETTINGS.DATE_FORMAT,
MAINTENANCE_WINDOW_DATE_FORMAT
);
const { setFieldValue } = useFormContext();
const [form] = useFormData({ watch: [fields.startDate.path, fields.endDate.path] });
@ -62,6 +70,7 @@ export const DatePickerRangeField: React.FC<DatePickerRangeFieldProps> = React.m
},
[setFieldValue, fields.endDate.path]
);
const isInvalid = startDate.isAfter(endDate);
return (
@ -79,6 +88,7 @@ export const DatePickerRangeField: React.FC<DatePickerRangeFieldProps> = React.m
showTimeSelect={showTimeSelect}
minDate={today}
utcOffset={startOffset}
dateFormat={systemDateFormat}
/>
}
endDateControl={
@ -91,6 +101,7 @@ export const DatePickerRangeField: React.FC<DatePickerRangeFieldProps> = React.m
showTimeSelect={showTimeSelect}
minDate={today}
utcOffset={endOffset}
dateFormat={systemDateFormat}
/>
}
fullWidth

View file

@ -7,14 +7,23 @@
import React from 'react';
import moment from 'moment';
import { fireEvent, waitFor } from '@testing-library/react';
import { fireEvent, waitFor, screen } from '@testing-library/react';
import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
import { MaintenanceWindowsList } from './maintenance_windows_list';
import { MaintenanceWindowStatus, MaintenanceWindow } from '../../../../common';
jest.mock('../../../utils/kibana_react', () => {
const originalModule = jest.requireActual('../../../utils/kibana_react');
return {
...originalModule,
// mocks the date format in settings
useUiSetting: () => 'YYYY/MM/DD',
};
});
describe('MaintenanceWindowsList', () => {
const date = moment('2023-04-05').toISOString();
const endDate = moment('2023-04-05').add(1, 'month').toISOString();
const date = moment('2023-04-21').toISOString();
const endDate = moment('2023-04-21').add(1, 'month').toISOString();
const items: MaintenanceWindow[] = [
{
id: '1',
@ -89,7 +98,7 @@ describe('MaintenanceWindowsList', () => {
});
test('it renders', () => {
const result = appMockRenderer.render(
appMockRenderer.render(
<MaintenanceWindowsList
refreshData={() => {}}
isLoading={false}
@ -105,31 +114,31 @@ describe('MaintenanceWindowsList', () => {
/>
);
expect(result.getAllByTestId('list-item')).toHaveLength(items.length);
expect(screen.getAllByTestId('list-item')).toHaveLength(items.length);
// check the title
expect(result.getAllByText('Host maintenance')).toHaveLength(1);
expect(result.getAllByText('Server outage west coast')).toHaveLength(1);
expect(result.getAllByText('Monthly maintenance window')).toHaveLength(2);
expect(screen.getAllByText('Host maintenance')).toHaveLength(1);
expect(screen.getAllByText('Server outage west coast')).toHaveLength(1);
expect(screen.getAllByText('Monthly maintenance window')).toHaveLength(2);
// check the status
expect(result.getAllByText('Running')).toHaveLength(1);
expect(result.getAllByText('Upcoming')).toHaveLength(1);
expect(result.getAllByText('Finished')).toHaveLength(1);
expect(result.getAllByText('Archived')).toHaveLength(1);
expect(screen.getAllByText('Running')).toHaveLength(1);
expect(screen.getAllByText('Upcoming')).toHaveLength(1);
expect(screen.getAllByText('Finished')).toHaveLength(1);
expect(screen.getAllByText('Archived')).toHaveLength(1);
// check the startDate formatting
expect(result.getAllByText('04/05/23 12:00 AM')).toHaveLength(4);
expect(screen.getAllByText('2023/04/21')).toHaveLength(4);
// check the endDate formatting
expect(result.getAllByText('05/05/23 12:00 AM')).toHaveLength(4);
expect(screen.getAllByText('2023/05/21')).toHaveLength(4);
// check if action menu is there
expect(result.getAllByTestId('table-actions-icon-button')).toHaveLength(items.length);
expect(screen.getAllByTestId('table-actions-icon-button')).toHaveLength(items.length);
});
test('it does NOT render action column in readonly', () => {
const result = appMockRenderer.render(
appMockRenderer.render(
<MaintenanceWindowsList
refreshData={() => {}}
isLoading={false}
@ -145,15 +154,15 @@ describe('MaintenanceWindowsList', () => {
/>
);
expect(result.getAllByTestId('list-item')).toHaveLength(items.length);
expect(screen.getAllByTestId('list-item')).toHaveLength(items.length);
// check if action menu is there
expect(result.queryByTestId('table-actions-icon-button')).not.toBeInTheDocument();
expect(screen.queryByTestId('table-actions-icon-button')).not.toBeInTheDocument();
});
test('it calls refreshData when user presses refresh button', async () => {
const refreshData = jest.fn();
const result = appMockRenderer.render(
appMockRenderer.render(
<MaintenanceWindowsList
refreshData={refreshData}
isLoading={false}
@ -168,7 +177,7 @@ describe('MaintenanceWindowsList', () => {
onSearchChange={() => {}}
/>
);
fireEvent.click(result.getByTestId('refresh-button'));
fireEvent.click(screen.getByTestId('refresh-button'));
await waitFor(() => expect(refreshData).toBeCalled());
});
});

View file

@ -19,6 +19,8 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import * as i18n from '../translations';
import { useEditMaintenanceWindowsNavigation } from '../../../hooks/use_navigation';
import { STATUS_DISPLAY, STATUS_SORT } from '../constants';
@ -33,6 +35,7 @@ import { TableActionsPopover, TableActionsPopoverProps } from './table_actions_p
import { useFinishMaintenanceWindow } from '../../../hooks/use_finish_maintenance_window';
import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window';
import { useFinishAndArchiveMaintenanceWindow } from '../../../hooks/use_finish_and_archive_maintenance_window';
import { useUiSetting } from '../../../utils/kibana_react';
interface MaintenanceWindowsListProps {
isLoading: boolean;
@ -48,7 +51,11 @@ interface MaintenanceWindowsListProps {
onSearchChange: (value: string) => void;
}
const COLUMNS: Array<EuiBasicTableColumn<MaintenanceWindow>> = [
const getColumns = ({
dateFormat,
}: {
dateFormat: string;
}): Array<EuiBasicTableColumn<MaintenanceWindow>> => [
{
field: 'title',
name: i18n.NAME,
@ -72,9 +79,7 @@ const COLUMNS: Array<EuiBasicTableColumn<MaintenanceWindow>> = [
render: (startDate: string, item: MaintenanceWindow) => {
return (
<EuiFlexGroup responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
{formatDate(startDate, MAINTENANCE_WINDOW_DATE_FORMAT)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{formatDate(startDate, dateFormat)}</EuiFlexItem>
{item.events.length > 1 ? (
<EuiFlexItem grow={false}>
<UpcomingEventsPopover maintenanceWindowFindResponse={item} />
@ -89,7 +94,7 @@ const COLUMNS: Array<EuiBasicTableColumn<MaintenanceWindow>> = [
field: 'eventEndTime',
name: i18n.TABLE_END_TIME,
dataType: 'date',
render: (endDate: string) => formatDate(endDate, MAINTENANCE_WINDOW_DATE_FORMAT),
render: (endDate: string) => formatDate(endDate, dateFormat),
},
];
@ -114,6 +119,10 @@ export const MaintenanceWindowsList = React.memo<MaintenanceWindowsListProps>(
}) => {
const { euiTheme } = useEuiTheme();
const [search, setSearch] = useState<string>('');
const systemDateFormat = useUiSetting<string>(
UI_SETTINGS.DATE_FORMAT,
MAINTENANCE_WINDOW_DATE_FORMAT
);
const { navigateToEditMaintenanceWindows } = useEditMaintenanceWindowsNavigation();
const onEdit = useCallback<TableActionsPopoverProps['onEdit']>(
@ -181,10 +190,10 @@ export const MaintenanceWindowsList = React.memo<MaintenanceWindowsListProps>(
[isMutatingOrLoading, onArchive, onCancel, onCancelAndArchive, onEdit]
);
const columns = useMemo(
() => (readOnly ? COLUMNS : COLUMNS.concat(actions)),
[actions, readOnly]
);
const columns = useMemo(() => {
const result = getColumns({ dateFormat: systemDateFormat });
return readOnly ? result : result.concat(actions);
}, [actions, readOnly, systemDateFormat]);
const onInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {

View file

@ -4,12 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { fireEvent } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import * as React from 'react';
import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
import { UpcomingEventsPopover } from './upcoming_events_popover';
import { MaintenanceWindowStatus } from '../../../../common';
jest.mock('../../../utils/kibana_react');
const { useUiSetting } = jest.requireMock('../../../utils/kibana_react');
useUiSetting.mockReturnValue('YYYY.MM.DD, h:mm:ss');
describe('rule_actions_popover', () => {
let appMockRenderer: AppMockRenderer;
@ -18,8 +24,8 @@ describe('rule_actions_popover', () => {
appMockRenderer = createAppMockRenderer();
});
it('renders the top 3 events', () => {
const result = appMockRenderer.render(
it('renders the top 3 events', async () => {
appMockRenderer.render(
<UpcomingEventsPopover
maintenanceWindowFindResponse={{
title: 'test MW',
@ -53,14 +59,20 @@ describe('rule_actions_popover', () => {
/>
);
const popoverButton = result.getByTestId('upcoming-events-icon-button');
const popoverButton = await screen.findByTestId('upcoming-events-icon-button');
expect(popoverButton).toBeInTheDocument();
fireEvent.click(popoverButton);
expect(result.getByTestId('upcoming-events-popover-title')).toBeInTheDocument();
expect(result.getByTestId('upcoming-events-popover-title')).toHaveTextContent(
expect(await screen.findByTestId('upcoming-events-popover-title')).toBeInTheDocument();
expect(await screen.findByTestId('upcoming-events-popover-title')).toHaveTextContent(
'Repeats every Friday'
);
expect(result.getAllByTestId('upcoming-events-popover-item').length).toBe(3);
// same format as in settings
expect(await screen.findByText('2023.04.28, 10:58:40')).toBeInTheDocument();
expect(await screen.findByText('2023.05.05, 10:58:40')).toBeInTheDocument();
expect(await screen.findByText('2023.05.12, 10:58:40')).toBeInTheDocument();
expect(screen.getAllByTestId('upcoming-events-popover-item').length).toBe(3);
});
});

View file

@ -20,8 +20,10 @@ import {
EuiText,
formatDate,
} from '@elastic/eui';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import * as i18n from '../translations';
import { MAINTENANCE_WINDOW_DATE_FORMAT, MaintenanceWindow } from '../../../../common';
import { useUiSetting } from '../../../utils/kibana_react';
import { recurringSummary } from '../helpers/recurring_summary';
import { getPresets } from '../helpers/get_presets';
import { convertFromMaintenanceWindowToForm } from '../helpers/convert_from_maintenance_window_to_form';
@ -32,6 +34,10 @@ interface UpcomingEventsPopoverProps {
export const UpcomingEventsPopover: React.FC<UpcomingEventsPopoverProps> = React.memo(
({ maintenanceWindowFindResponse }) => {
const systemDateFormat = useUiSetting<string>(
UI_SETTINGS.DATE_FORMAT,
MAINTENANCE_WINDOW_DATE_FORMAT
);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = useCallback(() => {
@ -107,7 +113,7 @@ export const UpcomingEventsPopover: React.FC<UpcomingEventsPopoverProps> = React
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
{formatDate(event.gte, MAINTENANCE_WINDOW_DATE_FORMAT)}
{formatDate(event.gte, systemDateFormat)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>