mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Uptime monitor management] Handle duplicate monitor name (#124005)
This commit is contained in:
parent
322c2bdb07
commit
268c896315
14 changed files with 292 additions and 13 deletions
|
@ -10,6 +10,8 @@ import * as t from 'io-ts';
|
|||
export const FetchMonitorManagementListQueryArgsType = t.partial({
|
||||
page: t.number,
|
||||
perPage: t.number,
|
||||
search: t.string,
|
||||
searchFields: t.array(t.string),
|
||||
});
|
||||
|
||||
export type FetchMonitorManagementListQueryArgs = t.TypeOf<
|
||||
|
|
8
x-pack/plugins/uptime/common/types/saved_objects.ts
Normal file
8
x-pack/plugins/uptime/common/types/saved_objects.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const syntheticsMonitorType = 'synthetics-monitor';
|
|
@ -10,3 +10,4 @@ export * from './uptime.journey';
|
|||
export * from './monitor_management.journey';
|
||||
export * from './step_duration.journey';
|
||||
export * from './alerts';
|
||||
export * from './monitor_name.journey';
|
||||
|
|
59
x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts
Normal file
59
x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 { journey, step, expect, before, Page } from '@elastic/synthetics';
|
||||
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
|
||||
import { byTestId } from './utils';
|
||||
|
||||
journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => {
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
step('Go to monitor-management', async () => {
|
||||
await uptime.navigateToMonitorManagement();
|
||||
});
|
||||
|
||||
step('login to Kibana', async () => {
|
||||
await uptime.loginToKibana();
|
||||
const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`);
|
||||
expect(await invalid.isVisible()).toBeFalsy();
|
||||
});
|
||||
|
||||
step(`shows error if name already exists`, async () => {
|
||||
await uptime.clickAddMonitor();
|
||||
await uptime.createBasicMonitorDetails({
|
||||
name: 'Test monitor',
|
||||
locations: ['US Central'],
|
||||
apmServiceName: 'synthetics',
|
||||
});
|
||||
await uptime.fillByTestSubj('syntheticsUrlField', 'https://www.google.com');
|
||||
|
||||
await uptime.assertText({ text: 'Monitor name already exists.' });
|
||||
|
||||
expect(await page.isEnabled(byTestId('monitorTestNowRunBtn'))).toBeFalsy();
|
||||
});
|
||||
|
||||
step(`form becomes valid after change`, async () => {
|
||||
await uptime.createBasicMonitorDetails({
|
||||
name: 'Test monitor 2',
|
||||
locations: ['US Central'],
|
||||
apmServiceName: 'synthetics',
|
||||
});
|
||||
|
||||
expect(await page.isEnabled(byTestId('monitorTestNowRunBtn'))).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ import { esArchiverLoad, esArchiverUnload } from './tasks/es_archiver';
|
|||
|
||||
import './journeys';
|
||||
import { createApmAndObsUsersAndRoles } from '../../apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles';
|
||||
import { importMonitors } from './tasks/import_monitors';
|
||||
|
||||
const listOfJourneys = [
|
||||
'uptime',
|
||||
|
@ -56,6 +57,8 @@ async function playwrightStart(getService: any, headless = true, match?: string)
|
|||
port: config.get('servers.kibana.port'),
|
||||
});
|
||||
|
||||
await importMonitors({ kibanaUrl });
|
||||
|
||||
await createApmAndObsUsersAndRoles({
|
||||
elasticsearch: { username: 'elastic', password: 'changeme' },
|
||||
kibana: { roleSuffix: 'e2e', hostname: kibanaUrl },
|
||||
|
|
50
x-pack/plugins/uptime/e2e/tasks/import_monitors.ts
Normal file
50
x-pack/plugins/uptime/e2e/tasks/import_monitors.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 axios from 'axios';
|
||||
import path from 'path';
|
||||
import FormData from 'form-data';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export const importMonitors = async ({
|
||||
kibanaUrl,
|
||||
username,
|
||||
password,
|
||||
}: {
|
||||
kibanaUrl: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Loading sample monitors');
|
||||
|
||||
const form = new FormData();
|
||||
|
||||
const file = fs.readFileSync(path.join(__dirname, './uptime_monitor.ndjson'));
|
||||
|
||||
form.append('file', file, 'uptime_monitor.ndjson');
|
||||
|
||||
try {
|
||||
axios
|
||||
.request({
|
||||
method: 'post',
|
||||
url: kibanaUrl + '/api/saved_objects/_import?overwrite=true',
|
||||
auth: { username: username ?? 'elastic', password: password ?? 'changeme' },
|
||||
headers: { 'kbn-xsrf': 'true', ...form.getHeaders() },
|
||||
data: form,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.successCount === 2) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully imported 2 monitors');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
};
|
3
x-pack/plugins/uptime/e2e/tasks/uptime_monitor.ndjson
Normal file
3
x-pack/plugins/uptime/e2e/tasks/uptime_monitor.ndjson
Normal file
|
@ -0,0 +1,3 @@
|
|||
{"attributes":{"__ui":{"is_tls_enabled":false,"is_zip_url_tls_enabled":false},"check.request.body":{"type":"text","value":""},"check.request.headers":{},"check.request.method":"GET","check.response.body.negative":[],"check.response.body.positive":[],"check.response.headers":{},"check.response.status":[],"enabled":true,"locations":[{"geo":{"lat":41.25,"lon":-95.86},"id":"us_central","label":"US Central","url":"https://us-central.synthetics.elastic.dev"}],"max_redirects":"0","name":"Test Monitor","password":"4SdnK70F8YLqril8Mh5qVveP","proxy_url":"","response.include_body":"on_error","response.include_headers":true,"schedule":{"number":"3","unit":"m"},"service.name":"","tags":[],"timeout":"16","type":"http","urls":"https://www.google.com","username":""},"coreMigrationVersion":"8.1.0","id":"832b9980-7fba-11ec-b360-25a79ce3f496","references":[],"sort":[1643319958480,20371],"type":"synthetics-monitor","updated_at":"2022-01-27T21:45:58.480Z","version":"WzExOTg3ODYsMl0="}
|
||||
{"attributes":{"__ui":{"is_tls_enabled":false,"is_zip_url_tls_enabled":false},"check.request.body":{"type":"text","value":""},"check.request.headers":{},"check.request.method":"GET","check.response.body.negative":[],"check.response.body.positive":[],"check.response.headers":{},"check.response.status":[],"enabled":true,"locations":[{"geo":{"lat":41.25,"lon":-95.86},"id":"us_central","label":"US Central","url":"https://us-central.synthetics.elastic.dev"}],"max_redirects":"0","name":"Test Monito","password":"4SdnK70F8YLqril8Mh5qVveP","proxy_url":"","response.include_body":"on_error","response.include_headers":true,"schedule":{"number":"3","unit":"m"},"service.name":"","tags":[],"timeout":"16","type":"http","urls":"https://www.google.com","username":""},"coreMigrationVersion":"8.1.0","id":"b28380d0-7fba-11ec-b360-25a79ce3f496","references":[],"sort":[1643320037906,20374],"type":"synthetics-monitor","updated_at":"2022-01-27T21:47:17.906Z","version":"WzExOTg4MDAsMl0="}
|
||||
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]}
|
|
@ -110,6 +110,7 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti
|
|||
iconType="play"
|
||||
onClick={() => onTestNow()}
|
||||
disabled={!isValid}
|
||||
data-test-subj={'monitorTestNowRunBtn'}
|
||||
>
|
||||
{testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL}
|
||||
</EuiButton>
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
|
||||
import { ConfigKey } from '../../../../common/runtime_types';
|
||||
import { Validation } from '../../../../common/types';
|
||||
import { usePolicyConfigContext } from '../../fleet_package/contexts';
|
||||
import { ServiceLocations } from './locations';
|
||||
import { useMonitorName } from './use_monitor_name';
|
||||
|
||||
interface Props {
|
||||
validate: Validation;
|
||||
|
@ -24,6 +26,14 @@ export const MonitorNameAndLocation = ({ validate }: Props) => {
|
|||
[ConfigKey.LOCATIONS]: locations,
|
||||
});
|
||||
|
||||
const [localName, setLocalName] = useState(name);
|
||||
|
||||
const { validName, nameAlreadyExists } = useMonitorName({ search: localName });
|
||||
|
||||
useEffect(() => {
|
||||
setName(validName);
|
||||
}, [setName, validName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -34,22 +44,26 @@ export const MonitorNameAndLocation = ({ validate }: Props) => {
|
|||
/>
|
||||
}
|
||||
fullWidth={true}
|
||||
isInvalid={isNameInvalid}
|
||||
isInvalid={isNameInvalid || nameAlreadyExists}
|
||||
error={
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorManagement.monitorNameFieldError"
|
||||
defaultMessage="Monitor name is required"
|
||||
/>
|
||||
nameAlreadyExists ? (
|
||||
NAME_ALREADY_EXISTS
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorManagement.monitorNameFieldError"
|
||||
defaultMessage="Monitor name is required"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
autoFocus={true}
|
||||
defaultValue={name}
|
||||
required={true}
|
||||
isInvalid={isNameInvalid}
|
||||
isInvalid={isNameInvalid || nameAlreadyExists}
|
||||
fullWidth={true}
|
||||
name="name"
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
onChange={(event) => setLocalName(event.target.value)}
|
||||
data-test-subj="monitorManagementMonitorName"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -61,3 +75,7 @@ export const MonitorNameAndLocation = ({ validate }: Props) => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NAME_ALREADY_EXISTS = i18n.translate('xpack.uptime.monitorManagement.duplicateNameError', {
|
||||
defaultMessage: 'Monitor name already exists.',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { defaultCore, WrappedHelper } from '../../../lib/helper/rtl_helpers';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useMonitorName } from './use_monitor_name';
|
||||
|
||||
import * as hooks from '../../../hooks/use_monitor';
|
||||
|
||||
describe('useMonitorName', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useMonitorName({}), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: '' });
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
monitorNames: { terms: { field: 'synthetics-monitor.attributes.name', size: 10000 } },
|
||||
},
|
||||
perPage: 0,
|
||||
type: 'synthetics-monitor',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns expected results after data', async () => {
|
||||
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
|
||||
aggregations: {
|
||||
monitorNames: {
|
||||
buckets: [{ key: 'Test' }, { key: 'Test 1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMonitorName({ search: 'Test' }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: true, validName: '' });
|
||||
});
|
||||
|
||||
it('returns expected results after data while editing monitor', async () => {
|
||||
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
|
||||
aggregations: {
|
||||
monitorNames: {
|
||||
buckets: [{ key: 'Test' }, { key: 'Test 1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(hooks, 'useMonitorId').mockReturnValue('test-id');
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useMonitorName({ search: 'Test' }), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toStrictEqual({ nameAlreadyExists: false, validName: 'Test' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { useFetcher } from '../../../../../observability/public';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
|
||||
interface AggsResponse {
|
||||
monitorNames: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const useMonitorName = ({ search = '' }: { search?: string }) => {
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
|
||||
const monitorId = useMonitorId();
|
||||
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data } = useFetcher(() => {
|
||||
const aggs = {
|
||||
monitorNames: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.name`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
return savedObjects?.client.find<unknown, typeof aggs>({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 0,
|
||||
aggs,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.aggregations) {
|
||||
const newValues = (data.aggregations as AggsResponse)?.monitorNames.buckets.map(({ key }) =>
|
||||
key.toLowerCase()
|
||||
);
|
||||
if (monitorId && newValues.includes(search.toLowerCase())) {
|
||||
setValues(newValues.filter((val) => val !== search.toLowerCase()));
|
||||
} else {
|
||||
setValues(newValues);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, monitorId]);
|
||||
|
||||
const hasMonitor = Boolean(
|
||||
search && values && values.length > 0 && values?.includes(search.trim().toLowerCase())
|
||||
);
|
||||
|
||||
return { nameAlreadyExists: hasMonitor, validName: hasMonitor ? '' : search };
|
||||
};
|
|
@ -101,8 +101,8 @@ const mockAppUrls: Record<string, string> = {
|
|||
};
|
||||
|
||||
/* default mock core */
|
||||
const defaultCore = coreMock.createStart();
|
||||
const mockCore: () => Partial<CoreStart> = () => {
|
||||
export const defaultCore = coreMock.createStart();
|
||||
export const mockCore: () => Partial<CoreStart> = () => {
|
||||
const core: Partial<CoreStart & ClientPluginsStart & { storage: IStorageWrapper }> = {
|
||||
...defaultCore,
|
||||
application: {
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
import { SyntheticsMonitorSavedObject } from '../../../common/types';
|
||||
import { apiService } from './utils';
|
||||
|
||||
// TODO: Type the return type from runtime types
|
||||
export const setMonitor = async ({
|
||||
monitor,
|
||||
id,
|
||||
|
@ -32,7 +31,6 @@ export const setMonitor = async ({
|
|||
}
|
||||
};
|
||||
|
||||
// TODO, change to monitor runtime type
|
||||
export const getMonitor = async ({ id }: { id: string }): Promise<SyntheticsMonitorSavedObject> => {
|
||||
return await apiService.get(`${API_URLS.SYNTHETICS_MONITORS}/${id}`);
|
||||
};
|
||||
|
|
|
@ -41,10 +41,12 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
query: schema.object({
|
||||
page: schema.maybe(schema.number()),
|
||||
perPage: schema.maybe(schema.number()),
|
||||
search: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ request, savedObjectsClient }): Promise<any> => {
|
||||
const { perPage = 50, page } = request.query;
|
||||
const { perPage = 50, page, search } = request.query;
|
||||
|
||||
// TODO: add query/filtering params
|
||||
const {
|
||||
saved_objects: monitors,
|
||||
|
@ -54,6 +56,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
|
|||
type: syntheticsMonitorType,
|
||||
perPage,
|
||||
page,
|
||||
filter: search ? `${syntheticsMonitorType}.attributes.name: ${search}` : '',
|
||||
});
|
||||
return {
|
||||
...rest,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue