mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics] Add private locations in synthetics settings (#146986)
Fixes https://github.com/elastic/kibana/issues/137486
This commit is contained in:
parent
405eb89f35
commit
802d8eaeb2
52 changed files with 2031 additions and 151 deletions
|
@ -17,6 +17,7 @@ export const PrivateLocationCodec = t.intersection([
|
|||
t.partial({
|
||||
isServiceManaged: t.boolean,
|
||||
isInvalid: t.boolean,
|
||||
tags: t.array(t.string),
|
||||
/* Empty Lat lon was accidentally saved as an empty string instead of undefined or null
|
||||
* Need a migration to fix */
|
||||
geo: t.interface({ lat: t.union([t.string, t.number]), lon: t.union([t.string, t.number]) }),
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
|
||||
export * from './data_view_permissions';
|
||||
export * from './read_only_user';
|
||||
export * from './synthetics';
|
||||
export * from './alerts';
|
||||
export * from './uptime.journey';
|
||||
export * from './step_duration.journey';
|
||||
export * from './read_only_user';
|
||||
export * from './monitor_details.journey';
|
||||
export * from './monitor_name.journey';
|
||||
export * from './monitor_management.journey';
|
||||
|
|
|
@ -12,29 +12,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import uuid from 'uuid';
|
||||
import { journey, step, expect, after, before, Page } from '@elastic/synthetics';
|
||||
import { journey, step, expect, after, Page } from '@elastic/synthetics';
|
||||
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
|
||||
|
||||
journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) => {
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
const name = `Test monitor ${uuid.v4()}`;
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await uptime.enableMonitorManagement(false);
|
||||
});
|
||||
|
||||
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();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step('create basic monitor', async () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { journey, step, expect, before, after, Page } from '@elastic/synthetics';
|
||||
import { journey, step, expect, after, Page } from '@elastic/synthetics';
|
||||
import { byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
|
||||
import { DataStream } from '../../common/runtime_types/monitor_management';
|
||||
|
@ -99,25 +99,13 @@ const createMonitorJourney = ({
|
|||
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
const isRemote = process.env.SYNTHETICS_REMOTE_ENABLED;
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await uptime.navigateToMonitorManagement();
|
||||
await uptime.enableMonitorManagement(false);
|
||||
});
|
||||
|
||||
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();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step(`create ${monitorType} monitor`, async () => {
|
||||
|
@ -168,20 +156,12 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page;
|
|||
apmServiceName: 'service',
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await uptime.enableMonitorManagement(false);
|
||||
});
|
||||
|
||||
step('Go to monitor-management', async () => {
|
||||
await uptime.navigateToMonitorManagement();
|
||||
});
|
||||
|
||||
step('login to Kibana', async () => {
|
||||
await uptime.loginToKibana();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step('Check breadcrumb', async () => {
|
||||
|
@ -243,10 +223,6 @@ journey(
|
|||
}),
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await uptime.navigateToMonitorManagement();
|
||||
await uptime.deleteMonitors();
|
||||
|
@ -254,15 +230,7 @@ journey(
|
|||
});
|
||||
|
||||
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();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
for (const monitorConfig of sortedMonitors) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { journey, step, expect, before, after, Page } from '@elastic/synthetics';
|
||||
import { journey, step, expect, after, Page } from '@elastic/synthetics';
|
||||
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
|
||||
|
||||
journey(
|
||||
|
@ -12,24 +12,12 @@ journey(
|
|||
async ({ page, params }: { page: Page; params: any }) => {
|
||||
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
before(async () => {
|
||||
await uptime.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await uptime.enableMonitorManagement(false);
|
||||
});
|
||||
|
||||
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();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step('check add monitor button', async () => {
|
||||
|
@ -48,20 +36,8 @@ journey(
|
|||
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('editor', 'changeme');
|
||||
const invalid = await page.locator(
|
||||
`text=Username or password is incorrect. Please try again.`
|
||||
);
|
||||
expect(await invalid.isVisible()).toBeFalsy();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step('check add monitor button', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import uuid from 'uuid';
|
||||
import { journey, step, expect, before, Page } from '@elastic/synthetics';
|
||||
import { journey, step, expect, Page } from '@elastic/synthetics';
|
||||
import { byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
|
||||
|
||||
|
@ -22,18 +22,8 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) =>
|
|||
});
|
||||
};
|
||||
|
||||
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();
|
||||
await uptime.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step('create basic monitor', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { before, expect, journey, Page, step } from '@elastic/synthetics';
|
||||
import { expect, journey, Page, step } from '@elastic/synthetics';
|
||||
import { byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { monitorManagementPageProvider } from '../../page_objects/monitor_management';
|
||||
|
||||
|
@ -14,12 +14,8 @@ journey(
|
|||
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();
|
||||
await uptime.navigateToMonitorManagement(false);
|
||||
});
|
||||
|
||||
step('login to Kibana', async () => {
|
||||
|
|
|
@ -5,15 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { journey, step, expect, before } from '@elastic/synthetics';
|
||||
import { waitForLoadingToFinish } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { journey, step, expect } from '@elastic/synthetics';
|
||||
import { loginPageProvider } from '../page_objects/login';
|
||||
|
||||
journey('StepsDuration', async ({ page, params }) => {
|
||||
const login = loginPageProvider({ page });
|
||||
before(async () => {
|
||||
await waitForLoadingToFinish({ page });
|
||||
});
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
dateRangeStart: '2021-11-21T22:06:06.502Z',
|
||||
|
|
|
@ -144,24 +144,15 @@ const createMonitorJourney = ({
|
|||
monitorEditDetails: Array<[string, string]>;
|
||||
}) => {
|
||||
journey(
|
||||
`Synthetics - add monitor - ${monitorName}`,
|
||||
`SyntheticsAddMonitor - ${monitorName}`,
|
||||
async ({ page, params }: { page: Page; params: any }) => {
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
step('Go to monitor management', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
await syntheticsApp.navigateToMonitorManagement(true);
|
||||
});
|
||||
|
||||
step('login to Kibana', async () => {
|
||||
await syntheticsApp.loginToKibana();
|
||||
const invalid = await page.locator(
|
||||
`text=Username or password is incorrect. Please try again.`
|
||||
);
|
||||
expect(await invalid.isVisible()).toBeFalsy();
|
||||
});
|
||||
|
||||
step('Ensure all montiors are deleted', async () => {
|
||||
await syntheticsApp.navigateToMonitorManagement();
|
||||
step('Ensure all monitors are deleted', async () => {
|
||||
await syntheticsApp.waitForLoadingToFinish();
|
||||
const isSuccessful = await syntheticsApp.deleteMonitors();
|
||||
expect(isSuccessful).toBeTruthy();
|
||||
|
|
|
@ -12,3 +12,4 @@ export * from './management_list.journey';
|
|||
export * from './overview_sorting.journey';
|
||||
export * from './overview_scrolling.journey';
|
||||
export * from './overview_search.journey';
|
||||
export * from './private_locations.journey';
|
||||
|
|
|
@ -20,9 +20,11 @@ journey('Overview Scrolling', async ({ page, params }) => {
|
|||
await enableMonitorManagedViaApi(params.kibanaUrl);
|
||||
await cleanTestMonitors(params);
|
||||
|
||||
const allPromises = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await addTestMonitor(params.kibanaUrl, `test monitor ${i}`);
|
||||
allPromises.push(addTestMonitor(params.kibanaUrl, `test monitor ${i}`));
|
||||
}
|
||||
await Promise.all(allPromises);
|
||||
|
||||
await syntheticsApp.waitForLoadingToFinish();
|
||||
});
|
||||
|
|
|
@ -26,22 +26,13 @@ journey('OverviewSorting', async ({ page, params }) => {
|
|||
await addTestMonitor(params.kibanaUrl, testMonitor1);
|
||||
await addTestMonitor(params.kibanaUrl, testMonitor2);
|
||||
await addTestMonitor(params.kibanaUrl, testMonitor3);
|
||||
|
||||
await syntheticsApp.waitForLoadingToFinish();
|
||||
});
|
||||
|
||||
step('Go to monitor-management', async () => {
|
||||
await syntheticsApp.navigateToOverview();
|
||||
});
|
||||
|
||||
step('login to Kibana', async () => {
|
||||
await syntheticsApp.loginToKibana();
|
||||
const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`);
|
||||
expect(await invalid.isVisible()).toBeFalsy();
|
||||
await syntheticsApp.navigateToOverview(true);
|
||||
});
|
||||
|
||||
step('sort alphabetical asc', async () => {
|
||||
await syntheticsApp.navigateToOverview();
|
||||
await page.waitForSelector(`[data-test-subj="syntheticsOverviewGridItem"]`);
|
||||
await page.click('[data-test-subj="syntheticsOverviewSortButton"]');
|
||||
await page.click('button:has-text("Alphabetical")');
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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, before, after, expect } from '@elastic/synthetics';
|
||||
import { byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { waitForLoadingToFinish } from '@kbn/ux-plugin/e2e/journeys/utils';
|
||||
import {
|
||||
addTestMonitor,
|
||||
cleanPrivateLocations,
|
||||
cleanTestMonitors,
|
||||
getPrivateLocations,
|
||||
} from './services/add_monitor';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app';
|
||||
|
||||
journey(`PrivateLocationsSettings`, async ({ page, params }) => {
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
page.setDefaultTimeout(2 * 30000);
|
||||
|
||||
before(async () => {
|
||||
await cleanPrivateLocations(params);
|
||||
await cleanTestMonitors(params);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanPrivateLocations(params);
|
||||
await cleanTestMonitors(params);
|
||||
});
|
||||
|
||||
step('Go to Settings page', async () => {
|
||||
await syntheticsApp.navigateToSettings(true);
|
||||
});
|
||||
|
||||
step('go to private locations tab', async () => {
|
||||
await page.click('text=Private Locations');
|
||||
});
|
||||
|
||||
step('Click text=Private Locations', async () => {
|
||||
await page.click('text=Private Locations');
|
||||
expect(page.url()).toBe('http://localhost:5620/app/synthetics/settings/private-locations');
|
||||
await page.click('text=No agent policies found');
|
||||
await page.click('text=Create agent policy');
|
||||
expect(page.url()).toBe('http://localhost:5620/app/fleet/policies?create');
|
||||
await page.click('[placeholder="Choose a name"]');
|
||||
await page.fill('[placeholder="Choose a name"]', 'Test fleet policy');
|
||||
await page.click('text=Collect system logs and metrics');
|
||||
await page.click('div[role="dialog"] button:has-text("Create agent policy")');
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
await waitForLoadingToFinish({ page });
|
||||
});
|
||||
step('Go to http://localhost:5620/app/fleet/policies', async () => {
|
||||
await syntheticsApp.navigateToSettings(false);
|
||||
await page.click('text=Private Locations');
|
||||
});
|
||||
step('Click button:has-text("Create location")', async () => {
|
||||
await page.click('button:has-text("Create location")');
|
||||
await page.click('[aria-label="Location name"]');
|
||||
await page.fill('[aria-label="Location name"]', 'Test private');
|
||||
await page.press('[aria-label="Location name"]', 'Tab');
|
||||
await page.click('[aria-label="Select agent policy"]');
|
||||
await page.click('button[role="option"]:has-text("Test fleet policyAgents: 0")');
|
||||
await page.click('.euiComboBox__inputWrap');
|
||||
await page.fill('[aria-label="Tags"]', 'Basement');
|
||||
await page.press('[aria-label="Tags"]', 'Enter');
|
||||
await page.fill('[aria-label="Tags"]', 'Area51');
|
||||
await page.press('[aria-label="Tags"]', 'Enter');
|
||||
await page.click('button:has-text("Save")');
|
||||
});
|
||||
let locationId: string;
|
||||
step('Click text=AlertingPrivate LocationsData Retention', async () => {
|
||||
await page.click('text=AlertingPrivate LocationsData Retention');
|
||||
await page.click('h1:has-text("Settings")');
|
||||
|
||||
const privateLocations = await getPrivateLocations(params);
|
||||
|
||||
const locations = privateLocations.attributes.locations;
|
||||
|
||||
expect(locations.length).toBe(1);
|
||||
|
||||
locationId = locations[0].id;
|
||||
|
||||
await addTestMonitor(params.kibanaUrl, 'test-monitor', {
|
||||
locations: [locations[0]],
|
||||
type: 'browser',
|
||||
});
|
||||
});
|
||||
|
||||
step('Click text=1', async () => {
|
||||
await page.click('text=1');
|
||||
await page.click('text=Test private');
|
||||
await page.click('.euiTableCellContent__hoverItem .euiToolTipAnchor');
|
||||
await page.click('button:has-text("Tags")');
|
||||
await page.click('[aria-label="Tags"] >> text=Area51');
|
||||
await page.click(
|
||||
'main div:has-text("Private locations allow you to run monitors from your own premises. They require")'
|
||||
);
|
||||
await page.click('text=Test private');
|
||||
|
||||
await page.click('.euiTableCellContent__hoverItem .euiToolTipAnchor');
|
||||
|
||||
await page.locator(byTestId(`deleteLocation-${locationId}`)).isDisabled();
|
||||
|
||||
const text =
|
||||
'This location cannot be deleted, because it has 1 monitors running. Please remove this location from your monitors before deleting this location.';
|
||||
|
||||
await page.locator(`text=${text}`).isVisible();
|
||||
});
|
||||
|
||||
step('Delete location', async () => {
|
||||
await cleanTestMonitors(params);
|
||||
|
||||
await page.click('text=Data Retention');
|
||||
expect(page.url()).toBe('http://localhost:5620/app/synthetics/settings/data-retention');
|
||||
await page.click('text=Private Locations');
|
||||
expect(page.url()).toBe('http://localhost:5620/app/synthetics/settings/private-locations');
|
||||
await page.click('[aria-label="Delete location"]');
|
||||
await page.click('button:has-text("Delete location")');
|
||||
await page.click('text=Create your first private location');
|
||||
});
|
||||
});
|
|
@ -6,6 +6,10 @@
|
|||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import {
|
||||
privateLocationsSavedObjectId,
|
||||
privateLocationsSavedObjectName,
|
||||
} from '../../../../common/saved_objects/private_locations';
|
||||
|
||||
export const enableMonitorManagedViaApi = async (kibanaUrl: string) => {
|
||||
try {
|
||||
|
@ -25,10 +29,10 @@ export const addTestMonitor = async (
|
|||
params: Record<string, any> = { type: 'browser' }
|
||||
) => {
|
||||
const testData = {
|
||||
locations: [{ id: 'us_central', isServiceManaged: true }],
|
||||
...(params?.type !== 'browser' ? {} : data),
|
||||
...(params || {}),
|
||||
name,
|
||||
locations: [{ id: 'us_central', isServiceManaged: true }],
|
||||
};
|
||||
try {
|
||||
await axios.post(kibanaUrl + '/internal/uptime/service/monitors', testData, {
|
||||
|
@ -41,6 +45,21 @@ export const addTestMonitor = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const getPrivateLocations = async (params: Record<string, any>) => {
|
||||
const getService = params.getService;
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
return await server.savedObjects.get({
|
||||
id: privateLocationsSavedObjectId,
|
||||
type: privateLocationsSavedObjectName,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanTestMonitors = async (params: Record<string, any>) => {
|
||||
const getService = params.getService;
|
||||
const server = getService('kibanaServer');
|
||||
|
@ -53,6 +72,21 @@ export const cleanTestMonitors = async (params: Record<string, any>) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const cleanPrivateLocations = async (params: Record<string, any>) => {
|
||||
const getService = params.getService;
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
await server.savedObjects.clean({ types: [privateLocationsSavedObjectName] });
|
||||
await server.savedObjects.clean({
|
||||
types: ['ingest-agent-policies', 'ingest-package-policies'],
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const data = {
|
||||
type: 'browser',
|
||||
form_monitor_type: 'single',
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { Page } from '@elastic/synthetics';
|
||||
import { waitForLoadingToFinish } from '@kbn/ux-plugin/e2e/journeys/utils';
|
||||
|
||||
export function loginPageProvider({
|
||||
page,
|
||||
|
@ -19,12 +20,18 @@ export function loginPageProvider({
|
|||
}) {
|
||||
return {
|
||||
async waitForLoadingToFinish() {
|
||||
while (true) {
|
||||
if ((await page.$('[data-test-subj=kbnLoadingMessage]')) === null) break;
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
}
|
||||
await waitForLoadingToFinish({ page });
|
||||
},
|
||||
async loginToKibana(usernameT?: 'editor' | 'viewer', passwordT?: string) {
|
||||
try {
|
||||
// Close Monitor Management tour added in 8.2.0
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('xpack.synthetics.monitorManagement.openTour', 'false');
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (isRemote) {
|
||||
await page.click('text="Log in with Elasticsearch"');
|
||||
}
|
||||
|
@ -35,13 +42,15 @@ export function loginPageProvider({
|
|||
|
||||
await page.click('[data-test-subj=loginSubmit]');
|
||||
|
||||
await this.waitForLoadingToFinish();
|
||||
// Close Monitor Management tour added in 8.2.0
|
||||
try {
|
||||
await page.click('[data-test-subj=syntheticsManagementTourDismiss]');
|
||||
while (await page.isVisible('[data-test-subj=loginSubmit]')) {
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
// ignore
|
||||
}
|
||||
|
||||
await waitForLoadingToFinish({ page });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -34,10 +34,13 @@ export function monitorManagementPageProvider({
|
|||
}),
|
||||
...utilsPageProvider({ page }),
|
||||
|
||||
async navigateToMonitorManagement() {
|
||||
async navigateToMonitorManagement(doLogin = false) {
|
||||
await page.goto(monitorManagement, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
if (doLogin) {
|
||||
await this.loginToKibana();
|
||||
}
|
||||
await this.waitForMonitorManagementLoadingToFinish();
|
||||
},
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
const monitorManagement = `${basePath}/app/synthetics/monitors`;
|
||||
const addMonitor = `${basePath}/app/synthetics/add-monitor`;
|
||||
const overview = `${basePath}/app/synthetics`;
|
||||
const settingsPage = `${basePath}/app/synthetics/settings`;
|
||||
|
||||
return {
|
||||
...loginPageProvider({
|
||||
page,
|
||||
|
@ -31,15 +33,21 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
}),
|
||||
...utilsPageProvider({ page }),
|
||||
|
||||
async navigateToMonitorManagement() {
|
||||
async navigateToMonitorManagement(doLogin = false) {
|
||||
await page.goto(monitorManagement, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
if (doLogin) {
|
||||
await this.loginToKibana();
|
||||
}
|
||||
await this.waitForMonitorManagementLoadingToFinish();
|
||||
},
|
||||
|
||||
async navigateToOverview() {
|
||||
async navigateToOverview(doLogin = false) {
|
||||
await page.goto(overview, { waitUntil: 'networkidle' });
|
||||
if (doLogin) {
|
||||
await this.loginToKibana();
|
||||
}
|
||||
},
|
||||
|
||||
async waitForMonitorManagementLoadingToFinish() {
|
||||
|
@ -53,8 +61,19 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
return await this.findByText('Create monitor');
|
||||
},
|
||||
|
||||
async navigateToSettings(doLogin = true) {
|
||||
await page.goto(settingsPage, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
if (doLogin) {
|
||||
await this.loginToKibana();
|
||||
}
|
||||
},
|
||||
|
||||
async navigateToAddMonitor() {
|
||||
await page.goto(addMonitor);
|
||||
await page.goto(addMonitor, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
},
|
||||
|
||||
async ensureIsOnMonitorConfigPage() {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { expect, Page } from '@elastic/synthetics';
|
||||
import { waitForLoadingToFinish } from '@kbn/ux-plugin/e2e/journeys/utils';
|
||||
|
||||
export function utilsPageProvider({ page }: { page: Page }) {
|
||||
return {
|
||||
|
@ -13,10 +14,7 @@ export function utilsPageProvider({ page }: { page: Page }) {
|
|||
},
|
||||
|
||||
async waitForLoadingToFinish() {
|
||||
while (true) {
|
||||
if ((await page.$(this.byTestId('kbnLoadingMessage'))) === null) break;
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
}
|
||||
await waitForLoadingToFinish({ page });
|
||||
},
|
||||
|
||||
async assertText({ text }: { text: string }) {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { EuiText, EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const TableTitle = ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
total,
|
||||
label,
|
||||
}: {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
label: string;
|
||||
}) => {
|
||||
const start = pageIndex * pageSize + 1;
|
||||
const end = Math.min(start + pageSize - 1, total);
|
||||
return (
|
||||
<>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.tableTitle.showing"
|
||||
defaultMessage="Showing {count} of {total} {label}"
|
||||
values={{
|
||||
count: (
|
||||
<strong>
|
||||
{start}-{end}
|
||||
</strong>
|
||||
),
|
||||
total,
|
||||
label: <strong>{label}</strong>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiHorizontalRule margin="none" style={{ height: 2 }} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -245,7 +245,7 @@ function LocationSelect({
|
|||
);
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
export function LoadingState() {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { EuiComboBox, EuiFormRow } from '@elastic/eui';
|
||||
import { Control, Controller, FieldErrors } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
|
||||
export function TagsField({
|
||||
tagsList,
|
||||
control,
|
||||
errors,
|
||||
}: {
|
||||
tagsList: string[];
|
||||
errors: FieldErrors;
|
||||
control: Control<PrivateLocation, any>;
|
||||
}) {
|
||||
return (
|
||||
<EuiFormRow fullWidth label={TAGS_LABEL}>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
aria-label={TAGS_LABEL}
|
||||
placeholder={TAGS_LABEL}
|
||||
isInvalid={!!errors?.tags}
|
||||
selectedOptions={field.value?.map((tag) => ({ label: tag, value: tag })) ?? []}
|
||||
options={tagsList.map((tag) => ({ label: tag, value: tag }))}
|
||||
onCreateOption={(newTag) => {
|
||||
field.onChange([...(field.value ?? []), newTag]);
|
||||
}}
|
||||
{...field}
|
||||
onChange={(selectedTags) => {
|
||||
field.onChange(selectedTags.map((tag) => tag.value));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
export const TAGS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.paramForm.tagsLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
});
|
|
@ -10,12 +10,14 @@ import { EuiPageHeaderProps } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { SYNTHETICS_SETTINGS_ROUTE } from '../../../../../common/constants';
|
||||
|
||||
export type SettingsTabId = 'data-retention' | 'params' | 'alerting' | 'private-locations';
|
||||
|
||||
export const getSettingsPageHeader = (
|
||||
history: ReturnType<typeof useHistory>,
|
||||
syntheticsPath: string
|
||||
): EuiPageHeaderProps => {
|
||||
// Not a component, but it doesn't matter. Hooks are just functions
|
||||
const match = useRouteMatch<{ tabId: string }>(SYNTHETICS_SETTINGS_ROUTE); // eslint-disable-line react-hooks/rules-of-hooks
|
||||
const match = useRouteMatch<{ tabId: SettingsTabId }>(SYNTHETICS_SETTINGS_ROUTE); // eslint-disable-line react-hooks/rules-of-hooks
|
||||
|
||||
if (!match) {
|
||||
return {};
|
||||
|
@ -23,6 +25,10 @@ export const getSettingsPageHeader = (
|
|||
|
||||
const { tabId } = match.params;
|
||||
|
||||
const replaceTab = (newTabId: SettingsTabId) => {
|
||||
return `${syntheticsPath}${SYNTHETICS_SETTINGS_ROUTE.replace(':tabId', newTabId)}`;
|
||||
};
|
||||
|
||||
return {
|
||||
pageTitle: i18n.translate('xpack.synthetics.settingsRoute.pageHeaderTitle', {
|
||||
defaultMessage: 'Settings',
|
||||
|
@ -33,15 +39,22 @@ export const getSettingsPageHeader = (
|
|||
label: i18n.translate('xpack.synthetics.settingsTabs.alerting', {
|
||||
defaultMessage: 'Alerting',
|
||||
}),
|
||||
isSelected: tabId === 'alerting' || !tabId,
|
||||
href: `${syntheticsPath}${SYNTHETICS_SETTINGS_ROUTE.replace(':tabId', 'alerting')}`,
|
||||
isSelected: tabId === 'alerting',
|
||||
href: replaceTab('alerting'),
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.synthetics.settingsTabs.privateLocations', {
|
||||
defaultMessage: 'Private Locations',
|
||||
}),
|
||||
isSelected: tabId === 'private-locations',
|
||||
href: replaceTab('private-locations'),
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.synthetics.settingsTabs.dataRetention', {
|
||||
defaultMessage: 'Data retention',
|
||||
defaultMessage: 'Data Retention',
|
||||
}),
|
||||
isSelected: tabId === 'data-retention',
|
||||
href: `${syntheticsPath}${SYNTHETICS_SETTINGS_ROUTE.replace(':tabId', 'data-retention')}`,
|
||||
href: replaceTab('data-retention'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiFlyout,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { usePrivateLocationPermissions } from './hooks/use_private_location_permission';
|
||||
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { LocationForm } from './location_form';
|
||||
import { NEED_FLEET_READ_AGENT_POLICIES_PERMISSION, NEED_PERMISSIONS } from './translations';
|
||||
|
||||
export const AddLocationFlyout = ({
|
||||
onSubmit,
|
||||
setIsOpen,
|
||||
privateLocations,
|
||||
isLoading,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
onSubmit: (val: PrivateLocation) => void;
|
||||
setIsOpen: (val: boolean) => void;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const form = useFormWrapped({
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
shouldFocusError: true,
|
||||
defaultValues: {
|
||||
label: '',
|
||||
agentPolicyId: '',
|
||||
id: '',
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
concurrentMonitors: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit } = form;
|
||||
|
||||
const { canReadAgentPolicies } = usePrivateLocationPermissions();
|
||||
|
||||
const closeFlyout = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<EuiFlyout onClose={closeFlyout} style={{ width: 540 }}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{ADD_PRIVATE_LOCATION}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{!canReadAgentPolicies && (
|
||||
<EuiCallOut title={NEED_PERMISSIONS} color="warning" iconType="help">
|
||||
<p>{NEED_FLEET_READ_AGENT_POLICIES_PERMISSION}</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
<LocationForm privateLocations={privateLocations} />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
onClick={closeFlyout}
|
||||
flush="left"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{CANCEL_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={handleSubmit(onSubmit)} isLoading={isLoading}>
|
||||
{SAVE_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const ADD_PRIVATE_LOCATION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.createPrivateLocations',
|
||||
{
|
||||
defaultMessage: 'Create private location',
|
||||
}
|
||||
);
|
||||
|
||||
const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.cancelLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
const SAVE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.saveLabel', {
|
||||
defaultMessage: 'Save',
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiButton, EuiEmptyPrompt, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import { LEARN_MORE, READ_DOCS } from './empty_locations';
|
||||
|
||||
export const AgentPolicyNeeded = () => {
|
||||
const { basePath } = useSyntheticsSettingsContext();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
hasBorder
|
||||
title={<h2>{AGENT_POLICY_NEEDED}</h2>}
|
||||
body={<p>{ADD_AGENT_POLICY_DESCRIPTION}</p>}
|
||||
actions={
|
||||
<EuiButton fill href={`${basePath}/app/fleet/policies?create`} color="primary">
|
||||
{CREATE_AGENT_POLICY}
|
||||
</EuiButton>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>{LEARN_MORE}</h3>
|
||||
</EuiTitle>
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href="https://www.elastic.co/guide/en/observability/current/uptime-set-up-choose-agent.html#private-locations"
|
||||
>
|
||||
{READ_DOCS}
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CREATE_AGENT_POLICY = i18n.translate('xpack.synthetics.monitorManagement.createAgentPolicy', {
|
||||
defaultMessage: 'Create agent policy',
|
||||
});
|
||||
|
||||
const AGENT_POLICY_NEEDED = i18n.translate('xpack.synthetics.monitorManagement.agentPolicyNeeded', {
|
||||
defaultMessage: 'No agent policies found',
|
||||
});
|
||||
const ADD_AGENT_POLICY_DESCRIPTION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.addAgentPolicyDesc',
|
||||
{
|
||||
defaultMessage:
|
||||
'Private locations require an Agent policy. In order to add a private location, first you must create an Agent policy in Fleet.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { EuiButtonIcon, EuiConfirmModal, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
|
||||
export const DeleteLocation = ({
|
||||
loading,
|
||||
id,
|
||||
label,
|
||||
locationMonitors,
|
||||
onDelete,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
loading?: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
locationMonitors: Array<{ id: string; count: number }>;
|
||||
}) => {
|
||||
const monCount = locationMonitors?.find((l) => l.id === id)?.count ?? 0;
|
||||
const canDelete = monCount === 0;
|
||||
|
||||
const { canSave } = useSyntheticsSettingsContext();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const deleteModal = (
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('xpack.synthetics.monitorManagement.deleteLocationName', {
|
||||
defaultMessage: 'Delete "{location}"',
|
||||
values: { location: label },
|
||||
})}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
onConfirm={() => onDelete(id)}
|
||||
cancelButtonText={CANCEL_LABEL}
|
||||
confirmButtonText={CONFIRM_LABEL}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
isLoading={loading}
|
||||
>
|
||||
<p>{ARE_YOU_SURE_LABEL}</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModalOpen && deleteModal}
|
||||
<EuiToolTip
|
||||
content={
|
||||
canDelete
|
||||
? DELETE_LABEL
|
||||
: i18n.translate('xpack.synthetics.monitorManagement.cannotDelete.description', {
|
||||
defaultMessage: `This location cannot be deleted, because it has {monCount, number} {monCount, plural,one {monitor} other {monitors}} running.
|
||||
Please remove this location from your monitors before deleting this location.`,
|
||||
values: { monCount },
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`deleteLocation-${id}`}
|
||||
isLoading={loading}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={DELETE_LABEL}
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
isDisabled={!canDelete || !canSave}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DELETE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.deleteLocation', {
|
||||
defaultMessage: 'Delete location',
|
||||
});
|
||||
|
||||
const CONFIRM_LABEL = i18n.translate('xpack.synthetics.monitorManagement.deleteLocationLabel', {
|
||||
defaultMessage: 'Delete location',
|
||||
});
|
||||
|
||||
const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.cancelLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
const ARE_YOU_SURE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.areYouSure', {
|
||||
defaultMessage: 'Are you sure you want to delete this location?',
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt, EuiButton, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setAddingNewPrivateLocation, setManageFlyoutOpen } from '../../../state/private_locations';
|
||||
|
||||
export const EmptyLocations = ({
|
||||
inFlyout = true,
|
||||
setIsAddingNew,
|
||||
disabled,
|
||||
}: {
|
||||
inFlyout?: boolean;
|
||||
disabled?: boolean;
|
||||
setIsAddingNew?: (val: boolean) => void;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
hasBorder
|
||||
title={<h2>{ADD_FIRST_LOCATION}</h2>}
|
||||
titleSize="s"
|
||||
body={
|
||||
<EuiText size="s">
|
||||
{!inFlyout ? FIRST_MONITOR : ''} {START_ADDING_LOCATIONS_DESCRIPTION}
|
||||
</EuiText>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
iconType="plusInCircle"
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
fill
|
||||
onClick={() => {
|
||||
setIsAddingNew?.(true);
|
||||
dispatch(setManageFlyoutOpen(true));
|
||||
dispatch(setAddingNewPrivateLocation(true));
|
||||
}}
|
||||
>
|
||||
{ADD_LOCATION}
|
||||
</EuiButton>
|
||||
}
|
||||
footer={
|
||||
<EuiText size="s">
|
||||
{LEARN_MORE} <PrivateLocationDocsLink />
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PrivateLocationDocsLink = ({ label }: { label?: string }) => (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/observability/current/synthetics-private-location.html"
|
||||
target="_blank"
|
||||
>
|
||||
{label ?? READ_DOCS}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
const FIRST_MONITOR = i18n.translate('xpack.synthetics.monitorManagement.firstLocationMonitor', {
|
||||
defaultMessage: 'In order to create a monitor, you will need to add a location first.',
|
||||
});
|
||||
|
||||
const ADD_FIRST_LOCATION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.createFirstLocation',
|
||||
{
|
||||
defaultMessage: 'Create your first private location',
|
||||
}
|
||||
);
|
||||
|
||||
export const START_ADDING_LOCATIONS_DESCRIPTION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.startAddingLocationsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Private locations allow you to run monitors from your own premises. They require an Elastic agent and Agent policy which you can control and maintain via Fleet.',
|
||||
}
|
||||
);
|
||||
|
||||
const ADD_LOCATION = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
|
||||
defaultMessage: 'Create location',
|
||||
});
|
||||
|
||||
export const READ_DOCS = i18n.translate('xpack.synthetics.monitorManagement.readDocs', {
|
||||
defaultMessage: 'read the docs',
|
||||
});
|
||||
|
||||
export const LEARN_MORE = i18n.translate('xpack.synthetics.monitorManagement.learnMore', {
|
||||
defaultMessage: 'For more information,',
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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';
|
||||
import { defaultCore, WrappedHelper } from '../../../../utils/testing';
|
||||
|
||||
import { useLocationMonitors } from './use_location_monitors';
|
||||
|
||||
describe('useLocationMonitors', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useLocationMonitors(), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ locationMonitors: [], loading: true });
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
locations: {
|
||||
terms: { field: 'synthetics-monitor.attributes.locations.id', size: 10000 },
|
||||
},
|
||||
},
|
||||
perPage: 0,
|
||||
type: 'synthetics-monitor',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns expected results after data', async () => {
|
||||
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
|
||||
aggregations: {
|
||||
locations: {
|
||||
buckets: [
|
||||
{ key: 'Test', doc_count: 5 },
|
||||
{ key: 'Test 1', doc_count: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationMonitors(), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ locationMonitors: [], loading: true });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
loading: false,
|
||||
locationMonitors: [
|
||||
{
|
||||
id: 'Test',
|
||||
count: 5,
|
||||
},
|
||||
{
|
||||
id: 'Test 1',
|
||||
count: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { useMemo } from 'react';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
monitorAttributes,
|
||||
syntheticsMonitorType,
|
||||
} from '../../../../../../../common/types/saved_objects';
|
||||
|
||||
interface AggsResponse {
|
||||
locations: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const useLocationMonitors = () => {
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data, loading } = useFetcher(() => {
|
||||
const aggs = {
|
||||
locations: {
|
||||
terms: {
|
||||
field: `${monitorAttributes}.locations.id`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
return savedObjects?.client.find<unknown, typeof aggs>({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 0,
|
||||
aggs,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
if (data?.aggregations) {
|
||||
const newValues = (data.aggregations as AggsResponse)?.locations.buckets.map(
|
||||
({ key, doc_count: count }) => ({ id: key, count })
|
||||
);
|
||||
|
||||
return { locationMonitors: newValues, loading };
|
||||
}
|
||||
return { locationMonitors: [], loading };
|
||||
}, [data, loading]);
|
||||
};
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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';
|
||||
import { defaultCore, WrappedHelper } from '../../../../utils/testing';
|
||||
|
||||
import { useLocationsAPI } from './use_locations_api';
|
||||
|
||||
describe('useLocationsAPI', () => {
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useLocationsAPI(), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
loading: true,
|
||||
privateLocations: [],
|
||||
})
|
||||
);
|
||||
expect(defaultCore.savedObjects.client.get).toHaveBeenCalledWith(
|
||||
'synthetics-privates-locations',
|
||||
'synthetics-privates-locations-singleton'
|
||||
);
|
||||
});
|
||||
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
|
||||
attributes: {
|
||||
locations: [
|
||||
{
|
||||
id: 'Test',
|
||||
agentPolicyId: 'testPolicy',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
it('returns expected results after data', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI(), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
loading: true,
|
||||
privateLocations: [],
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
loading: false,
|
||||
privateLocations: [
|
||||
{
|
||||
id: 'Test',
|
||||
agentPolicyId: 'testPolicy',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds location on submit', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI(), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.onSubmit({
|
||||
id: 'new',
|
||||
agentPolicyId: 'newPolicy',
|
||||
label: 'new',
|
||||
concurrentMonitors: 1,
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultCore.savedObjects.client.create).toHaveBeenCalledWith(
|
||||
'synthetics-privates-locations',
|
||||
{
|
||||
locations: [
|
||||
{ id: 'Test', agentPolicyId: 'testPolicy' },
|
||||
{
|
||||
concurrentMonitors: 1,
|
||||
id: 'newPolicy',
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
label: 'new',
|
||||
agentPolicyId: 'newPolicy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes location on delete', async () => {
|
||||
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
|
||||
attributes: {
|
||||
locations: [
|
||||
{
|
||||
id: 'Test',
|
||||
agentPolicyId: 'testPolicy',
|
||||
},
|
||||
{
|
||||
id: 'Test1',
|
||||
agentPolicyId: 'testPolicy1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI(), {
|
||||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.onDelete('Test');
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(defaultCore.savedObjects.client.create).toHaveBeenLastCalledWith(
|
||||
'synthetics-privates-locations',
|
||||
{
|
||||
locations: [
|
||||
{
|
||||
id: 'Test1',
|
||||
agentPolicyId: 'testPolicy1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useState } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setAddingNewPrivateLocation } from '../../../../state/private_locations';
|
||||
import {
|
||||
getSyntheticsPrivateLocations,
|
||||
setSyntheticsPrivateLocations,
|
||||
} from '../../../../state/private_locations/api';
|
||||
import { PrivateLocation } from '../../../../../../../common/runtime_types';
|
||||
|
||||
export const useLocationsAPI = () => {
|
||||
const [formData, setFormData] = useState<PrivateLocation>();
|
||||
const [deleteId, setDeleteId] = useState<string>();
|
||||
const [privateLocations, setPrivateLocations] = useState<PrivateLocation[]>([]);
|
||||
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val));
|
||||
|
||||
const { loading: fetchLoading } = useFetcher(async () => {
|
||||
const result = await getSyntheticsPrivateLocations(savedObjects?.client!);
|
||||
setPrivateLocations(result);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const { loading: saveLoading } = useFetcher(async () => {
|
||||
if (privateLocations && formData) {
|
||||
const existingLocations = privateLocations.filter((loc) => loc.id !== formData.agentPolicyId);
|
||||
|
||||
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
|
||||
locations: [...(existingLocations ?? []), { ...formData, id: formData.agentPolicyId }],
|
||||
});
|
||||
setPrivateLocations(result.locations);
|
||||
setFormData(undefined);
|
||||
setIsAddingNew(false);
|
||||
return result;
|
||||
}
|
||||
}, [formData, privateLocations]);
|
||||
|
||||
const onSubmit = (data: PrivateLocation) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
setDeleteId(id);
|
||||
};
|
||||
|
||||
const { loading: deleteLoading } = useFetcher(async () => {
|
||||
if (deleteId) {
|
||||
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
|
||||
locations: (privateLocations ?? []).filter((loc) => loc.id !== deleteId),
|
||||
});
|
||||
setPrivateLocations(result.locations);
|
||||
setDeleteId(undefined);
|
||||
return result;
|
||||
}
|
||||
}, [deleteId, privateLocations]);
|
||||
|
||||
return {
|
||||
formData,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
deleteLoading: Boolean(deleteLoading),
|
||||
loading: Boolean(fetchLoading || saveLoading),
|
||||
privateLocations,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { BrowserFields, ConfigKey } from '../../../../../../../common/runtime_types';
|
||||
import { ClientPluginsStart } from '../../../../../../plugin';
|
||||
|
||||
export function usePrivateLocationPermissions(monitor?: BrowserFields) {
|
||||
const { fleet } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const canSaveIntegrations: boolean = Boolean(fleet?.authz.integrations.writeIntegrationPolicies);
|
||||
const canReadAgentPolicies = Boolean(fleet?.authz.fleet.readAgentPolicies);
|
||||
|
||||
const locations = (monitor as BrowserFields)?.[ConfigKey.LOCATIONS];
|
||||
|
||||
const hasPrivateLocation = locations?.some((location) => !location.isServiceManaged);
|
||||
|
||||
const canUpdatePrivateMonitor = !(hasPrivateLocation && !canSaveIntegrations);
|
||||
|
||||
return { canUpdatePrivateMonitor, canReadAgentPolicies };
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFormContext, useFormState } from 'react-hook-form';
|
||||
import { TagsField } from '../components/tags_field';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { AgentPolicyNeeded } from './agent_policy_needed';
|
||||
import { PolicyHostsField } from './policy_hosts';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const LocationForm = ({
|
||||
privateLocations,
|
||||
}: {
|
||||
onDiscard?: () => void;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const { data } = useSelector(selectAgentPolicies);
|
||||
const { control, register } = useFormContext<PrivateLocation>();
|
||||
const { errors } = useFormState();
|
||||
|
||||
const tagsList = privateLocations.reduce((acc, item) => {
|
||||
const tags = item.tags || [];
|
||||
return [...acc, ...tags];
|
||||
}, [] as string[]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.items.length === 0 && <AgentPolicyNeeded />}
|
||||
<EuiForm component="form" noValidate>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={LOCATION_NAME_LABEL}
|
||||
isInvalid={Boolean(errors?.label)}
|
||||
error={errors?.label?.message}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
aria-label={LOCATION_NAME_LABEL}
|
||||
{...register('label', {
|
||||
required: {
|
||||
value: true,
|
||||
message: NAME_REQUIRED,
|
||||
},
|
||||
validate: (val: string) => {
|
||||
return privateLocations.some((loc) => loc.label === val)
|
||||
? NAME_ALREADY_EXISTS
|
||||
: undefined;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<PolicyHostsField errors={errors} control={control} privateLocations={privateLocations} />
|
||||
<EuiSpacer />
|
||||
<TagsField tagsList={tagsList} control={control} errors={errors} />
|
||||
<EuiSpacer />
|
||||
<EuiCallOut title={AGENT_CALLOUT_TITLE} size="s" style={{ textAlign: 'left' }}>
|
||||
<p>
|
||||
{
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorManagement.agentCallout.content"
|
||||
defaultMessage='If you intend to run "Browser" monitors on this private location, please ensure you are using the {code} Docker container, which contains the dependencies to run these monitors. For more information, {link}.'
|
||||
values={{
|
||||
code: <EuiCode>elastic-agent-complete</EuiCode>,
|
||||
link: (
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href="https://www.elastic.co/guide/en/observability/current/uptime-set-up-choose-agent.html#private-locations"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorManagement.agentCallout.link"
|
||||
defaultMessage="read the docs"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AGENT_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.agentCallout.title',
|
||||
{
|
||||
defaultMessage: 'Requirement',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOCATION_NAME_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.locationName',
|
||||
{
|
||||
defaultMessage: 'Location name',
|
||||
}
|
||||
);
|
||||
|
||||
const NAME_ALREADY_EXISTS = i18n.translate('xpack.synthetics.monitorManagement.alreadyExists', {
|
||||
defaultMessage: 'Location name already exists.',
|
||||
});
|
||||
|
||||
const NAME_REQUIRED = i18n.translate('xpack.synthetics.monitorManagement.nameRequired', {
|
||||
defaultMessage: 'Location name is required',
|
||||
});
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table';
|
||||
import { ViewLocationMonitors } from './view_location_monitors';
|
||||
import { TableTitle } from '../../common/components/table_title';
|
||||
import { TAGS_LABEL } from '../components/tags_field';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import { setAddingNewPrivateLocation } from '../../../state/private_locations';
|
||||
import { PrivateLocationDocsLink, START_ADDING_LOCATIONS_DESCRIPTION } from './empty_locations';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { DeleteLocation } from './delete_location';
|
||||
import { useLocationMonitors } from './hooks/use_location_monitors';
|
||||
import { PolicyName } from './policy_name';
|
||||
import { LOCATION_NAME_LABEL } from './location_form';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
interface ListItem extends PrivateLocation {
|
||||
monitors: number;
|
||||
}
|
||||
|
||||
export const PrivateLocationsTable = ({
|
||||
deleteLoading,
|
||||
onDelete,
|
||||
privateLocations,
|
||||
}: {
|
||||
deleteLoading: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const { locationMonitors, loading } = useLocationMonitors();
|
||||
|
||||
const { canSave } = useSyntheticsSettingsContext();
|
||||
|
||||
const tagsList = privateLocations.reduce((acc, item) => {
|
||||
const tags = item.tags || [];
|
||||
return new Set([...acc, ...tags]);
|
||||
}, new Set<string>());
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'label',
|
||||
name: LOCATION_NAME_LABEL,
|
||||
},
|
||||
{
|
||||
field: 'monitors',
|
||||
name: MONITORS,
|
||||
render: (monitors: number, item: ListItem) => (
|
||||
<ViewLocationMonitors count={monitors} locationName={item.label} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'agentPolicyId',
|
||||
name: AGENT_POLICY_LABEL,
|
||||
render: (agentPolicyId: string) => <PolicyName agentPolicyId={agentPolicyId} />,
|
||||
},
|
||||
{
|
||||
name: TAGS_LABEL,
|
||||
field: 'tags',
|
||||
sortable: true,
|
||||
render: (val: string[]) => {
|
||||
const tags = val ?? [];
|
||||
if (tags.length === 0) {
|
||||
return <EuiText>--</EuiText>;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xs" wrap>
|
||||
{tags.map((tag) => (
|
||||
<EuiFlexItem grow={false} key={tag}>
|
||||
<EuiBadge>{tag}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ACTIONS_LABEL,
|
||||
actions: [
|
||||
{
|
||||
name: DELETE_LOCATION,
|
||||
description: DELETE_LOCATION,
|
||||
render: (item: ListItem) => (
|
||||
<DeleteLocation
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
locationMonitors={locationMonitors}
|
||||
onDelete={onDelete}
|
||||
loading={deleteLoading}
|
||||
/>
|
||||
),
|
||||
isPrimary: true,
|
||||
'data-test-subj': 'action-delete',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const items = privateLocations.map((location) => ({
|
||||
...location,
|
||||
monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0,
|
||||
}));
|
||||
|
||||
const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val));
|
||||
|
||||
const { fleet } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const hasFleetPermissions = Boolean(fleet?.authz.fleet.readAgentPolicies);
|
||||
|
||||
const renderToolRight = () => {
|
||||
return [
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj={'addPrivateLocationButton'}
|
||||
isLoading={loading}
|
||||
disabled={!hasFleetPermissions || !canSave}
|
||||
onClick={() => setIsAddingNew(true)}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{ADD_LABEL}
|
||||
</EuiButton>,
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiText>
|
||||
{START_ADDING_LOCATIONS_DESCRIPTION} <PrivateLocationDocsLink label={LEARN_MORE} />
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiInMemoryTable<ListItem>
|
||||
itemId={'id'}
|
||||
tableLayout="auto"
|
||||
tableCaption={PRIVATE_LOCATIONS}
|
||||
items={items}
|
||||
columns={columns}
|
||||
childrenBetween={
|
||||
<TableTitle
|
||||
total={items.length}
|
||||
label={PRIVATE_LOCATIONS}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
pageSize,
|
||||
pageIndex,
|
||||
}}
|
||||
onTableChange={({ page }: Criteria<any>) => {
|
||||
setPageIndex(page?.index ?? 0);
|
||||
setPageSize(page?.size ?? 10);
|
||||
}}
|
||||
search={{
|
||||
toolsRight: renderToolRight(),
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'tags',
|
||||
name: TAGS_LABEL,
|
||||
multiSelect: true,
|
||||
options: [...tagsList].map((tag) => ({
|
||||
value: tag,
|
||||
name: tag,
|
||||
view: tag,
|
||||
})),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PRIVATE_LOCATIONS = i18n.translate('xpack.synthetics.monitorManagement.privateLocations', {
|
||||
defaultMessage: 'Private locations',
|
||||
});
|
||||
|
||||
const ACTIONS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
||||
export const MONITORS = i18n.translate('xpack.synthetics.monitorManagement.monitors', {
|
||||
defaultMessage: 'Monitors',
|
||||
});
|
||||
|
||||
export const AGENT_POLICY_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agentPolicy', {
|
||||
defaultMessage: 'Agent Policy',
|
||||
});
|
||||
|
||||
const DELETE_LOCATION = i18n.translate(
|
||||
'xpack.synthetics.settingsRoute.privateLocations.deleteLabel',
|
||||
{
|
||||
defaultMessage: 'Delete private location',
|
||||
}
|
||||
);
|
||||
|
||||
const ADD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
|
||||
defaultMessage: 'Create location',
|
||||
});
|
||||
|
||||
export const LEARN_MORE = i18n.translate('xpack.synthetics.privateLocations.learnMore.label', {
|
||||
defaultMessage: 'Learn more.',
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { AgentPolicyNeeded } from './agent_policy_needed';
|
||||
import { EmptyLocations } from './empty_locations';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const ManageEmptyState: FC<{
|
||||
privateLocations: PrivateLocation[];
|
||||
hasFleetPermissions: boolean;
|
||||
setIsAddingNew: (val: boolean) => void;
|
||||
}> = ({ children, privateLocations, setIsAddingNew, hasFleetPermissions }) => {
|
||||
const { data: agentPolicies } = useSelector(selectAgentPolicies);
|
||||
|
||||
if (agentPolicies?.total === 0) {
|
||||
return <AgentPolicyNeeded />;
|
||||
}
|
||||
|
||||
if (privateLocations.length === 0) {
|
||||
return <EmptyLocations setIsAddingNew={setIsAddingNew} disabled={!hasFleetPermissions} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout';
|
||||
import { PrivateLocationsTable } from './locations_table';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
import { ManageEmptyState } from './manage_empty_state';
|
||||
import { AddLocationFlyout } from './add_location_flyout';
|
||||
import { useLocationsAPI } from './hooks/use_locations_api';
|
||||
import {
|
||||
getAgentPoliciesAction,
|
||||
selectAddingNewPrivateLocation,
|
||||
setAddingNewPrivateLocation,
|
||||
} from '../../../state/private_locations';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { getServiceLocations } from '../../../state';
|
||||
import { NEED_FLEET_READ_AGENT_POLICIES_PERMISSION, NEED_PERMISSIONS } from './translations';
|
||||
|
||||
export const ManagePrivateLocations = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isAddingNew = useSelector(selectAddingNewPrivateLocation);
|
||||
|
||||
const setIsAddingNew = (val: boolean) => dispatch(setAddingNewPrivateLocation(val));
|
||||
|
||||
const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = useLocationsAPI();
|
||||
|
||||
const { fleet } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const hasFleetPermissions = Boolean(fleet?.authz.fleet.readAgentPolicies);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAgentPoliciesAction.get());
|
||||
dispatch(getServiceLocations());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = (formData: PrivateLocation) => {
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<ManageEmptyState
|
||||
privateLocations={privateLocations}
|
||||
setIsAddingNew={setIsAddingNew}
|
||||
hasFleetPermissions={hasFleetPermissions}
|
||||
>
|
||||
<PrivateLocationsTable
|
||||
privateLocations={privateLocations}
|
||||
onDelete={onDelete}
|
||||
deleteLoading={deleteLoading}
|
||||
/>
|
||||
</ManageEmptyState>
|
||||
)}
|
||||
{!hasFleetPermissions && (
|
||||
<EuiCallOut title={NEED_PERMISSIONS} color="warning" iconType="help">
|
||||
<p>{NEED_FLEET_READ_AGENT_POLICIES_PERMISSION}</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
{isAddingNew ? (
|
||||
<AddLocationFlyout
|
||||
setIsOpen={setIsAddingNew}
|
||||
onSubmit={handleSubmit}
|
||||
privateLocations={privateLocations}
|
||||
isLoading={loading}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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 { Controller, FieldErrors, Control } from 'react-hook-form';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiHealth,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const PolicyHostsField = ({
|
||||
errors,
|
||||
control,
|
||||
privateLocations,
|
||||
}: {
|
||||
errors: FieldErrors;
|
||||
control: Control<PrivateLocation, any>;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const { data } = useSelector(selectAgentPolicies);
|
||||
|
||||
const policyHostsOptions = data?.items.map((item) => {
|
||||
const hasLocation = privateLocations.find((location) => location.agentPolicyId === item.id);
|
||||
return {
|
||||
disabled: Boolean(hasLocation),
|
||||
value: item.id,
|
||||
inputDisplay: (
|
||||
<EuiHealth
|
||||
color={item.status === 'active' ? 'success' : 'warning'}
|
||||
style={{ lineHeight: 'inherit' }}
|
||||
>
|
||||
{item.name}
|
||||
</EuiHealth>
|
||||
),
|
||||
'data-test-subj': item.name,
|
||||
dropdownDisplay: (
|
||||
<EuiToolTip
|
||||
content={
|
||||
hasLocation?.label
|
||||
? i18n.translate('xpack.synthetics.monitorManagement.anotherPrivateLocation', {
|
||||
defaultMessage:
|
||||
'This agent policy is already attached to location: {locationName}.',
|
||||
values: { locationName: hasLocation?.label },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<>
|
||||
<EuiHealth
|
||||
color={item.status === 'active' ? 'success' : 'warning'}
|
||||
style={{ lineHeight: 'inherit' }}
|
||||
>
|
||||
<strong>{item.name}</strong>
|
||||
</EuiHealth>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued" className="eui-textNoWrap">
|
||||
<p>
|
||||
{AGENTS_LABEL} {item.agents}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>{item.description}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={POLICY_HOST_LABEL}
|
||||
helpText={!errors?.agentPolicyId ? SELECT_POLICY_HOSTS_HELP_TEXT : undefined}
|
||||
isInvalid={!!errors?.agentPolicyId}
|
||||
error={SELECT_POLICY_HOSTS}
|
||||
>
|
||||
<Controller
|
||||
name="agentPolicyId"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
aria-label={SELECT_POLICY_HOSTS}
|
||||
placeholder={SELECT_POLICY_HOSTS}
|
||||
valueOfSelected={field.value}
|
||||
itemLayoutAlign="top"
|
||||
popoverProps={{ repositionOnScroll: true }}
|
||||
hasDividers
|
||||
isInvalid={!!errors?.agentPolicyId}
|
||||
options={policyHostsOptions ?? []}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const AGENTS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agentsLabel', {
|
||||
defaultMessage: 'Agents: ',
|
||||
});
|
||||
|
||||
const SELECT_POLICY_HOSTS = i18n.translate('xpack.synthetics.monitorManagement.selectPolicyHost', {
|
||||
defaultMessage: 'Select agent policy',
|
||||
});
|
||||
|
||||
const SELECT_POLICY_HOSTS_HELP_TEXT = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.selectPolicyHost.helpText',
|
||||
{
|
||||
defaultMessage: 'We recommend using a single Elastic agent per agent policy.',
|
||||
}
|
||||
);
|
||||
|
||||
const POLICY_HOST_LABEL = i18n.translate('xpack.synthetics.monitorManagement.policyHost', {
|
||||
defaultMessage: 'Agent policy',
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiLoadingSpinner, EuiText, EuiTextColor } from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import { usePrivateLocationPermissions } from './hooks/use_private_location_permission';
|
||||
import { selectAgentPolicies } from '../../../state/private_locations';
|
||||
|
||||
export const PolicyName = ({ agentPolicyId }: { agentPolicyId: string }) => {
|
||||
const { canReadAgentPolicies } = usePrivateLocationPermissions();
|
||||
|
||||
const { basePath } = useSyntheticsSettingsContext();
|
||||
|
||||
const { data: policies, loading } = useSelector(selectAgentPolicies);
|
||||
|
||||
const policy = policies?.items.find((policyT) => policyT.id === agentPolicyId);
|
||||
|
||||
if (loading) {
|
||||
return <EuiLoadingSpinner size="s" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{canReadAgentPolicies && (
|
||||
<EuiTextColor color="subdued">
|
||||
{policy ? (
|
||||
<EuiLink href={`${basePath}/app/fleet/policies/${agentPolicyId}`}>
|
||||
{policy?.name}
|
||||
</EuiLink>
|
||||
) : (
|
||||
<EuiText color="danger" size="s" className="eui-displayInline">
|
||||
{POLICY_IS_DELETED}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiTextColor>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
||||
|
||||
const POLICY_IS_DELETED = i18n.translate('xpack.synthetics.monitorManagement.deletedPolicy', {
|
||||
defaultMessage: 'Policy is deleted',
|
||||
});
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const NEED_PERMISSIONS = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.needPermissions',
|
||||
{
|
||||
defaultMessage: 'Need permissions',
|
||||
}
|
||||
);
|
||||
|
||||
export const NEED_FLEET_READ_AGENT_POLICIES_PERMISSION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission',
|
||||
{
|
||||
defaultMessage:
|
||||
'You are not authorized to access Fleet. Fleet permissions are required to create new private locations.',
|
||||
}
|
||||
);
|
|
@ -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 React, { useState } from 'react';
|
||||
import { EuiPopover, EuiButtonEmpty, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
export const ViewLocationMonitors = ({
|
||||
count,
|
||||
locationName,
|
||||
}: {
|
||||
count: number;
|
||||
locationName: string;
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const onButtonClick = () => setIsPopoverOpen((prevState) => !prevState);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
const button = <EuiButtonEmpty onClick={onButtonClick}>{count}</EuiButtonEmpty>;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<EuiPopover button={button} isOpen={isPopoverOpen} closePopover={closePopover}>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.monitorManagement.viewMonitors"
|
||||
defaultMessage="Location {name} has {count, number} {count, plural,one {monitor} other {monitors}} running."
|
||||
values={{ count, name: <strong>{locationName}</strong> }}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{count > 0 ? (
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: '/monitors',
|
||||
search: `?locations=${JSON.stringify([locationName])}`,
|
||||
})}
|
||||
>
|
||||
{VIEW_LOCATION_MONITORS}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: '/add-monitor',
|
||||
})}
|
||||
>
|
||||
{CREATE_MONITOR}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
const VIEW_LOCATION_MONITORS = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.viewLocationMonitors',
|
||||
{
|
||||
defaultMessage: 'View location monitors',
|
||||
}
|
||||
);
|
||||
|
||||
const CREATE_MONITOR = i18n.translate('xpack.synthetics.monitorManagement.createLocationMonitors', {
|
||||
defaultMessage: 'Create monitor',
|
||||
});
|
|
@ -7,17 +7,28 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Redirect, useParams } from 'react-router-dom';
|
||||
import { SettingsTabId } from './page_header';
|
||||
import { DataRetentionTab } from './data_retention';
|
||||
import { useSettingsBreadcrumbs } from './use_settings_breadcrumbs';
|
||||
import { ManagePrivateLocations } from './private_locations/manage_private_locations';
|
||||
|
||||
export const SettingsPage = () => {
|
||||
useSettingsBreadcrumbs();
|
||||
|
||||
const { tabId } = useParams<{ tabId: string }>();
|
||||
const { tabId } = useParams<{ tabId: SettingsTabId }>();
|
||||
|
||||
if (!tabId) {
|
||||
return <Redirect to="/settings/alerting" />;
|
||||
}
|
||||
const renderTab = () => {
|
||||
switch (tabId) {
|
||||
case 'private-locations':
|
||||
return <ManagePrivateLocations />;
|
||||
case 'data-retention':
|
||||
return <DataRetentionTab />;
|
||||
case 'alerting':
|
||||
return <div>TODO: Alerting</div>;
|
||||
default:
|
||||
return <Redirect to="/settings/alerting" />;
|
||||
}
|
||||
};
|
||||
|
||||
return <div>{tabId === 'alerting' ? <div>TODO: Alerting</div> : <DataRetentionTab />}</div>;
|
||||
return <div>{renderTab()}</div>;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
I18nStart,
|
||||
} from '@kbn/core/public';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ClientPluginsSetup, ClientPluginsStart } from '../../../plugin';
|
||||
import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../../../common/constants';
|
||||
import { useGetUrlParams } from '../hooks';
|
||||
|
@ -43,6 +44,7 @@ export interface SyntheticsAppProps {
|
|||
}
|
||||
|
||||
export interface SyntheticsSettingsContextValues {
|
||||
canSave: boolean;
|
||||
basePath: string;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
|
@ -69,6 +71,7 @@ const defaultContext: SyntheticsSettingsContextValues = {
|
|||
isInfraAvailable: true,
|
||||
isLogsAvailable: true,
|
||||
isDev: false,
|
||||
canSave: false,
|
||||
};
|
||||
export const SyntheticsSettingsContext = createContext(defaultContext);
|
||||
|
||||
|
@ -81,8 +84,13 @@ export const SyntheticsSettingsContextProvider: React.FC<SyntheticsAppProps> = (
|
|||
|
||||
const { dateRangeStart, dateRangeEnd } = useGetUrlParams();
|
||||
|
||||
const { application } = useKibana().services;
|
||||
|
||||
const canSave = (application?.capabilities.uptime.save ?? false) as boolean;
|
||||
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
canSave,
|
||||
isDev,
|
||||
basePath,
|
||||
isApmAvailable,
|
||||
|
@ -93,6 +101,7 @@ export const SyntheticsSettingsContextProvider: React.FC<SyntheticsAppProps> = (
|
|||
dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END,
|
||||
};
|
||||
}, [
|
||||
canSave,
|
||||
isDev,
|
||||
basePath,
|
||||
isApmAvailable,
|
||||
|
|
|
@ -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 { createAction } from '@reduxjs/toolkit';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
import { AgentPoliciesList } from '.';
|
||||
|
||||
export const getAgentPoliciesAction = createAsyncAction<void, AgentPoliciesList>(
|
||||
'[AGENT POLICIES] GET'
|
||||
);
|
||||
|
||||
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');
|
||||
|
||||
export const setAddingNewPrivateLocation = createAction<boolean>('SET MANAGE FLYOUT ADDING NEW');
|
|
@ -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 { SavedObjectsClientContract } from '@kbn/core/public';
|
||||
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||
import { apiService } from '../../../../utils/api_service/api_service';
|
||||
import { AgentPoliciesList } from '.';
|
||||
import {
|
||||
privateLocationsSavedObjectId,
|
||||
privateLocationsSavedObjectName,
|
||||
} from '../../../../../common/saved_objects/private_locations';
|
||||
|
||||
const FLEET_URLS = {
|
||||
AGENT_POLICIES: '/api/fleet/agent_policies',
|
||||
};
|
||||
|
||||
export const fetchAgentPolicies = async (): Promise<AgentPoliciesList> => {
|
||||
return await apiService.get(
|
||||
FLEET_URLS.AGENT_POLICIES,
|
||||
{
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sortField: 'name',
|
||||
sortOrder: 'asc',
|
||||
full: true,
|
||||
kuery: 'ingest-agent-policies.is_managed : false',
|
||||
},
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
export const setSyntheticsPrivateLocations = async (
|
||||
client: SavedObjectsClientContract,
|
||||
privateLocations: SyntheticsPrivateLocations
|
||||
) => {
|
||||
const result = await client.create(privateLocationsSavedObjectName, privateLocations, {
|
||||
id: privateLocationsSavedObjectId,
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
return result.attributes;
|
||||
};
|
||||
|
||||
export const getSyntheticsPrivateLocations = async (client: SavedObjectsClientContract) => {
|
||||
try {
|
||||
const obj = await client.get<SyntheticsPrivateLocations>(
|
||||
privateLocationsSavedObjectName,
|
||||
privateLocationsSavedObjectId
|
||||
);
|
||||
return obj?.attributes.locations ?? [];
|
||||
} catch (getErr) {
|
||||
return [];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { takeLeading } from 'redux-saga/effects';
|
||||
import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||
import { fetchAgentPolicies } from './api';
|
||||
import { getAgentPoliciesAction } from './actions';
|
||||
|
||||
export function* fetchAgentPoliciesEffect() {
|
||||
yield takeLeading(
|
||||
getAgentPoliciesAction.get,
|
||||
fetchEffectFactory(
|
||||
fetchAgentPolicies,
|
||||
getAgentPoliciesAction.success,
|
||||
getAgentPoliciesAction.fail
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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 { createReducer } from '@reduxjs/toolkit';
|
||||
import { AgentPolicy } from '@kbn/fleet-plugin/common';
|
||||
import { IHttpSerializedFetchError } from '..';
|
||||
import { getAgentPoliciesAction, setAddingNewPrivateLocation } from './actions';
|
||||
|
||||
export interface AgentPoliciesList {
|
||||
items: AgentPolicy[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export interface AgentPoliciesState {
|
||||
data: AgentPoliciesList | null;
|
||||
loading: boolean;
|
||||
error: IHttpSerializedFetchError | null;
|
||||
isManageFlyoutOpen?: boolean;
|
||||
isAddingNewPrivateLocation?: boolean;
|
||||
}
|
||||
|
||||
const initialState: AgentPoliciesState = {
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
isManageFlyoutOpen: false,
|
||||
isAddingNewPrivateLocation: false,
|
||||
};
|
||||
|
||||
export const agentPoliciesReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(getAgentPoliciesAction.get, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(getAgentPoliciesAction.success, (state, action) => {
|
||||
state.data = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(getAgentPoliciesAction.fail, (state, action) => {
|
||||
state.error = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(setAddingNewPrivateLocation, (state, action) => {
|
||||
state.isAddingNewPrivateLocation = action.payload;
|
||||
});
|
||||
});
|
||||
|
||||
export * from './actions';
|
||||
export * from './effects';
|
||||
export * from './selectors';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { createSelector } from 'reselect';
|
||||
import { AppState } from '..';
|
||||
|
||||
const getState = (appState: AppState) => appState.agentPolicies;
|
||||
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
||||
|
||||
export const selectAddingNewPrivateLocation = (state: AppState) =>
|
||||
state.agentPolicies.isAddingNewPrivateLocation ?? false;
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { all, fork } from 'redux-saga/effects';
|
||||
import { fetchAgentPoliciesEffect } from './private_locations';
|
||||
import { fetchNetworkEventsEffect } from './network_events/effects';
|
||||
import { fetchSyntheticsMonitorEffect } from './monitor_details';
|
||||
import { fetchIndexStatusEffect } from './index_status';
|
||||
|
@ -29,5 +30,6 @@ export const rootEffect = function* root(): Generator {
|
|||
fork(fetchOverviewStatusEffect),
|
||||
fork(fetchNetworkEventsEffect),
|
||||
fork(fetchPingStatusesEffect),
|
||||
fork(fetchAgentPoliciesEffect),
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
|
||||
import { agentPoliciesReducer, AgentPoliciesState } from './private_locations';
|
||||
import { networkEventsReducer, NetworkEventsState } from './network_events';
|
||||
import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details';
|
||||
import { uiReducer, UiState } from './ui';
|
||||
|
@ -30,6 +31,7 @@ export interface SyntheticsAppState {
|
|||
browserJourney: BrowserJourneyState;
|
||||
networkEvents: NetworkEventsState;
|
||||
pingStatus: PingStatusState;
|
||||
agentPolicies: AgentPoliciesState;
|
||||
}
|
||||
|
||||
export const rootReducer = combineReducers<SyntheticsAppState>({
|
||||
|
@ -43,4 +45,5 @@ export const rootReducer = combineReducers<SyntheticsAppState>({
|
|||
browserJourney: browserJourneyReducer,
|
||||
networkEvents: networkEventsReducer,
|
||||
pingStatus: pingStatusReducer,
|
||||
agentPolicies: agentPoliciesReducer,
|
||||
});
|
||||
|
|
|
@ -92,6 +92,7 @@ const Application = (props: SyntheticsAppProps) => {
|
|||
observability: startPlugins.observability,
|
||||
cases: startPlugins.cases,
|
||||
spaces: startPlugins.spaces,
|
||||
fleet: startPlugins.fleet,
|
||||
}}
|
||||
>
|
||||
<Router history={appMountParameters.history}>
|
||||
|
|
|
@ -111,6 +111,11 @@ export const mockState: SyntheticsAppState = {
|
|||
browserJourney: getBrowserJourneyMockSlice(),
|
||||
networkEvents: {},
|
||||
pingStatus: getPingStatusesMockSlice(),
|
||||
agentPolicies: {
|
||||
loading: false,
|
||||
error: null,
|
||||
data: null,
|
||||
},
|
||||
};
|
||||
|
||||
function getBrowserJourneyMockSlice() {
|
||||
|
|
|
@ -89,6 +89,7 @@ export interface ClientPluginsStart {
|
|||
docLinks: DocLinksStart;
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
usageCollection: UsageCollectionStart;
|
||||
savedObjects: CoreStart['savedObjects'];
|
||||
}
|
||||
|
||||
export interface UptimePluginServices extends Partial<CoreStart> {
|
||||
|
|
|
@ -10,7 +10,11 @@ import { expect, Page } from '@elastic/synthetics';
|
|||
export async function waitForLoadingToFinish({ page }: { page: Page }) {
|
||||
while (true) {
|
||||
if ((await page.$(byTestId('kbnLoadingMessage'))) === null) break;
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
await page.waitForTimeout(2 * 1000);
|
||||
}
|
||||
while (true) {
|
||||
if ((await page.$(byTestId('globalLoadingIndicator'))) === null) break;
|
||||
await page.waitForTimeout(2 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue