[Uptime] add basic monitor adding behavior (#119965) (#120011)

Co-authored-by: Dominique Clarke <doclarke71@gmail.com>
This commit is contained in:
Kibana Machine 2021-11-30 15:04:39 -05:00 committed by GitHub
parent d3c7fe8829
commit fdff04e46f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 263 additions and 3 deletions

View file

@ -231,6 +231,8 @@ export type ICustomFields = HTTPFields &
[ConfigKeys.NAME]: string;
};
export type Monitor = Partial<ICustomFields>;
export interface PolicyConfig {
[DataStream.HTTP]: HTTPFields;
[DataStream.TCP]: TCPFields;

View file

@ -0,0 +1,98 @@
/*
* 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 { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../../lib/helper/rtl_helpers';
import * as fetchers from '../../../state/api/monitor_management';
import { DataStream, ScheduleUnit } from '../../fleet_package/types';
import { ActionBar } from './action_bar';
describe('<ActionBar />', () => {
const setMonitor = jest.spyOn(fetchers, 'setMonitor');
const monitor = {
name: 'test-monitor',
schedule: {
unit: ScheduleUnit.MINUTES,
number: '2',
},
urls: 'https://elastic.co',
type: DataStream.BROWSER,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('only calls setMonitor when valid and after submission', () => {
const id = 'test-id';
render(<ActionBar monitor={monitor} id={id} isValid={true} />);
userEvent.click(screen.getByText('Edit monitor'));
expect(setMonitor).toBeCalledWith({ monitor, id });
});
it('does not call setMonitor until submission', () => {
const id = 'test-id';
render(<ActionBar monitor={monitor} id={id} isValid={true} />);
expect(setMonitor).not.toBeCalled();
userEvent.click(screen.getByText('Edit monitor'));
expect(setMonitor).toBeCalledWith({ monitor, id });
});
it('does not call setMonitor if invalid', () => {
const id = 'test-id';
render(<ActionBar monitor={monitor} id={id} isValid={false} />);
expect(setMonitor).not.toBeCalled();
userEvent.click(screen.getByText('Edit monitor'));
expect(setMonitor).not.toBeCalled();
});
it.each([
['', 'Save monitor'],
['test-id', 'Edit monitor'],
])('displays right call to action', (id, callToAction) => {
render(<ActionBar monitor={monitor} id={id} isValid={true} />);
expect(screen.getByText(callToAction)).toBeInTheDocument();
});
it('disables button and displays help text when form is invalid after first submission', async () => {
render(<ActionBar monitor={monitor} isValid={false} />);
expect(
screen.queryByText('Your monitor has errors. Please fix them before saving.')
).not.toBeInTheDocument();
expect(screen.getByText('Save monitor')).not.toBeDisabled();
userEvent.click(screen.getByText('Save monitor'));
await waitFor(() => {
expect(
screen.getByText('Your monitor has errors. Please fix them before saving.')
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Save monitor' })).toBeDisabled();
});
});
it('calls option onSave when saving monitor', () => {
const onSave = jest.fn();
render(<ActionBar monitor={monitor} isValid={false} onSave={onSave} />);
userEvent.click(screen.getByText('Save monitor'));
expect(onSave).toBeCalled();
});
});

View file

@ -0,0 +1,131 @@
/*
* 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, useState, useEffect } from 'react';
import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FETCH_STATUS, useFetcher } from '../../../../../observability/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { setMonitor } from '../../../state/api';
import { Monitor } from '../../fleet_package/types';
interface Props {
id?: string;
monitor: Monitor;
isValid: boolean;
onSave?: () => void;
}
export const ActionBar = ({ id, monitor, isValid, onSave }: Props) => {
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const { notifications } = useKibana();
const { data, status } = useFetcher(() => {
if (!isSaving || !isValid) {
return;
}
return setMonitor({ monitor, id });
}, [monitor, id, isValid, isSaving]);
const handleOnSave = useCallback(() => {
if (onSave) {
onSave();
}
setIsSaving(true);
setHasBeenSubmitted(true);
}, [onSave]);
useEffect(() => {
if (!isSaving) {
return;
}
if (!isValid) {
setIsSaving(false);
return;
}
if (status === FETCH_STATUS.FAILURE || status === FETCH_STATUS.SUCCESS) {
setIsSaving(false);
}
if (status === FETCH_STATUS.FAILURE) {
notifications.toasts.danger({
title: <p data-test-subj="uptimeAddMonitorFailure">{MONITOR_FAILURE_LABEL}</p>,
toastLifeTimeMs: 3000,
});
} else if (status === FETCH_STATUS.SUCCESS) {
notifications.toasts.success({
title: <p data-test-subj="uptimeAddMonitorSuccess">{MONITOR_SUCCESS_LABEL}</p>,
toastLifeTimeMs: 3000,
});
}
}, [data, status, notifications.toasts, isSaving, isValid]);
return (
<EuiBottomBar>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>{!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" size="s" iconType="cross">
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
fill
size="s"
iconType="check"
onClick={handleOnSave}
isLoading={isSaving}
disabled={hasBeenSubmitted && !isValid}
>
{id ? EDIT_MONITOR_LABEL : SAVE_MONITOR_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
);
};
const DISCARD_LABEL = i18n.translate('xpack.uptime.monitorManagement.discardLabel', {
defaultMessage: 'Discard',
});
const SAVE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.saveMonitorLabel', {
defaultMessage: 'Save monitor',
});
const EDIT_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorLabel', {
defaultMessage: 'Edit monitor',
});
const VALIDATION_ERROR_LABEL = i18n.translate('xpack.uptime.monitorManagement.validationError', {
defaultMessage: 'Your monitor has errors. Please fix them before saving.',
});
const MONITOR_SUCCESS_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.monitorSuccessMessage',
{
defaultMessage: 'Monitor added successfully.',
}
);
// TODO: Discuss error states with product
const MONITOR_FAILURE_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.monitorFailureMessage',
{
defaultMessage: 'Monitor was unable to be saved. Please try again later.',
}
);

View file

@ -12,9 +12,14 @@ import { defaultConfig, usePolicyConfigContext } from '../fleet_package/contexts
import { usePolicy } from '../fleet_package/hooks/use_policy';
import { validate } from '../fleet_package/validation';
import { MonitorFields } from './monitor_fields';
import { ActionBar } from './action_bar/action_bar';
import { useFormatMonitor } from './hooks/use_format_monitor';
export const MonitorConfig = () => {
interface Props {
id?: string;
}
export const MonitorConfig = ({ id }: Props) => {
const { monitorType } = usePolicyConfigContext();
/* TODO - Use Effect to make sure the package/index templates are loaded. Wait for it to load before showing view
* then show error message if it fails */
@ -26,12 +31,17 @@ export const MonitorConfig = () => {
This type of helper should ideally be moved to task manager where we are syncing the config.
We can process validation (isValid) and formatting for heartbeat (formattedMonitor) separately
We don't need to save the heartbeat compatible version in saved objects */
useFormatMonitor({
const { isValid } = useFormatMonitor({
monitorType,
validate,
config: policyConfig[monitorType],
defaultConfig: defaultConfig[monitorType],
});
return <MonitorFields />;
return (
<>
<MonitorFields />
<ActionBar id={id} monitor={policyConfig[monitorType]} isValid={isValid} />
</>
);
};

View file

@ -13,3 +13,4 @@ export * from './dynamic_settings';
export * from './index_status';
export * from './ping';
export * from './monitor_duration';
export * from './monitor_management';

View file

@ -0,0 +1,18 @@
/*
* 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 { API_URLS } from '../../../common/constants';
import { apiService } from './utils';
// TODO, change to monitor runtime type
export const setMonitor = async ({ monitor, id }: { monitor: any; id?: string }): Promise<void> => {
if (id) {
return await apiService.post(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor);
} else {
return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor);
}
};