Skip to content

Commit

Permalink
[ResponseOps][Window Maintenance] Add the maintenance window table (e…
Browse files Browse the repository at this point in the history
…lastic#154491)

Resolves elastic#153770
Resolves elastic#153773

## Summary

This pr creates the maintenance window table, and adds pagination and
sorting. It also adds the search bar and filtering.

<img width="1711" alt="Screen Shot 2023-04-06 at 9 41 42 AM"
src="https://user-images.githubusercontent.com/109488926/230396037-10336b73-1c2e-4cd3-b7ae-26c834cf500d.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

- Create a maintenance window and verify you can view it in the table

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and nikitaindik committed Apr 25, 2023
1 parent 4bcc645 commit c861693
Show file tree
Hide file tree
Showing 23 changed files with 841 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -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 { renderHook } from '@testing-library/react-hooks/dom';
import { waitFor } from '@testing-library/dom';

import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils';
import { useFindMaintenanceWindows } from './use_find_maintenance_windows';

const mockAddDanger = 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: { addDanger: mockAddDanger } },
},
};
},
};
});
jest.mock('../services/maintenance_windows_api/find', () => ({
findMaintenanceWindows: jest.fn(),
}));

const { findMaintenanceWindows } = jest.requireMock('../services/maintenance_windows_api/find');

let appMockRenderer: AppMockRenderer;

describe('useFindMaintenanceWindows', () => {
beforeEach(() => {
jest.clearAllMocks();

appMockRenderer = createAppMockRenderer();
});

it('should call onError if api fails', async () => {
findMaintenanceWindows.mockRejectedValue('This is an error.');

renderHook(() => useFindMaintenanceWindows(), {
wrapper: appMockRenderer.AppWrapper,
});

await waitFor(() =>
expect(mockAddDanger).toBeCalledWith('Unable to load maintenance windows.')
);
});
});
Original file line number Diff line number Diff line change
@@ -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 { useQuery } from '@tanstack/react-query';
import { useKibana } from '../utils/kibana_react';
import { findMaintenanceWindows } from '../services/maintenance_windows_api/find';

