mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ResponseOps][Window Maintenance] Add the stop and archive actions to the maintenance window table (#155201)
Resolves https://github.com/elastic/kibana/issues/154814 ## Summary This pr adds the cancel/archive actions to the Maintenance Windows table, and adds an archive callout to the create form. @lcawl I think I need your help with some of the text in the modals 🙂 **Create Form:** Callout <img width="1370" alt="Screen Shot 2023-04-18 at 9 49 58 PM" src="https://user-images.githubusercontent.com/109488926/232945833-77f01988-e5c4-4d5f-a2f9-b361085964e2.png"> Modal <img width="1335" alt="Screen Shot 2023-04-18 at 9 50 05 PM" src="https://user-images.githubusercontent.com/109488926/232945810-dacb3a26-fa59-4e5e-995c-14b7f0977c10.png"> **Cancel Modal:** <img width="628" alt="Screen Shot 2023-04-18 at 9 52 18 PM" src="https://user-images.githubusercontent.com/109488926/232946092-c4001ccc-d2f9-475a-bc67-3b2ab6fdf31e.png"> **Cancel and Archive Modal:** <img width="621" alt="Screen Shot 2023-04-18 at 9 52 56 PM" src="https://user-images.githubusercontent.com/109488926/232946171-154f8601-d5aa-40f1-824a-bff25b4d3d91.png"> **Archive Modal:** <img width="611" alt="Screen Shot 2023-04-18 at 9 53 25 PM" src="https://user-images.githubusercontent.com/109488926/232946240-ee25bcd0-80d6-403c-9fb3-9aec89d11ebd.png"> **Unarchive Modal:** <img width="620" alt="Screen Shot 2023-04-18 at 9 54 16 PM" src="https://user-images.githubusercontent.com/109488926/232946350-814d0c05-5511-4fad-8ac4-0c5824d94b8e.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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Lisa Cawley <lcawley@elastic.co>
This commit is contained in:
parent
da5a4b08d3
commit
dbb8e2ebec
22 changed files with 1186 additions and 59 deletions
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks/dom';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
|
||||
import { MaintenanceWindow } from '../pages/maintenance_windows/types';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils';
|
||||
import { useArchiveMaintenanceWindow } from './use_archive_maintenance_window';
|
||||
|
||||
const mockAddDanger = jest.fn();
|
||||
const mockAddSuccess = jest.fn();
|
||||
|
||||
jest.mock('../utils/kibana_react', () => {
|
||||
const originalModule = jest.requireActual('../utils/kibana_react');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => {
|
||||
const { services } = originalModule.useKibana();
|
||||
return {
|
||||
services: {
|
||||
...services,
|
||||
notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../services/maintenance_windows_api/archive', () => ({
|
||||
archiveMaintenanceWindow: jest.fn(),
|
||||
}));
|
||||
|
||||
const { archiveMaintenanceWindow } = jest.requireMock(
|
||||
'../services/maintenance_windows_api/archive'
|
||||
);
|
||||
|
||||
const maintenanceWindow: MaintenanceWindow = {
|
||||
title: 'archive',
|
||||
duration: 1,
|
||||
rRule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
},
|
||||
};
|
||||
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
describe('useArchiveMaintenanceWindow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
archiveMaintenanceWindow.mockResolvedValue(maintenanceWindow);
|
||||
});
|
||||
|
||||
it('should call onSuccess if api succeeds', async () => {
|
||||
const { result } = renderHook(() => useArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate({ maintenanceWindowId: '123', archive: true });
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockAddSuccess).toBeCalledWith("Archived maintenance window 'archive'")
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError if api fails', async () => {
|
||||
archiveMaintenanceWindow.mockRejectedValue('');
|
||||
|
||||
const { result } = renderHook(() => useArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate({ maintenanceWindowId: '123', archive: true });
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockAddDanger).toBeCalledWith('Failed to archive maintenance window.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSuccess if api succeeds (unarchive)', async () => {
|
||||
const { result } = renderHook(() => useArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate({ maintenanceWindowId: '123', archive: false });
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockAddSuccess).toBeCalledWith("Unarchived maintenance window 'archive'")
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError if api fails (unarchive)', async () => {
|
||||
archiveMaintenanceWindow.mockRejectedValue('');
|
||||
|
||||
const { result } = renderHook(() => useArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate({ maintenanceWindowId: '123', archive: false });
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockAddDanger).toBeCalledWith('Failed to unarchive maintenance window.')
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { archiveMaintenanceWindow } from '../services/maintenance_windows_api/archive';
|
||||
|
||||
export function useArchiveMaintenanceWindow() {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const mutationFn = ({
|
||||
maintenanceWindowId,
|
||||
archive,
|
||||
}: {
|
||||
maintenanceWindowId: string;
|
||||
archive: boolean;
|
||||
}) => {
|
||||
return archiveMaintenanceWindow({ http, maintenanceWindowId, archive });
|
||||
};
|
||||
|
||||
return useMutation(mutationFn, {
|
||||
onSuccess: (data, { archive }) => {
|
||||
const archiveToast = i18n.translate('xpack.alerting.maintenanceWindowsArchiveSuccess', {
|
||||
defaultMessage: "Archived maintenance window '{title}'",
|
||||
values: {
|
||||
title: data.title,
|
||||
},
|
||||
});
|
||||
const unarchiveToast = i18n.translate('xpack.alerting.maintenanceWindowsUnarchiveSuccess', {
|
||||
defaultMessage: "Unarchived maintenance window '{title}'",
|
||||
values: {
|
||||
title: data.title,
|
||||
},
|
||||
});
|
||||
toasts.addSuccess(archive ? archiveToast : unarchiveToast);
|
||||
},
|
||||
onError: (error, { archive }) => {
|
||||
const archiveToast = i18n.translate('xpack.alerting.maintenanceWindowsArchiveFailure', {
|
||||
defaultMessage: 'Failed to archive maintenance window.',
|
||||
});
|
||||
const unarchiveToast = i18n.translate('xpack.alerting.maintenanceWindowsUnarchiveFailure', {
|
||||
defaultMessage: 'Failed to unarchive maintenance window.',
|
||||
});
|
||||
toasts.addDanger(archive ? archiveToast : unarchiveToast);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -23,12 +23,12 @@ export function useCreateMaintenanceWindow() {
|
|||
};
|
||||
|
||||
return useMutation(mutationFn, {
|
||||
onSuccess: (variables: MaintenanceWindow) => {
|
||||
onSuccess: (data) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsCreateSuccess', {
|
||||
defaultMessage: "Created maintenance window '{title}'",
|
||||
values: {
|
||||
title: variables.title,
|
||||
title: data.title,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -30,7 +30,11 @@ export const useFindMaintenanceWindows = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const { isLoading, data = [] } = useQuery({
|
||||
const {
|
||||
isLoading,
|
||||
data = [],
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['findMaintenanceWindows'],
|
||||
queryFn,
|
||||
onError: onErrorFn,
|
||||
|
@ -42,5 +46,6 @@ export const useFindMaintenanceWindows = () => {
|
|||
return {
|
||||
maintenanceWindows: data,
|
||||
isLoading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks/dom';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
|
||||
import { MaintenanceWindow } from '../pages/maintenance_windows/types';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils';
|
||||
import { useFinishAndArchiveMaintenanceWindow } from './use_finish_and_archive_maintenance_window';
|
||||
|
||||
const mockAddDanger = jest.fn();
|
||||
const mockAddSuccess = jest.fn();
|
||||
|
||||
jest.mock('../utils/kibana_react', () => {
|
||||
const originalModule = jest.requireActual('../utils/kibana_react');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => {
|
||||
const { services } = originalModule.useKibana();
|
||||
return {
|
||||
services: {
|
||||
...services,
|
||||
notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../services/maintenance_windows_api/finish', () => ({
|
||||
finishMaintenanceWindow: jest.fn(),
|
||||
}));
|
||||
jest.mock('../services/maintenance_windows_api/archive', () => ({
|
||||
archiveMaintenanceWindow: jest.fn(),
|
||||
}));
|
||||
|
||||
const { finishMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/finish');
|
||||
const { archiveMaintenanceWindow } = jest.requireMock(
|
||||
'../services/maintenance_windows_api/archive'
|
||||
);
|
||||
|
||||
const maintenanceWindow: MaintenanceWindow = {
|
||||
title: 'test',
|
||||
duration: 1,
|
||||
rRule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
},
|
||||
};
|
||||
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
describe('useFinishAndArchiveMaintenanceWindow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
finishMaintenanceWindow.mockResolvedValue(maintenanceWindow);
|
||||
archiveMaintenanceWindow.mockResolvedValue(maintenanceWindow);
|
||||
});
|
||||
|
||||
it('should call onSuccess if api succeeds', async () => {
|
||||
const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate('123');
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockAddSuccess).toBeCalledWith(
|
||||
"Cancelled and archived running maintenance window 'test'"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError if finish api fails', async () => {
|
||||
finishMaintenanceWindow.mockRejectedValue('');
|
||||
|
||||
const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate('123');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockAddDanger).toBeCalledWith('Failed to cancel and archive maintenance window.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError if archive api fails', async () => {
|
||||
archiveMaintenanceWindow.mockRejectedValue('');
|
||||
|
||||
const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate('123');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockAddDanger).toBeCalledWith('Failed to cancel and archive maintenance window.')
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { finishMaintenanceWindow } from '../services/maintenance_windows_api/finish';
|
||||
import { archiveMaintenanceWindow } from '../services/maintenance_windows_api/archive';
|
||||
|
||||
export function useFinishAndArchiveMaintenanceWindow() {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const mutationFn = async (maintenanceWindowId: string) => {
|
||||
await finishMaintenanceWindow({ http, maintenanceWindowId });
|
||||
return archiveMaintenanceWindow({ http, maintenanceWindowId, archive: true });
|
||||
};
|
||||
|
||||
return useMutation(mutationFn, {
|
||||
onSuccess: (data) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsFinishedAndArchiveSuccess', {
|
||||
defaultMessage: "Cancelled and archived running maintenance window '{title}'",
|
||||
values: {
|
||||
title: data.title,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsFinishedAndArchiveFailure', {
|
||||
defaultMessage: 'Failed to cancel and archive maintenance window.',
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks/dom';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
|
||||
import { MaintenanceWindow } from '../pages/maintenance_windows/types';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils';
|
||||
import { useFinishMaintenanceWindow } from './use_finish_maintenance_window';
|
||||
|
||||
const mockAddDanger = jest.fn();
|
||||
const mockAddSuccess = jest.fn();
|
||||
|
||||
jest.mock('../utils/kibana_react', () => {
|
||||
const originalModule = jest.requireActual('../utils/kibana_react');
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => {
|
||||
const { services } = originalModule.useKibana();
|
||||
return {
|
||||
services: {
|
||||
...services,
|
||||
notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../services/maintenance_windows_api/finish', () => ({
|
||||
finishMaintenanceWindow: jest.fn(),
|
||||
}));
|
||||
|
||||
const { finishMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/finish');
|
||||
|
||||
const maintenanceWindow: MaintenanceWindow = {
|
||||
title: 'cancel',
|
||||
duration: 1,
|
||||
rRule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
},
|
||||
};
|
||||
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
describe('useFinishMaintenanceWindow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
finishMaintenanceWindow.mockResolvedValue(maintenanceWindow);
|
||||
});
|
||||
|
||||
it('should call onSuccess if api succeeds', async () => {
|
||||
const { result } = renderHook(() => useFinishMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate('123');
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockAddSuccess).toBeCalledWith("Cancelled running maintenance window 'cancel'")
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError if api fails', async () => {
|
||||
finishMaintenanceWindow.mockRejectedValue('');
|
||||
|
||||
const { result } = renderHook(() => useFinishMaintenanceWindow(), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutate('123');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockAddDanger).toBeCalledWith('Failed to cancel maintenance window.')
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { finishMaintenanceWindow } from '../services/maintenance_windows_api/finish';
|
||||
|
||||
export function useFinishMaintenanceWindow() {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const mutationFn = (maintenanceWindowId: string) => {
|
||||
return finishMaintenanceWindow({ http, maintenanceWindowId });
|
||||
};
|
||||
|
||||
return useMutation(mutationFn, {
|
||||
onSuccess: (data) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsFinishedSuccess', {
|
||||
defaultMessage: "Cancelled running maintenance window '{title}'",
|
||||
values: {
|
||||
title: data.title,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsFinishedFailure', {
|
||||
defaultMessage: 'Failed to cancel maintenance window.',
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -79,7 +79,7 @@ describe('useUpdateMaintenanceWindow', () => {
|
|||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockAddDanger).toBeCalledWith("Failed to update maintenance window '123'")
|
||||
expect(mockAddDanger).toBeCalledWith('Failed to update maintenance window.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,13 +39,10 @@ export function useUpdateMaintenanceWindow() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onError: (error, variables) => {
|
||||
onError: () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsUpdateFailure', {
|
||||
defaultMessage: "Failed to update maintenance window '{id}'",
|
||||
values: {
|
||||
id: variables.maintenanceWindowId,
|
||||
},
|
||||
defaultMessage: 'Failed to update maintenance window.',
|
||||
})
|
||||
);
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
FIELD_TYPES,
|
||||
|
@ -16,7 +16,10 @@ import {
|
|||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiConfirmModal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
|
@ -33,6 +36,7 @@ import { useCreateMaintenanceWindow } from '../../../hooks/use_create_maintenanc
|
|||
import { useUpdateMaintenanceWindow } from '../../../hooks/use_update_maintenance_window';
|
||||
import { useUiSetting } from '../../../utils/kibana_react';
|
||||
import { DatePickerRangeField } from './fields/date_picker_range_field';
|
||||
import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window';
|
||||
|
||||
const UseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -56,6 +60,7 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
({ onCancel, onSuccess, initialValue, maintenanceWindowId }) => {
|
||||
const [defaultStartDateValue] = useState<string>(moment().toISOString());
|
||||
const [defaultEndDateValue] = useState<string>(moment().add(30, 'minutes').toISOString());
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { defaultTimezone, isBrowser } = useDefaultTimezone();
|
||||
|
||||
const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined;
|
||||
|
@ -63,6 +68,7 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
useCreateMaintenanceWindow();
|
||||
const { mutate: updateMaintenanceWindow, isLoading: isUpdateLoading } =
|
||||
useUpdateMaintenanceWindow();
|
||||
const { mutate: archiveMaintenanceWindow } = useArchiveMaintenanceWindow();
|
||||
|
||||
const submitMaintenanceWindow = useCallback(
|
||||
async (formData, isValid) => {
|
||||
|
@ -109,6 +115,35 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
const isRecurring = recurring || false;
|
||||
const showTimezone = isBrowser || initialValue?.timezone !== undefined;
|
||||
|
||||
const closeModal = useCallback(() => setIsModalVisible(false), []);
|
||||
const showModal = useCallback(() => setIsModalVisible(true), []);
|
||||
|
||||
const modal = useMemo(() => {
|
||||
let m;
|
||||
if (isModalVisible) {
|
||||
m = (
|
||||
<EuiConfirmModal
|
||||
title={i18n.ARCHIVE_TITLE}
|
||||
onCancel={closeModal}
|
||||
onConfirm={() => {
|
||||
closeModal();
|
||||
archiveMaintenanceWindow(
|
||||
{ maintenanceWindowId: maintenanceWindowId!, archive: true },
|
||||
{ onSuccess }
|
||||
);
|
||||
}}
|
||||
cancelButtonText={i18n.CANCEL}
|
||||
confirmButtonText={i18n.ARCHIVE_TITLE}
|
||||
defaultFocusedButton="confirm"
|
||||
buttonColor="danger"
|
||||
>
|
||||
<p>{i18n.ARCHIVE_CALLOUT_SUBTITLE}</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}, [closeModal, archiveMaintenanceWindow, isModalVisible, maintenanceWindowId, onSuccess]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup direction="column" responsive={false}>
|
||||
|
@ -192,6 +227,15 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
{isRecurring ? <RecurringSchedule data-test-subj="recurring-form" /> : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isEditMode ? (
|
||||
<EuiCallOut title={i18n.ARCHIVE_TITLE} color="danger" iconType="trash">
|
||||
<p>{i18n.ARCHIVE_SUBTITLE}</p>
|
||||
<EuiButton fill color="danger" onClick={showModal}>
|
||||
{i18n.ARCHIVE}
|
||||
</EuiButton>
|
||||
{modal}
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
|
|
@ -93,7 +93,9 @@ describe('MaintenanceWindowsList', () => {
|
|||
});
|
||||
|
||||
test('it renders', () => {
|
||||
const result = appMockRenderer.render(<MaintenanceWindowsList loading={false} items={items} />);
|
||||
const result = appMockRenderer.render(
|
||||
<MaintenanceWindowsList refreshData={() => {}} loading={false} items={items} />
|
||||
);
|
||||
|
||||
expect(result.getAllByTestId('list-item')).toHaveLength(items.length);
|
||||
|
||||
|
|
|
@ -5,29 +5,34 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
formatDate,
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
useEuiBackgroundColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
SearchFilterConfig,
|
||||
EuiBadge,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { MaintenanceWindowFindResponse, SortDirection } from '../types';
|
||||
import * as i18n from '../translations';
|
||||
import { useEditMaintenanceWindowsNavigation } from '../../../hooks/use_navigation';
|
||||
import { STATUS_DISPLAY, STATUS_SORT } from '../constants';
|
||||
import { UpcomingEventsPopover } from './upcoming_events_popover';
|
||||
import { StatusColor, STATUS_DISPLAY, STATUS_SORT } from '../constants';
|
||||
import { MaintenanceWindowStatus } from '../../../../common';
|
||||
import { StatusFilter } from './status_filter';
|
||||
import { TableActionsPopover } from './table_actions_popover';
|
||||
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';
|
||||
|
||||
interface MaintenanceWindowsListProps {
|
||||
loading: boolean;
|
||||
items: MaintenanceWindowFindResponse[];
|
||||
refreshData: () => void;
|
||||
}
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = [
|
||||
|
@ -39,23 +44,9 @@ const columns: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = [
|
|||
{
|
||||
field: 'status',
|
||||
name: i18n.TABLE_STATUS,
|
||||
render: (status: string) => {
|
||||
render: (status: MaintenanceWindowStatus) => {
|
||||
return (
|
||||
<EuiButton
|
||||
css={css`
|
||||
cursor: default;
|
||||
|
||||
:hover:not(:disabled) {
|
||||
text-decoration: none;
|
||||
}
|
||||
`}
|
||||
fill={status === MaintenanceWindowStatus.Running}
|
||||
color={STATUS_DISPLAY[status].color as StatusColor}
|
||||
size="s"
|
||||
onClick={() => {}}
|
||||
>
|
||||
{STATUS_DISPLAY[status].label}
|
||||
</EuiButton>
|
||||
<EuiBadge color={STATUS_DISPLAY[status].color}>{STATUS_DISPLAY[status].label}</EuiBadge>
|
||||
);
|
||||
},
|
||||
sortable: ({ status }) => STATUS_SORT[status],
|
||||
|
@ -108,38 +99,61 @@ const search: { filters: SearchFilterConfig[] } = {
|
|||
};
|
||||
|
||||
export const MaintenanceWindowsList = React.memo<MaintenanceWindowsListProps>(
|
||||
({ loading, items }) => {
|
||||
({ loading, items, refreshData }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { navigateToEditMaintenanceWindows } = useEditMaintenanceWindowsNavigation();
|
||||
const warningBackgroundColor = useEuiBackgroundColor('warning');
|
||||
const subduedBackgroundColor = useEuiBackgroundColor('subdued');
|
||||
const onEdit = useCallback(
|
||||
(id) => navigateToEditMaintenanceWindows(id),
|
||||
[navigateToEditMaintenanceWindows]
|
||||
);
|
||||
const { mutate: finishMaintenanceWindow, isLoading: isLoadingFinish } =
|
||||
useFinishMaintenanceWindow();
|
||||
const onCancel = useCallback(
|
||||
(id) => finishMaintenanceWindow(id, { onSuccess: () => refreshData() }),
|
||||
[finishMaintenanceWindow, refreshData]
|
||||
);
|
||||
const { mutate: archiveMaintenanceWindow, isLoading: isLoadingArchive } =
|
||||
useArchiveMaintenanceWindow();
|
||||
const onArchive = useCallback(
|
||||
(id: string, archive: boolean) =>
|
||||
archiveMaintenanceWindow(
|
||||
{ maintenanceWindowId: id, archive },
|
||||
{ onSuccess: () => refreshData() }
|
||||
),
|
||||
[archiveMaintenanceWindow, refreshData]
|
||||
);
|
||||
const { mutate: finishAndArchiveMaintenanceWindow, isLoading: isLoadingFinishAndArchive } =
|
||||
useFinishAndArchiveMaintenanceWindow();
|
||||
const onCancelAndArchive = useCallback(
|
||||
(id: string) => finishAndArchiveMaintenanceWindow(id, { onSuccess: () => refreshData() }),
|
||||
[finishAndArchiveMaintenanceWindow, refreshData]
|
||||
);
|
||||
|
||||
const tableCss = useMemo(() => {
|
||||
return css`
|
||||
.euiTableRow {
|
||||
&.running {
|
||||
background-color: ${warningBackgroundColor};
|
||||
}
|
||||
|
||||
&.archived {
|
||||
background-color: ${subduedBackgroundColor};
|
||||
background-color: ${euiTheme.colors.highlight};
|
||||
}
|
||||
}
|
||||
`;
|
||||
}, [warningBackgroundColor, subduedBackgroundColor]);
|
||||
}, [euiTheme.colors.highlight]);
|
||||
|
||||
const actions: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = [
|
||||
{
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.TABLE_ACTION_EDIT,
|
||||
isPrimary: true,
|
||||
description: 'Edit maintenance window',
|
||||
icon: 'pencil',
|
||||
type: 'icon',
|
||||
onClick: (mw: MaintenanceWindowFindResponse) => navigateToEditMaintenanceWindows(mw.id),
|
||||
'data-test-subj': 'action-edit',
|
||||
},
|
||||
],
|
||||
render: ({ status, id }: { status: MaintenanceWindowStatus; id: string }) => {
|
||||
return (
|
||||
<TableActionsPopover
|
||||
id={id}
|
||||
status={status}
|
||||
onEdit={onEdit}
|
||||
onCancel={onCancel}
|
||||
onArchive={onArchive}
|
||||
onCancelAndArchive={onCancelAndArchive}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -147,7 +161,7 @@ export const MaintenanceWindowsList = React.memo<MaintenanceWindowsListProps>(
|
|||
<EuiInMemoryTable
|
||||
css={tableCss}
|
||||
itemId="id"
|
||||
loading={loading}
|
||||
loading={loading || isLoadingFinish || isLoadingArchive || isLoadingFinishAndArchive}
|
||||
tableCaption="Maintenance Windows List"
|
||||
items={items}
|
||||
columns={columns.concat(actions)}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { fireEvent } from '@testing-library/dom';
|
||||
import React from 'react';
|
||||
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
|
||||
import { TableActionsPopover } from './table_actions_popover';
|
||||
import { MaintenanceWindowStatus } from '../../../../common';
|
||||
|
||||
describe('TableActionsPopover', () => {
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
});
|
||||
|
||||
test('it renders', () => {
|
||||
const result = appMockRenderer.render(
|
||||
<TableActionsPopover
|
||||
id={'123'}
|
||||
status={MaintenanceWindowStatus.Running}
|
||||
onEdit={() => {}}
|
||||
onCancel={() => {}}
|
||||
onArchive={() => {}}
|
||||
onCancelAndArchive={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(result.getByTestId('table-actions-icon-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it shows the correct actions when a maintenance window is running', () => {
|
||||
const result = appMockRenderer.render(
|
||||
<TableActionsPopover
|
||||
id={'123'}
|
||||
status={MaintenanceWindowStatus.Running}
|
||||
onEdit={() => {}}
|
||||
onCancel={() => {}}
|
||||
onArchive={() => {}}
|
||||
onCancelAndArchive={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(result.getByTestId('table-actions-icon-button'));
|
||||
expect(result.getByTestId('table-actions-edit')).toBeInTheDocument();
|
||||
expect(result.getByTestId('table-actions-cancel')).toBeInTheDocument();
|
||||
expect(result.getByTestId('table-actions-cancel-and-archive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it shows the correct actions when a maintenance window is upcoming', () => {
|
||||
const result = appMockRenderer.render(
|
||||
<TableActionsPopover
|
||||
id={'123'}
|
||||
status={MaintenanceWindowStatus.Upcoming}
|
||||
onEdit={() => {}}
|
||||
onCancel={() => {}}
|
||||
onArchive={() => {}}
|
||||
onCancelAndArchive={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(result.getByTestId('table-actions-icon-button'));
|
||||
expect(result.getByTestId('table-actions-edit')).toBeInTheDocument();
|
||||
expect(result.getByTestId('table-actions-archive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it shows the correct actions when a maintenance window is finished', () => {
|
||||
const result = appMockRenderer.render(
|
||||
<TableActionsPopover
|
||||
id={'123'}
|
||||
status={MaintenanceWindowStatus.Finished}
|
||||
onEdit={() => {}}
|
||||
onCancel={() => {}}
|
||||
onArchive={() => {}}
|
||||
onCancelAndArchive={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(result.getByTestId('table-actions-icon-button'));
|
||||
expect(result.getByTestId('table-actions-edit')).toBeInTheDocument();
|
||||
expect(result.getByTestId('table-actions-archive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it shows the correct actions when a maintenance window is archived', () => {
|
||||
const result = appMockRenderer.render(
|
||||
<TableActionsPopover
|
||||
id={'123'}
|
||||
status={MaintenanceWindowStatus.Archived}
|
||||
onEdit={() => {}}
|
||||
onCancel={() => {}}
|
||||
onArchive={() => {}}
|
||||
onCancelAndArchive={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(result.getByTestId('table-actions-icon-button'));
|
||||
expect(result.getByTestId('table-actions-unarchive')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiConfirmModal,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import * as i18n from '../translations';
|
||||
import { MaintenanceWindowStatus } from '../../../../common';
|
||||
|
||||
interface TableActionsPopoverProps {
|
||||
id: string;
|
||||
status: MaintenanceWindowStatus;
|
||||
onEdit: (id: string) => void;
|
||||
onCancel: (id: string) => void;
|
||||
onArchive: (id: string, archive: boolean) => void;
|
||||
onCancelAndArchive: (id: string) => void;
|
||||
}
|
||||
type ModalType = 'cancel' | 'cancelAndArchive' | 'archive' | 'unarchive';
|
||||
type ActionType = ModalType | 'edit';
|
||||
|
||||
export const TableActionsPopover: React.FC<TableActionsPopoverProps> = React.memo(
|
||||
({ id, status, onEdit, onCancel, onArchive, onCancelAndArchive }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modalType, setModalType] = useState<ModalType>();
|
||||
|
||||
const onButtonClick = useCallback(() => {
|
||||
setIsPopoverOpen((open) => !open);
|
||||
}, []);
|
||||
const closePopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => setIsModalVisible(false), []);
|
||||
const showModal = useCallback((type: ModalType) => {
|
||||
setModalType(type);
|
||||
setIsModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const modal = useMemo(() => {
|
||||
const modals = {
|
||||
cancel: {
|
||||
props: {
|
||||
title: i18n.CANCEL_MODAL_TITLE,
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
onCancel(id);
|
||||
},
|
||||
cancelButtonText: i18n.CANCEL_MODAL_BUTTON,
|
||||
confirmButtonText: i18n.CANCEL_MODAL_TITLE,
|
||||
},
|
||||
subtitle: i18n.CANCEL_MODAL_SUBTITLE,
|
||||
},
|
||||
cancelAndArchive: {
|
||||
props: {
|
||||
title: i18n.CANCEL_AND_ARCHIVE_MODAL_TITLE,
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
onCancelAndArchive(id);
|
||||
},
|
||||
cancelButtonText: i18n.CANCEL_MODAL_BUTTON,
|
||||
confirmButtonText: i18n.CANCEL_AND_ARCHIVE_MODAL_TITLE,
|
||||
},
|
||||
subtitle: i18n.CANCEL_AND_ARCHIVE_MODAL_SUBTITLE,
|
||||
},
|
||||
archive: {
|
||||
props: {
|
||||
title: i18n.ARCHIVE_TITLE,
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
onArchive(id, true);
|
||||
},
|
||||
cancelButtonText: i18n.CANCEL,
|
||||
confirmButtonText: i18n.ARCHIVE_TITLE,
|
||||
},
|
||||
subtitle: i18n.ARCHIVE_SUBTITLE,
|
||||
},
|
||||
unarchive: {
|
||||
props: {
|
||||
title: i18n.UNARCHIVE_MODAL_TITLE,
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
onArchive(id, false);
|
||||
},
|
||||
cancelButtonText: i18n.CANCEL,
|
||||
confirmButtonText: i18n.UNARCHIVE_MODAL_TITLE,
|
||||
},
|
||||
subtitle: i18n.UNARCHIVE_MODAL_SUBTITLE,
|
||||
},
|
||||
};
|
||||
let m;
|
||||
if (isModalVisible && modalType) {
|
||||
const modalProps = modals[modalType];
|
||||
m = (
|
||||
<EuiConfirmModal
|
||||
{...modalProps.props}
|
||||
style={{ width: 600 }}
|
||||
onCancel={closeModal}
|
||||
defaultFocusedButton="confirm"
|
||||
buttonColor="danger"
|
||||
>
|
||||
<p>{modalProps.subtitle}</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}, [id, modalType, isModalVisible, closeModal, onArchive, onCancel, onCancelAndArchive]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const menuItems = {
|
||||
edit: (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="table-actions-edit"
|
||||
key="edit"
|
||||
icon="pencil"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onEdit(id);
|
||||
}}
|
||||
>
|
||||
{i18n.TABLE_ACTION_EDIT}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
cancel: (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="table-actions-cancel"
|
||||
key="cancel"
|
||||
icon="stopSlash"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
showModal('cancel');
|
||||
}}
|
||||
>
|
||||
{i18n.TABLE_ACTION_CANCEL}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
cancelAndArchive: (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="table-actions-cancel-and-archive"
|
||||
key="cancel-and-archive"
|
||||
icon="trash"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
showModal('cancelAndArchive');
|
||||
}}
|
||||
>
|
||||
{i18n.TABLE_ACTION_CANCEL_AND_ARCHIVE}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
archive: (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="table-actions-archive"
|
||||
key="archive"
|
||||
icon="trash"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
showModal('archive');
|
||||
}}
|
||||
>
|
||||
{i18n.ARCHIVE}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
unarchive: (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="table-actions-unarchive"
|
||||
key="unarchive"
|
||||
icon="exit"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
showModal('unarchive');
|
||||
}}
|
||||
>
|
||||
{i18n.TABLE_ACTION_UNARCHIVE}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
};
|
||||
const statusMenuItemsMap: Record<MaintenanceWindowStatus, ActionType[]> = {
|
||||
running: ['edit', 'cancel', 'cancelAndArchive'],
|
||||
upcoming: ['edit', 'archive'],
|
||||
finished: ['edit', 'archive'],
|
||||
archived: ['unarchive'],
|
||||
};
|
||||
return statusMenuItemsMap[status].map((type) => menuItems[type]);
|
||||
}, [id, status, onEdit, closePopover, showModal]);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="table-actions-icon-button"
|
||||
iconType="boxesHorizontal"
|
||||
size="s"
|
||||
aria-label="Upcoming events"
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
),
|
||||
[onButtonClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downCenter"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
TableActionsPopover.displayName = 'TableActionsPopover';
|
|
@ -107,15 +107,13 @@ export const RRULE_WEEKDAYS_TO_ISO_WEEKDAYS = mapValues(invert(ISO_WEEKDAYS_TO_R
|
|||
Number(v)
|
||||
);
|
||||
|
||||
export const STATUS_DISPLAY: Record<string, { color: string; label: string }> = {
|
||||
[MaintenanceWindowStatus.Running]: { color: 'warning', label: i18n.TABLE_STATUS_RUNNING },
|
||||
export const STATUS_DISPLAY = {
|
||||
[MaintenanceWindowStatus.Running]: { color: 'primary', label: i18n.TABLE_STATUS_RUNNING },
|
||||
[MaintenanceWindowStatus.Upcoming]: { color: 'warning', label: i18n.TABLE_STATUS_UPCOMING },
|
||||
[MaintenanceWindowStatus.Finished]: { color: 'success', label: i18n.TABLE_STATUS_FINISHED },
|
||||
[MaintenanceWindowStatus.Archived]: { color: 'text', label: i18n.TABLE_STATUS_ARCHIVED },
|
||||
[MaintenanceWindowStatus.Archived]: { color: 'default', label: i18n.TABLE_STATUS_ARCHIVED },
|
||||
};
|
||||
|
||||
export type StatusColor = 'warning' | 'success' | 'text';
|
||||
|
||||
export const STATUS_SORT = {
|
||||
[MaintenanceWindowStatus.Running]: 0,
|
||||
[MaintenanceWindowStatus.Upcoming]: 1,
|
||||
|
|
|
@ -31,7 +31,7 @@ export const MaintenanceWindowsPage = React.memo(() => {
|
|||
const { docLinks } = useKibana().services;
|
||||
const { navigateToCreateMaintenanceWindow } = useCreateMaintenanceWindowNavigation();
|
||||
|
||||
const { isLoading, maintenanceWindows } = useFindMaintenanceWindows();
|
||||
const { isLoading, maintenanceWindows, refetch } = useFindMaintenanceWindows();
|
||||
|
||||
useBreadcrumbs(AlertingDeepLinkId.maintenanceWindows);
|
||||
|
||||
|
@ -39,6 +39,8 @@ export const MaintenanceWindowsPage = React.memo(() => {
|
|||
navigateToCreateMaintenanceWindow();
|
||||
}, [navigateToCreateMaintenanceWindow]);
|
||||
|
||||
const refreshData = useCallback(() => refetch(), [refetch]);
|
||||
|
||||
const showEmptyPrompt = !isLoading && maintenanceWindows.length === 0;
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -77,7 +79,11 @@ export const MaintenanceWindowsPage = React.memo(() => {
|
|||
) : (
|
||||
<>
|
||||
<EuiSpacer size="xl" />
|
||||
<MaintenanceWindowsList loading={isLoading} items={maintenanceWindows} />
|
||||
<MaintenanceWindowsList
|
||||
refreshData={refreshData}
|
||||
loading={isLoading}
|
||||
items={maintenanceWindows}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -454,6 +454,102 @@ export const SAVE_MAINTENANCE_WINDOW = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const TABLE_ACTION_CANCEL = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.table.cancel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_MODAL_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.cancelModal.title',
|
||||
{
|
||||
defaultMessage: 'Cancel maintenance window',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_MODAL_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.cancelModal.subtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'Rule notifications resume immediately. Running maintenance window events are canceled; upcoming events are unaffected.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_MODAL_BUTTON = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.cancelModal.button',
|
||||
{
|
||||
defaultMessage: 'Keep running',
|
||||
}
|
||||
);
|
||||
|
||||
export const TABLE_ACTION_CANCEL_AND_ARCHIVE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.table.cancelAndArchive',
|
||||
{
|
||||
defaultMessage: 'Cancel and archive',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_AND_ARCHIVE_MODAL_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.cancelAndArchiveModal.title',
|
||||
{
|
||||
defaultMessage: 'Cancel and archive maintenance window',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_AND_ARCHIVE_MODAL_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.cancelAndArchiveModal.subtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'Rule notifications resume immediately. All running and upcoming maintenance window events are canceled and the window is queued for deletion.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ARCHIVE = i18n.translate('xpack.alerting.maintenanceWindows.archive', {
|
||||
defaultMessage: 'Archive',
|
||||
});
|
||||
|
||||
export const ARCHIVE_TITLE = i18n.translate('xpack.alerting.maintenanceWindows.archive.title', {
|
||||
defaultMessage: 'Archive maintenance window',
|
||||
});
|
||||
|
||||
export const ARCHIVE_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.archive.subtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'Upcoming maintenance window events are canceled and the window is queued for deletion.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TABLE_ACTION_UNARCHIVE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.table.unarchive',
|
||||
{
|
||||
defaultMessage: 'Unarchive',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNARCHIVE_MODAL_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.unarchiveModal.title',
|
||||
{
|
||||
defaultMessage: 'Unarchive maintenance window',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNARCHIVE_MODAL_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.unarchiveModal.subtitle',
|
||||
{
|
||||
defaultMessage: 'Upcoming maintenance window events are scheduled.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ARCHIVE_CALLOUT_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.archiveCallout.subtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'The changes you have made here will not be saved. Are you sure you want to discard these unsaved changes and archive this maintenance window?',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPERIMENTAL_LABEL = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.badge.experimentalLabel',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { MaintenanceWindow } from '../../pages/maintenance_windows/types';
|
||||
import { archiveMaintenanceWindow } from './archive';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('archiveMaintenanceWindow', () => {
|
||||
test('should call archive maintenance window api', async () => {
|
||||
const apiResponse = {
|
||||
title: 'test',
|
||||
duration: 1,
|
||||
r_rule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
freq: 3,
|
||||
interval: 1,
|
||||
byweekday: ['TH'],
|
||||
},
|
||||
};
|
||||
http.post.mockResolvedValueOnce(apiResponse);
|
||||
|
||||
const maintenanceWindow: MaintenanceWindow = {
|
||||
title: 'test',
|
||||
duration: 1,
|
||||
rRule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
freq: 3,
|
||||
interval: 1,
|
||||
byweekday: ['TH'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await archiveMaintenanceWindow({
|
||||
http,
|
||||
maintenanceWindowId: '123',
|
||||
archive: true,
|
||||
});
|
||||
expect(result).toEqual(maintenanceWindow);
|
||||
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/internal/alerting/rules/maintenance_window/123/_archive",
|
||||
Object {
|
||||
"body": "{\\"archive\\":true}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
|
||||
|
||||
import { MaintenanceWindow } from '../../pages/maintenance_windows/types';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common';
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
});
|
||||
|
||||
export async function archiveMaintenanceWindow({
|
||||
http,
|
||||
maintenanceWindowId,
|
||||
archive,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
maintenanceWindowId: string;
|
||||
archive: boolean;
|
||||
}): Promise<MaintenanceWindow> {
|
||||
const res = await http.post<AsApiContract<MaintenanceWindow>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${encodeURIComponent(
|
||||
maintenanceWindowId
|
||||
)}/_archive`,
|
||||
{ body: JSON.stringify({ archive }) }
|
||||
);
|
||||
|
||||
return rewriteBodyRes(res);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { MaintenanceWindow } from '../../pages/maintenance_windows/types';
|
||||
import { finishMaintenanceWindow } from './finish';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('finishMaintenanceWindow', () => {
|
||||
test('should call finish maintenance window api', async () => {
|
||||
const apiResponse = {
|
||||
title: 'test',
|
||||
duration: 1,
|
||||
r_rule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
freq: 3,
|
||||
interval: 1,
|
||||
byweekday: ['TH'],
|
||||
},
|
||||
};
|
||||
http.post.mockResolvedValueOnce(apiResponse);
|
||||
|
||||
const maintenanceWindow: MaintenanceWindow = {
|
||||
title: 'test',
|
||||
duration: 1,
|
||||
rRule: {
|
||||
dtstart: '2023-03-23T19:16:21.293Z',
|
||||
tzid: 'America/New_York',
|
||||
freq: 3,
|
||||
interval: 1,
|
||||
byweekday: ['TH'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await finishMaintenanceWindow({
|
||||
http,
|
||||
maintenanceWindowId: '123',
|
||||
});
|
||||
expect(result).toEqual(maintenanceWindow);
|
||||
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/internal/alerting/rules/maintenance_window/123/_finish",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
|
||||
|
||||
import { MaintenanceWindow } from '../../pages/maintenance_windows/types';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common';
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
});
|
||||
|
||||
export async function finishMaintenanceWindow({
|
||||
http,
|
||||
maintenanceWindowId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
maintenanceWindowId: string;
|
||||
}): Promise<MaintenanceWindow> {
|
||||
const res = await http.post<AsApiContract<MaintenanceWindow>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${encodeURIComponent(
|
||||
maintenanceWindowId
|
||||
)}/_finish`
|
||||
);
|
||||
|
||||
return rewriteBodyRes(res);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue