Skip to content

Commit

Permalink
[Uptime][Monitor Management UI] Add Enabled column in monitor managem…
Browse files Browse the repository at this point in the history
…ent monitors list table. (#121682) (elastic/uptime/issues/415)

* Add enabled column in monitor management monitors list table.
  • Loading branch information
awahab07 authored Jan 10, 2022
1 parent 25f4d0c commit 786f416
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui';
import { UptimeSettingsContext } from '../../../contexts';
import { useFetcher, FETCH_STATUS } from '../../../../../observability/public';
import { deleteMonitor } from '../../../state/api/monitor_management';
import { deleteMonitor } from '../../../state/api';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ConfigKey, DataStream, SyntheticsMonitor } from '../../../../common/runtime_types';
import { render } from '../../../lib/helper/rtl_helpers';
import { FETCH_STATUS } from '../../../../../observability/public';
import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher';
import { MonitorEnabled } from './monitor_enabled';

describe('<MonitorEnabled />', () => {
const setRefresh = jest.fn();
const testMonitor = {
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
[ConfigKey.ENABLED]: true,
} as unknown as SyntheticsMonitor;

const assertMonitorEnabled = (button: HTMLButtonElement) =>
expect(button).toHaveAttribute('aria-checked', 'true');
const assertMonitorDisabled = (button: HTMLButtonElement) =>
expect(button).toHaveAttribute('aria-checked', 'false');

let useFetcher: jest.SpyInstance;

beforeEach(() => {
useFetcher?.mockClear();
useFetcher = spyOnUseFetcher({});
});

it('correctly renders "enabled" state', () => {
render(<MonitorEnabled id="test-id" monitor={testMonitor} setRefresh={setRefresh} />);

const switchButton = screen.getByRole('switch') as HTMLButtonElement;
assertMonitorEnabled(switchButton);
});

it('correctly renders "disabled" state', () => {
render(
<MonitorEnabled
id="test-id"
monitor={{ ...testMonitor, [ConfigKey.ENABLED]: false }}
setRefresh={setRefresh}
/>
);

const switchButton = screen.getByRole('switch') as HTMLButtonElement;
assertMonitorDisabled(switchButton);
});

it('toggles on click', () => {
render(<MonitorEnabled id="test-id" monitor={testMonitor} setRefresh={setRefresh} />);

const switchButton = screen.getByRole('switch') as HTMLButtonElement;
userEvent.click(switchButton);
assertMonitorDisabled(switchButton);
userEvent.click(switchButton);
assertMonitorEnabled(switchButton);
});

it('is disabled while request is in progress', () => {
useFetcher.mockReturnValue({
data: {},
status: FETCH_STATUS.LOADING,
refetch: () => {},
});

render(<MonitorEnabled id="test-id" monitor={testMonitor} setRefresh={setRefresh} />);
const switchButton = screen.getByRole('switch') as HTMLButtonElement;
userEvent.click(switchButton);

expect(switchButton).toHaveAttribute('disabled');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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 { EuiSwitch, EuiProgress, EuiSwitchEvent } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { FETCH_STATUS, useFetcher } from '../../../../../observability/public';
import { ConfigKey, SyntheticsMonitor } from '../../../../common/runtime_types';
import { setMonitor } from '../../../state/api';

interface Props {
id: string;
monitor: SyntheticsMonitor;
setRefresh: React.Dispatch<React.SetStateAction<boolean>>;
}

export const MonitorEnabled = ({ id, monitor, setRefresh }: Props) => {
const [isEnabled, setIsEnabled] = useState<boolean | null>(null);

const { notifications } = useKibana();

const { status } = useFetcher(() => {
if (isEnabled !== null) {
return setMonitor({ id, monitor: { ...monitor, [ConfigKey.ENABLED]: isEnabled } });
}
}, [isEnabled]);

useEffect(() => {
if (status === FETCH_STATUS.FAILURE) {
notifications.toasts.danger({
title: (
<p data-test-subj="uptimeMonitorEnabledUpdateFailure">
{getMonitorEnabledUpdateFailureMessage(monitor[ConfigKey.NAME])}
</p>
),
toastLifeTimeMs: 3000,
});
setIsEnabled(null);
} else if (status === FETCH_STATUS.SUCCESS) {
notifications.toasts.success({
title: (
<p data-test-subj="uptimeMonitorEnabledUpdateSuccess">
{isEnabled
? getMonitorEnabledSuccessLabel(monitor[ConfigKey.NAME])
: getMonitorDisabledSuccessLabel(monitor[ConfigKey.NAME])}
</p>
),
toastLifeTimeMs: 3000,
});
setRefresh(true);
}
}, [status]); // eslint-disable-line react-hooks/exhaustive-deps

const enabled = isEnabled ?? monitor[ConfigKey.ENABLED];
const isLoading = status === FETCH_STATUS.LOADING;

const handleEnabledChange = (event: EuiSwitchEvent) => {
const checked = event.target.checked;
setIsEnabled(checked);
};

return (
<div css={{ position: 'relative' }} aria-busy={isLoading}>
<EuiSwitch
checked={enabled}
disabled={isLoading}
showLabel={false}
label={enabled ? DISABLE_MONITOR_LABEL : ENABLE_MONITOR_LABEL}
title={enabled ? DISABLE_MONITOR_LABEL : ENABLE_MONITOR_LABEL}
data-test-subj="syntheticsIsMonitorEnabled"
onChange={handleEnabledChange}
/>
{isLoading ? (
<EuiProgress
css={{ position: 'absolute', left: 0, bottom: -4, width: '100%', height: 2 }}
size="xs"
color="primary"
/>
) : null}
</div>
);
};

const ENABLE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.enableMonitorLabel', {
defaultMessage: 'Enable monitor',
});

const DISABLE_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.disableMonitorLabel', {
defaultMessage: 'Disable monitor',
});

const getMonitorEnabledSuccessLabel = (name: string) =>
i18n.translate('xpack.uptime.monitorManagement.monitorEnabledSuccessMessage', {
defaultMessage: 'Monitor {name} enabled successfully.',
values: { name },
});

const getMonitorDisabledSuccessLabel = (name: string) =>
i18n.translate('xpack.uptime.monitorManagement.monitorDisabledSuccessMessage', {
defaultMessage: 'Monitor {name} disabled successfully.',
values: { name },
});

const getMonitorEnabledUpdateFailureMessage = (name: string) =>
i18n.translate('xpack.uptime.monitorManagement.monitorEnabledUpdateFailureMessage', {
defaultMessage: 'Unable to update monitor {name}.',
values: { name },
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import React, { useContext, useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiPanel, EuiSpacer, EuiLink } from '@elastic/eui';
import { SyntheticsMonitorSavedObject } from '../../../../common/types';
import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management';
import { MonitorFields } from '../../../../common/runtime_types';
import { MonitorFields, SyntheticsMonitor } from '../../../../common/runtime_types';
import { UptimeSettingsContext } from '../../../contexts';
import { Actions } from './actions';
import { MonitorLocations } from './monitor_locations';
import { MonitorTags } from './tags';
import { MonitorEnabled } from './monitor_enabled';
import * as labels from '../../overview/monitor_list/translations';

interface Props {
Expand All @@ -32,7 +34,8 @@ export const MonitorManagementList = ({
setPageSize,
setPageIndex,
}: Props) => {
const { monitors, total, perPage, page: pageIndex } = list as MonitorManagementListState['list'];
const { total, perPage, page: pageIndex } = list as MonitorManagementListState['list'];
const monitors = list.monitors as SyntheticsMonitorSavedObject[];
const { basePath } = useContext(UptimeSettingsContext);

const pagination = useMemo(
Expand Down Expand Up @@ -84,23 +87,23 @@ export const MonitorManagementList = ({
name: i18n.translate('xpack.uptime.monitorManagement.monitorList.monitorType', {
defaultMessage: 'Monitor type',
}),
render: ({ type }: Partial<MonitorFields>) => type,
render: ({ type }: SyntheticsMonitor) => type,
},
{
align: 'left' as const,
field: 'attributes',
name: i18n.translate('xpack.uptime.monitorManagement.monitorList.tags', {
defaultMessage: 'Tags',
}),
render: ({ tags }: Partial<MonitorFields>) => (tags ? <MonitorTags tags={tags} /> : null),
render: ({ tags }: SyntheticsMonitor) => (tags ? <MonitorTags tags={tags} /> : null),
},
{
align: 'left' as const,
field: 'attributes',
name: i18n.translate('xpack.uptime.monitorManagement.monitorList.locations', {
defaultMessage: 'Locations',
}),
render: ({ locations }: Partial<MonitorFields>) =>
render: ({ locations }: SyntheticsMonitor) =>
locations ? <MonitorLocations locations={locations} /> : null,
},
{
Expand All @@ -109,18 +112,27 @@ export const MonitorManagementList = ({
name: i18n.translate('xpack.uptime.monitorManagement.monitorList.schedule', {
defaultMessage: 'Schedule',
}),
render: ({ schedule }: Partial<MonitorFields>) =>
`@every ${schedule?.number}${schedule?.unit}`,
render: ({ schedule }: SyntheticsMonitor) => `@every ${schedule?.number}${schedule?.unit}`,
},
{
align: 'left' as const,
field: 'attributes',
name: i18n.translate('xpack.uptime.monitorManagement.monitorList.URL', {
defaultMessage: 'URL',
}),
render: (attributes: Partial<MonitorFields>) => attributes.urls || attributes.hosts,
render: (attributes: MonitorFields) => attributes.urls || attributes.hosts,
truncateText: true,
},
{
align: 'left' as const,
field: 'attributes',
name: i18n.translate('xpack.uptime.monitorManagement.monitorList.enabled', {
defaultMessage: 'Enabled',
}),
render: (attributes: SyntheticsMonitor, record: SyntheticsMonitorSavedObject) => (
<MonitorEnabled id={record.id} monitor={attributes} setRefresh={setRefresh} />
),
},
{
align: 'left' as const,
field: 'id',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const MonitorManagementPage: React.FC = () => {
useEffect(() => {
if (refresh) {
dispatch(getMonitors({ page: pageIndex, perPage: pageSize }));
setRefresh(false);
setRefresh(false); // TODO: avoid extra re-rendering when `refresh` turn to false (pass down the handler instead)
}
}, [dispatch, refresh, pageIndex, pageSize]);

Expand Down
4 changes: 1 addition & 3 deletions x-pack/plugins/uptime/public/state/api/monitor_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,20 @@ import {
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { apiService } from './utils';

// TODO: Type the return type from runtime types
export const setMonitor = async ({
monitor,
id,
}: {
monitor: SyntheticsMonitor;
id?: string;
}): Promise<void> => {
}): Promise<SyntheticsMonitorSavedObject> => {
if (id) {
return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor);
} else {
return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor);
}
};

// TODO, change to monitor runtime type
export const getMonitor = async ({ id }: { id: string }): Promise<SyntheticsMonitorSavedObject> => {
return await apiService.get(`${API_URLS.SYNTHETICS_MONITORS}/${id}`);
};
Expand Down

0 comments on commit 786f416

Please sign in to comment.