export const useFindMaintenanceWindows = () => {
const {
http,
notifications: { toasts },
} = useKibana().services;

const queryFn = () => {
return findMaintenanceWindows({ http });
};

const onErrorFn = (error: Error) => {
if (error) {
toasts.addDanger(
i18n.translate('xpack.alerting.maintenanceWindowsListFailure', {
defaultMessage: 'Unable to load maintenance windows.',
})
);
}
};

const { isLoading, data = [] } = useQuery({
queryKey: ['findMaintenanceWindows'],
queryFn,
onError: onErrorFn,
refetchOnWindowFocus: false,
retry: false,
});

return {
maintenanceWindows: data,
isLoading,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 from 'react';

import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
import { CenterJustifiedSpinner } from './center_justified_spinner';

describe('CenterJustifiedSpinner', () => {
let appMockRenderer: AppMockRenderer;

beforeEach(() => {
jest.clearAllMocks();
appMockRenderer = createAppMockRenderer();
});

test('it renders', () => {
const result = appMockRenderer.render(<CenterJustifiedSpinner />);

expect(result.getByTestId('center-justified-spinner')).toBeInTheDocument();
});
});
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.
*/

import React from 'react';

import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { EuiLoadingSpinnerSize } from '@elastic/eui/src/components/loading/loading_spinner';

interface Props {
size?: EuiLoadingSpinnerSize;
}

export const CenterJustifiedSpinner: React.FunctionComponent<Props> = ({ size }) => (
<EuiFlexGroup data-test-subj="center-justified-spinner" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size={size || 'xl'} />
</EuiFlexItem>
</EuiFlexGroup>
);
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ export const useTimeZone = (): string => {

export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFormProps>(
({ onCancel, onSuccess, initialValue }) => {
const [defaultDateValue] = useState<string>(moment().toISOString());
const [defaultStartDateValue] = useState<string>(moment().toISOString());
const [defaultEndDateValue] = useState<string>(moment().add(30, 'minutes').toISOString());
const timezone = useTimeZone();

const { mutate: createMaintenanceWindow } = useCreateMaintenanceWindow();
const { mutate: createMaintenanceWindow, isLoading: isCreateLoading } =
useCreateMaintenanceWindow();

const submitMaintenanceWindow = useCallback(
async (formData, isValid) => {
Expand Down Expand Up @@ -101,15 +103,15 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
path: 'startDate',
config: {
label: i18n.CREATE_FORM_SCHEDULE,
defaultValue: defaultDateValue,
defaultValue: defaultStartDateValue,
validations: [],
},
},
endDate: {
path: 'endDate',
config: {
label: '',
defaultValue: defaultDateValue,
defaultValue: defaultEndDateValue,
validations: [],
},
},
Expand Down Expand Up @@ -149,7 +151,7 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SubmitButton />
<SubmitButton isLoading={isCreateLoading} />
</EuiFlexItem>
</EuiFlexGroup>
</Form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export const ButtonGroupField: React.FC<ButtonGroupFieldProps> = React.memo(
<EuiFormRow label={field.label} {...rest} fullWidth>
{type === 'multi' ? (
<EuiButtonGroup
buttonSize="compressed"
isFullWidth
legend={legend}
onChange={onChangeMulti}
Expand All @@ -60,7 +59,6 @@ export const ButtonGroupField: React.FC<ButtonGroupFieldProps> = React.memo(
/>
) : (
<EuiButtonGroup
buttonSize="compressed"
isFullWidth
legend={legend}
onChange={onChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* 2.0.
*/

import React, { useCallback } from 'react';
import { Moment } from 'moment';
import React, { useCallback, useState } from 'react';
import moment, { Moment } from 'moment';
import { EuiDatePicker, EuiFormRow } from '@elastic/eui';
import {
useFormData,
Expand All @@ -23,6 +23,8 @@ interface DatePickerFieldProps {

export const DatePickerField: React.FC<DatePickerFieldProps> = React.memo(
({ field, showTimeSelect = true, ...rest }) => {
const [today] = useState<Moment>(moment());

const { setFieldValue } = useFormContext();
const [form] = useFormData({ watch: [field.path] });

Expand All @@ -42,7 +44,7 @@ export const DatePickerField: React.FC<DatePickerFieldProps> = React.memo(
showTimeSelect={showTimeSelect}
selected={selected}
onChange={onChange}
minDate={selected}
minDate={today}
fullWidth
/>
</EuiFormRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* 2.0.
*/

import React, { useCallback } from 'react';
import { Moment } from 'moment';
import React, { useCallback, useState } from 'react';
import moment, { Moment } from 'moment';
import { EuiDatePicker, EuiDatePickerRange, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
import {
useFormData,
Expand All @@ -24,18 +24,34 @@ interface DatePickerRangeFieldProps {

export const DatePickerRangeField: React.FC<DatePickerRangeFieldProps> = React.memo(
({ fields, showTimeSelect = true, ...rest }) => {
const [today] = useState<Moment>(moment());

const { setFieldValue } = useFormContext();
const [form] = useFormData({ watch: [fields.startDate.path, fields.endDate.path] });

const startDate = getSelected(form, fields.startDate.path);
const endDate = getSelected(form, fields.endDate.path);

const onChange = useCallback(
(currentDate: Moment | null, path: string) => {
const onStartDateChange = useCallback(
(currentDate: Moment | null) => {
if (currentDate && currentDate.isAfter(endDate)) {
// if the current start date is ahead of the end date
// set the end date to the current start date + 30 min
const updatedEndDate = moment(currentDate).add(30, 'minutes');
setFieldValue(fields.endDate.path, updatedEndDate);
}
// convert the moment date back into a string if it's not null
setFieldValue(fields.startDate.path, currentDate ? currentDate.toISOString() : currentDate);
},
[setFieldValue, endDate, fields.endDate.path, fields.startDate.path]
);

const onEndDateChange = useCallback(
(currentDate: Moment | null) => {
// convert the moment date back into a string if it's not null
setFieldValue(path, currentDate ? currentDate.toISOString() : currentDate);
setFieldValue(fields.endDate.path, currentDate ? currentDate.toISOString() : currentDate);
},
[setFieldValue]
[setFieldValue, fields.endDate.path]
);
const isInvalid = startDate.isAfter(endDate);

Expand All @@ -47,23 +63,23 @@ export const DatePickerRangeField: React.FC<DatePickerRangeFieldProps> = React.m
startDateControl={
<EuiDatePicker
selected={startDate}
onChange={(date) => date && onChange(date, fields.startDate.path)}
onChange={(date) => date && onStartDateChange(date)}
startDate={startDate}
endDate={endDate}
aria-label="Start date"
showTimeSelect={showTimeSelect}
minDate={startDate}
minDate={today}
/>
}
endDateControl={
<EuiDatePicker
selected={endDate}
onChange={(date) => date && onChange(date, fields.endDate.path)}
onChange={(date) => date && onEndDateChange(date)}
startDate={startDate}
endDate={endDate}
aria-label="End date"
showTimeSelect={showTimeSelect}
minDate={startDate}
minDate={today}
/>
}
fullWidth
Expand Down
Loading

0 comments on commit c861693

Please sign in to comment.