mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
Revert [Synthetics] Edit private locations labels and tags and bulkUpdate (#225273)
…tics (#221515)" This reverts commitb1b8fb0a88
and97941682db
as part of an emergency release to address https://github.com/elastic/kibana/issues/225254 on serverless ## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ...
This commit is contained in:
parent
e2a833785b
commit
0cb63c4ee0
41 changed files with 124 additions and 1079 deletions
|
@ -56864,69 +56864,6 @@ paths:
|
|||
summary: Get a private location
|
||||
tags:
|
||||
- synthetics
|
||||
put:
|
||||
description: |
|
||||
Update an existing private location's label.
|
||||
You must have `all` privileges for the Synthetics and Uptime feature in the Observability section of the Kibana feature privileges.
|
||||
When a private location's label is updated, all monitors using this location will also be updated to maintain data consistency.
|
||||
operationId: put-private-location
|
||||
parameters:
|
||||
- description: The unique identifier of the private location to be updated.
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
putPrivateLocationRequestExample1:
|
||||
description: Update a private location's label.
|
||||
value: |-
|
||||
{
|
||||
"label": "Updated Private Location Name"
|
||||
}
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
label:
|
||||
description: A new label for the private location. Must be at least 1 character long.
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- label
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
putPrivateLocationResponseExample1:
|
||||
value: |-
|
||||
{
|
||||
"label": "Updated Private Location Name",
|
||||
"id": "test-private-location-id",
|
||||
"agentPolicyId": "test-private-location-id",
|
||||
"isServiceManaged": false,
|
||||
"isInvalid": false,
|
||||
"tags": ["private", "testing", "updated"],
|
||||
"geo": {
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
schema:
|
||||
$ref: '#/components/schemas/Synthetics_getPrivateLocation'
|
||||
description: A successful response.
|
||||
'400':
|
||||
description: If the `label` is shorter than 1 character the API will return a 400 Bad Request response with a corresponding error message.
|
||||
'404':
|
||||
description: If the private location with the specified ID does not exist, the API will return a 404 Not Found response.
|
||||
summary: Update a private location
|
||||
tags:
|
||||
- synthetics
|
||||
/api/task_manager/_health:
|
||||
get:
|
||||
description: |
|
||||
|
|
|
@ -53,7 +53,6 @@ import {
|
|||
expectError,
|
||||
createBadRequestErrorPayload,
|
||||
expectUpdateResult,
|
||||
MULTI_NAMESPACE_TYPE,
|
||||
} from '../../test_helpers/repository.test.common';
|
||||
import type { ISavedObjectsSecurityExtension } from '@kbn/core-saved-objects-server';
|
||||
import { savedObjectsExtensionsMock } from '../../mocks/saved_objects_extensions.mock';
|
||||
|
@ -621,74 +620,6 @@ describe('#bulkUpdate', () => {
|
|||
2
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates single namespace objects using the object namespace', async () => {
|
||||
const modifiedObj2 = {
|
||||
...obj2,
|
||||
coreMigrationVersion: '8.0.0',
|
||||
namespace: 'test',
|
||||
};
|
||||
const objects = [modifiedObj2];
|
||||
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true }));
|
||||
|
||||
await bulkUpdateSuccess(client, repository, registry, objects);
|
||||
|
||||
expect(migrator.migrateDocument).toHaveBeenCalledTimes(2);
|
||||
expectMigrationArgs(
|
||||
{
|
||||
id: modifiedObj2.id,
|
||||
namespace: 'test',
|
||||
},
|
||||
true,
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates multiple namespace objects using the object namespaces', async () => {
|
||||
const modifiedObj2 = {
|
||||
...obj2,
|
||||
type: MULTI_NAMESPACE_TYPE,
|
||||
coreMigrationVersion: '8.0.0',
|
||||
namespace: 'test',
|
||||
};
|
||||
const objects = [modifiedObj2];
|
||||
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true }));
|
||||
|
||||
await bulkUpdateSuccess(client, repository, registry, objects);
|
||||
|
||||
expect(migrator.migrateDocument).toHaveBeenCalledTimes(2);
|
||||
expectMigrationArgs(
|
||||
{
|
||||
id: modifiedObj2.id,
|
||||
namespaces: ['test'],
|
||||
},
|
||||
true,
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates namespace agnsostic objects', async () => {
|
||||
const modifiedObj2 = {
|
||||
...obj2,
|
||||
type: NAMESPACE_AGNOSTIC_TYPE,
|
||||
coreMigrationVersion: '8.0.0',
|
||||
namespace: 'test', // specify a namespace, but it should be ignored
|
||||
};
|
||||
const objects = [modifiedObj2];
|
||||
migrator.migrateDocument.mockImplementationOnce((doc) => ({ ...doc, migrated: true }));
|
||||
|
||||
await bulkUpdateSuccess(client, repository, registry, objects);
|
||||
|
||||
expect(migrator.migrateDocument).toHaveBeenCalledTimes(2);
|
||||
expectMigrationArgs(
|
||||
{
|
||||
id: modifiedObj2.id,
|
||||
namespaces: [],
|
||||
},
|
||||
true,
|
||||
2
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns', () => {
|
||||
|
|
|
@ -150,11 +150,17 @@ export const performBulkUpdate = async <T>(
|
|||
};
|
||||
}
|
||||
|
||||
// `objectNamespace` is a namespace string, while `namespace` is a namespace ID.
|
||||
// The object namespace string, if defined, will supersede the operation's namespace ID.
|
||||
const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
|
||||
|
||||
const getNamespaceId = (objectNamespace?: string) =>
|
||||
objectNamespace !== undefined
|
||||
? SavedObjectsUtils.namespaceStringToId(objectNamespace)
|
||||
: namespace;
|
||||
|
||||
const getNamespaceString = (objectNamespace?: string) => objectNamespace ?? namespaceString;
|
||||
|
||||
const bulkGetDocs = validObjects.map(({ value: { type, id, objectNamespace } }) => ({
|
||||
_id: serializer.generateRawId(getNamespaceId(objectNamespace), type, id),
|
||||
_index: commonHelper.getIndexForType(type),
|
||||
|
@ -229,6 +235,7 @@ export const performBulkUpdate = async <T>(
|
|||
mergeAttributes,
|
||||
} = expectedBulkGetResult.value;
|
||||
|
||||
let namespaces: string[] | undefined;
|
||||
const versionProperties = getExpectedVersionProperties(version);
|
||||
const indexFound = bulkGetResponse?.statusCode !== 404;
|
||||
const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined;
|
||||
|
@ -251,18 +258,15 @@ export const performBulkUpdate = async <T>(
|
|||
});
|
||||
}
|
||||
|
||||
let savedObjectNamespace: string | undefined;
|
||||
let savedObjectNamespaces: string[] | undefined;
|
||||
|
||||
if (isMultiNS) {
|
||||
// @ts-expect-error MultiGetHit is incorrectly missing _id, _source
|
||||
savedObjectNamespaces = actualResult!._source.namespaces ?? [
|
||||
namespaces = actualResult!._source.namespaces ?? [
|
||||
// @ts-expect-error MultiGetHit is incorrectly missing _id, _source
|
||||
SavedObjectsUtils.namespaceIdToString(actualResult!._source.namespace),
|
||||
];
|
||||
} else if (registry.isSingleNamespace(type)) {
|
||||
// if `objectNamespace` is undefined, fall back to `options.namespace`
|
||||
savedObjectNamespace = objectNamespace ?? namespace;
|
||||
namespaces = [getNamespaceString(objectNamespace)];
|
||||
}
|
||||
|
||||
const document = getSavedObjectFromSource<T>(
|
||||
|
@ -306,8 +310,8 @@ export const performBulkUpdate = async <T>(
|
|||
...migrated!,
|
||||
id,
|
||||
type,
|
||||
...(savedObjectNamespace && { namespace: savedObjectNamespace }),
|
||||
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
|
||||
namespace,
|
||||
namespaces,
|
||||
attributes: updatedAttributes,
|
||||
updated_at: time,
|
||||
updated_by: updatedBy,
|
||||
|
@ -317,9 +321,6 @@ export const performBulkUpdate = async <T>(
|
|||
migratedUpdatedSavedObjectDoc as SavedObjectSanitizedDoc
|
||||
);
|
||||
|
||||
const namespaces =
|
||||
savedObjectNamespaces ?? (savedObjectNamespace ? [savedObjectNamespace] : []);
|
||||
|
||||
const expectedResult = {
|
||||
type,
|
||||
id,
|
||||
|
|
|
@ -546,13 +546,9 @@ describe('#update', () => {
|
|||
namespace: 'default',
|
||||
});
|
||||
expect(client.index).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.stringMatching(`${type}:${id}`),
|
||||
}),
|
||||
expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }),
|
||||
expect.anything()
|
||||
);
|
||||
// Assert that 'namespace' does not exist at all
|
||||
expect(client.index.mock.calls[0][0]).not.toHaveProperty('namespace');
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id when using agnostic-namespace type`, async () => {
|
||||
|
|
|
@ -716,7 +716,7 @@ export const expectUpdateResult = ({
|
|||
attributes,
|
||||
references,
|
||||
version: mockVersion,
|
||||
namespaces: [],
|
||||
namespaces: ['default'],
|
||||
...mockTimestampFields,
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export const GETTING_STARTED_ROUTE = '/monitors/getting-started';
|
|||
|
||||
export const SETTINGS_ROUTE = '/settings';
|
||||
|
||||
export const PRIVATE_LOCATIONS_ROUTE = '/settings/private-locations';
|
||||
export const PRIVATE_LOCATIOSN_ROUTE = '/settings/private-locations';
|
||||
|
||||
export const SYNTHETICS_SETTINGS_ROUTE = '/settings/:tabId';
|
||||
|
||||
|
|
|
@ -1024,71 +1024,6 @@ paths:
|
|||
},
|
||||
"namespace": "default"
|
||||
}
|
||||
put:
|
||||
summary: Update a private location
|
||||
operationId: put-private-location
|
||||
description: >
|
||||
Update an existing private location's label.
|
||||
|
||||
You must have `all` privileges for the Synthetics and Uptime feature in the Observability section of the Kibana feature privileges.
|
||||
|
||||
When a private location's label is updated, all monitors using this location will also be updated to maintain data consistency.
|
||||
tags:
|
||||
- synthetics
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
description: The unique identifier of the private location to be updated.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- label
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: A new label for the private location. Must be at least 1 character long.
|
||||
examples:
|
||||
putPrivateLocationRequestExample1:
|
||||
description: Update a private location's label.
|
||||
value: |-
|
||||
{
|
||||
"label": "Updated Private Location Name"
|
||||
}
|
||||
responses:
|
||||
'200':
|
||||
description: A successful response.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/getPrivateLocation"
|
||||
examples:
|
||||
putPrivateLocationResponseExample1:
|
||||
value: |-
|
||||
{
|
||||
"label": "Updated Private Location Name",
|
||||
"id": "test-private-location-id",
|
||||
"agentPolicyId": "test-private-location-id",
|
||||
"isServiceManaged": false,
|
||||
"isInvalid": false,
|
||||
"tags": ["private", "testing", "updated"],
|
||||
"geo": {
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
'400':
|
||||
description: If the `label` is shorter than 1 character the API will return a 400 Bad Request response with a corresponding error message.
|
||||
'404':
|
||||
description: If the private location with the specified ID does not exist, the API will return a 404 Not Found response.
|
||||
components:
|
||||
schemas:
|
||||
commonMonitorFields:
|
||||
|
|
|
@ -15,7 +15,6 @@ import { syntheticsAppPageProvider } from '../page_objects/synthetics_app';
|
|||
journey(`PrivateLocationsSettings`, async ({ page, params }) => {
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
|
||||
const services = new SyntheticsServices(params);
|
||||
const NEW_LOCATION_LABEL = 'Updated Test Location';
|
||||
|
||||
page.setDefaultTimeout(2 * 30000);
|
||||
|
||||
|
@ -88,33 +87,11 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => {
|
|||
});
|
||||
});
|
||||
|
||||
step('Edit private location label and verify disabled fields', async () => {
|
||||
// Click on the edit button for the location
|
||||
await page.click('[data-test-subj="action-edit"]');
|
||||
|
||||
// Verify that agent policy selector is disabled
|
||||
expect(await page.locator('[aria-label="Select agent policy"]').isDisabled()).toBe(true);
|
||||
|
||||
// Verify that tags field is disabled
|
||||
expect(await page.locator('[aria-label="Tags"]').isDisabled()).toBe(false);
|
||||
|
||||
// Verify that spaces selector is disabled
|
||||
expect(await page.locator('[aria-label="Spaces "]').isDisabled()).toBe(true);
|
||||
|
||||
await page.fill('[aria-label="Location name"]', NEW_LOCATION_LABEL);
|
||||
|
||||
// Save the changes
|
||||
await page.click('[data-test-subj="syntheticsLocationFlyoutSaveButton"]');
|
||||
|
||||
// Wait for the save to complete and verify the updated label appears in the table
|
||||
await page.waitForSelector(`td:has-text("${NEW_LOCATION_LABEL}")`);
|
||||
});
|
||||
|
||||
step('Integration cannot be edited in Fleet', async () => {
|
||||
await page.goto(`${params.kibanaUrl}/app/integrations/detail/synthetics/policies`);
|
||||
await page.waitForSelector('h1:has-text("Elastic Synthetics")');
|
||||
|
||||
await page.click(`text="test-monitor-${NEW_LOCATION_LABEL}-default"`);
|
||||
await page.click('text="test-monitor-Test private-default"');
|
||||
await page.waitForSelector('h1:has-text("Edit Elastic Synthetics integration")');
|
||||
await page.waitForSelector('text="This package policy is managed by the Synthetics app."');
|
||||
});
|
||||
|
@ -134,14 +111,14 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => {
|
|||
await page.click('h1:has-text("Settings")');
|
||||
await page.click('text=Private Locations');
|
||||
await page.waitForSelector('td:has-text("1")');
|
||||
await page.waitForSelector(`td:has-text("${NEW_LOCATION_LABEL}")`);
|
||||
await page.waitForSelector('td:has-text("Test private")');
|
||||
await page.click('.euiTableRowCell .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=${NEW_LOCATION_LABEL}`);
|
||||
await page.click('text=Test private');
|
||||
|
||||
await page.click('.euiTableRowCell .euiToolTipAnchor');
|
||||
|
||||
|
|
|
@ -21,9 +21,8 @@ describe('GettingStartedPage', () => {
|
|||
loading: false,
|
||||
privateLocations: [],
|
||||
deleteLoading: false,
|
||||
onCreateLocationAPI: jest.fn(),
|
||||
onDeleteLocationAPI: jest.fn(),
|
||||
onEditLocationAPI: jest.fn(),
|
||||
onSubmit: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
createLoading: false,
|
||||
});
|
||||
jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true);
|
||||
|
@ -83,7 +82,7 @@ describe('GettingStartedPage', () => {
|
|||
loading: false,
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: true,
|
||||
isCreatePrivateLocationFlyoutVisible: true,
|
||||
},
|
||||
agentPolicies: {
|
||||
data: [],
|
||||
|
@ -113,7 +112,7 @@ describe('GettingStartedPage', () => {
|
|||
data: [{}],
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: true,
|
||||
isCreatePrivateLocationFlyoutVisible: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -152,7 +151,7 @@ describe('GettingStartedPage', () => {
|
|||
data: [{}],
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: true,
|
||||
isCreatePrivateLocationFlyoutVisible: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -27,14 +27,11 @@ import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_
|
|||
import { getServiceLocations, cleanMonitorListState } from '../../state';
|
||||
import { MONITOR_ADD_ROUTE } from '../../../../../common/constants/ui';
|
||||
import { SimpleMonitorForm } from './simple_monitor_form';
|
||||
import {
|
||||
AddOrEditLocationFlyout,
|
||||
NewLocation,
|
||||
} from '../settings/private_locations/add_or_edit_location_flyout';
|
||||
import { AddLocationFlyout, NewLocation } from '../settings/private_locations/add_location_flyout';
|
||||
import type { ClientPluginsStart } from '../../../../plugin';
|
||||
import { getAgentPoliciesAction, selectAgentPolicies } from '../../state/agent_policies';
|
||||
import { setIsPrivateLocationFlyoutVisible } from '../../state/private_locations/actions';
|
||||
import { selectPrivateLocationFlyoutVisible } from '../../state/private_locations/selectors';
|
||||
import { selectAddingNewPrivateLocation } from '../../state/settings/selectors';
|
||||
import { setIsCreatePrivateLocationFlyoutVisible } from '../../state/private_locations/actions';
|
||||
|
||||
export const GettingStartedPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -130,23 +127,23 @@ export const GettingStartedOnPrem = () => {
|
|||
|
||||
useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview
|
||||
|
||||
const isPrivateLocationFlyoutVisible = useSelector(selectPrivateLocationFlyoutVisible);
|
||||
const isAddingNewLocation = useSelector(selectAddingNewPrivateLocation);
|
||||
|
||||
const setIsFlyoutOpen = useCallback(
|
||||
(val: boolean) => dispatch(setIsPrivateLocationFlyoutVisible(val)),
|
||||
const setIsAddingNewLocation = useCallback(
|
||||
(val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const { onCreateLocationAPI, privateLocations } = usePrivateLocationsAPI();
|
||||
const { onSubmit, privateLocations } = usePrivateLocationsAPI();
|
||||
|
||||
const handleSubmit = (formData: NewLocation) => {
|
||||
onCreateLocationAPI(formData);
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
// make sure flyout is closed when first visiting the page
|
||||
useEffect(() => {
|
||||
setIsFlyoutOpen(false);
|
||||
}, [setIsFlyoutOpen]);
|
||||
setIsAddingNewLocation(false);
|
||||
}, [setIsAddingNewLocation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -166,7 +163,7 @@ export const GettingStartedOnPrem = () => {
|
|||
fill
|
||||
iconType="plusInCircleFilled"
|
||||
data-test-subj="gettingStartedAddLocationButton"
|
||||
onClick={() => setIsFlyoutOpen(true)}
|
||||
onClick={() => setIsAddingNewLocation(true)}
|
||||
>
|
||||
{CREATE_LOCATION_LABEL}
|
||||
</EuiButton>
|
||||
|
@ -176,9 +173,9 @@ export const GettingStartedOnPrem = () => {
|
|||
footer={<GettingStartedLink />}
|
||||
/>
|
||||
|
||||
{isPrivateLocationFlyoutVisible ? (
|
||||
<AddOrEditLocationFlyout
|
||||
onCloseFlyout={() => setIsFlyoutOpen(false)}
|
||||
{isAddingNewLocation ? (
|
||||
<AddLocationFlyout
|
||||
setIsOpen={setIsAddingNewLocation}
|
||||
onSubmit={handleSubmit}
|
||||
privateLocations={privateLocations}
|
||||
/>
|
||||
|
|
|
@ -16,13 +16,9 @@ import { ClientPluginsStart } from '../../../../../plugin';
|
|||
|
||||
interface SpaceSelectorProps {
|
||||
helpText: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const SpaceSelector = <T extends FieldValues>({
|
||||
helpText,
|
||||
isDisabled = false,
|
||||
}: SpaceSelectorProps) => {
|
||||
export const SpaceSelector = <T extends FieldValues>({ helpText }: SpaceSelectorProps) => {
|
||||
const NAMESPACES_NAME = 'spaces' as Path<T>;
|
||||
const { services } = useKibana<ClientPluginsStart>();
|
||||
const [spacesList, setSpacesList] = React.useState<Array<{ id: string; label: string }>>([]);
|
||||
|
@ -65,7 +61,6 @@ export const SpaceSelector = <T extends FieldValues>({
|
|||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<EuiComboBox
|
||||
isDisabled={isDisabled}
|
||||
fullWidth
|
||||
aria-label={SPACES_LABEL}
|
||||
placeholder={SPACES_LABEL}
|
||||
|
|
|
@ -15,12 +15,10 @@ export function TagsField({
|
|||
tagsList,
|
||||
control,
|
||||
errors,
|
||||
isDisabled,
|
||||
}: {
|
||||
tagsList: string[];
|
||||
errors: FieldErrors;
|
||||
control: Control<PrivateLocation, any>;
|
||||
isDisabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<EuiFormRow fullWidth label={TAGS_LABEL}>
|
||||
|
@ -29,7 +27,6 @@ export function TagsField({
|
|||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiComboBox
|
||||
isDisabled={isDisabled}
|
||||
fullWidth
|
||||
aria-label={TAGS_LABEL}
|
||||
placeholder={TAGS_LABEL}
|
||||
|
|
|
@ -35,22 +35,20 @@ import { selectPrivateLocationsState } from '../../../state/private_locations/se
|
|||
export type NewLocation = Omit<PrivateLocation, 'id'>;
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
export const AddOrEditLocationFlyout = ({
|
||||
export const AddLocationFlyout = ({
|
||||
onSubmit,
|
||||
onCloseFlyout,
|
||||
setIsOpen,
|
||||
privateLocations,
|
||||
privateLocationToEdit,
|
||||
}: {
|
||||
onSubmit: (val: NewLocation) => void;
|
||||
onCloseFlyout: () => void;
|
||||
setIsOpen: (val: boolean) => void;
|
||||
privateLocations: PrivateLocation[];
|
||||
privateLocationToEdit?: PrivateLocation;
|
||||
}) => {
|
||||
const form = useFormWrapped({
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
shouldFocusError: true,
|
||||
defaultValues: privateLocationToEdit || {
|
||||
defaultValues: {
|
||||
label: '',
|
||||
agentPolicyId: '',
|
||||
geo: {
|
||||
|
@ -63,7 +61,7 @@ export const AddOrEditLocationFlyout = ({
|
|||
|
||||
const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext();
|
||||
|
||||
const { createLoading, editLoading } = useSelector(selectPrivateLocationsState);
|
||||
const { createLoading } = useSelector(selectPrivateLocationsState);
|
||||
|
||||
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
|
@ -74,35 +72,33 @@ export const AddOrEditLocationFlyout = ({
|
|||
);
|
||||
|
||||
const { handleSubmit } = form;
|
||||
const closeFlyout = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextWrapper>
|
||||
<FormProvider {...form}>
|
||||
<EuiFlyout onClose={onCloseFlyout} css={{ width: 540 }}>
|
||||
<EuiFlyout onClose={closeFlyout} style={{ width: 540 }}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
{privateLocationToEdit !== undefined ? EDIT_PRIVATE_LOCATION : ADD_PRIVATE_LOCATION}
|
||||
</h2>
|
||||
<h2>{ADD_PRIVATE_LOCATION}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<ManageEmptyState privateLocations={privateLocations} showEmptyLocations={false}>
|
||||
<LocationForm
|
||||
privateLocations={privateLocations}
|
||||
privateLocationToEdit={privateLocationToEdit}
|
||||
/>
|
||||
<LocationForm privateLocations={privateLocations} />
|
||||
</ManageEmptyState>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="syntheticsLocationFlyoutCancelButton"
|
||||
data-test-subj="syntheticsAddLocationFlyoutButton"
|
||||
iconType="cross"
|
||||
onClick={onCloseFlyout}
|
||||
onClick={closeFlyout}
|
||||
flush="left"
|
||||
isLoading={createLoading || editLoading}
|
||||
isLoading={createLoading}
|
||||
>
|
||||
{CANCEL_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
|
@ -110,10 +106,10 @@ export const AddOrEditLocationFlyout = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<NoPermissionsTooltip canEditSynthetics={canSave}>
|
||||
<EuiButton
|
||||
data-test-subj="syntheticsLocationFlyoutSaveButton"
|
||||
data-test-subj="syntheticsAddLocationFlyoutButton"
|
||||
fill
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
isLoading={createLoading || editLoading}
|
||||
isLoading={createLoading}
|
||||
isDisabled={!canSave || !canManagePrivateLocations}
|
||||
>
|
||||
{SAVE_LABEL}
|
||||
|
@ -135,13 +131,6 @@ const ADD_PRIVATE_LOCATION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const EDIT_PRIVATE_LOCATION = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.editPrivateLocations',
|
||||
{
|
||||
defaultMessage: 'Edit private location',
|
||||
}
|
||||
);
|
||||
|
||||
const CANCEL_LABEL = i18n.translate('xpack.synthetics.monitorManagement.cancelLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
|
@ -12,19 +12,19 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { NoPermissionsTooltip } from '../../common/components/permissions';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import { PRIVATE_LOCATIONS_ROUTE } from '../../../../../../common/constants';
|
||||
import { PRIVATE_LOCATIOSN_ROUTE } from '../../../../../../common/constants';
|
||||
import {
|
||||
setIsPrivateLocationFlyoutVisible,
|
||||
setIsCreatePrivateLocationFlyoutVisible,
|
||||
setManageFlyoutOpen,
|
||||
} from '../../../state/private_locations/actions';
|
||||
|
||||
export const EmptyLocations = ({
|
||||
inFlyout = true,
|
||||
setIsFlyoutOpen,
|
||||
setIsAddingNew,
|
||||
redirectToSettings,
|
||||
}: {
|
||||
inFlyout?: boolean;
|
||||
setIsFlyoutOpen?: (val: boolean) => void;
|
||||
setIsAddingNew?: (val: boolean) => void;
|
||||
redirectToSettings?: boolean;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -52,7 +52,7 @@ export const EmptyLocations = ({
|
|||
fill
|
||||
isDisabled={!canSave}
|
||||
href={history.createHref({
|
||||
pathname: PRIVATE_LOCATIONS_ROUTE,
|
||||
pathname: PRIVATE_LOCATIOSN_ROUTE,
|
||||
})}
|
||||
>
|
||||
{ADD_LOCATION}
|
||||
|
@ -65,9 +65,9 @@ export const EmptyLocations = ({
|
|||
color="primary"
|
||||
fill
|
||||
onClick={() => {
|
||||
setIsFlyoutOpen?.(true);
|
||||
setIsAddingNew?.(true);
|
||||
dispatch(setManageFlyoutOpen(true));
|
||||
dispatch(setIsPrivateLocationFlyoutVisible(true));
|
||||
dispatch(setIsCreatePrivateLocationFlyoutVisible(true));
|
||||
}}
|
||||
>
|
||||
{ADD_LOCATION}
|
||||
|
|
|
@ -7,12 +7,10 @@
|
|||
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { EditPrivateLocationAttributes } from '../../../../../../../server/routes/settings/private_locations/edit_private_location';
|
||||
import { NewLocation } from '../add_or_edit_location_flyout';
|
||||
import { NewLocation } from '../add_location_flyout';
|
||||
import {
|
||||
createPrivateLocationAction,
|
||||
deletePrivateLocationAction,
|
||||
editPrivateLocationAction,
|
||||
getPrivateLocationsAction,
|
||||
} from '../../../../state/private_locations/actions';
|
||||
import { selectPrivateLocationsState } from '../../../../state/private_locations/selectors';
|
||||
|
@ -32,22 +30,17 @@ export const usePrivateLocationsAPI = () => {
|
|||
}
|
||||
}, [data, dispatch]);
|
||||
|
||||
const onCreateLocationAPI = (newLoc: NewLocation) => {
|
||||
const onSubmit = (newLoc: NewLocation) => {
|
||||
dispatch(createPrivateLocationAction.get(newLoc));
|
||||
};
|
||||
|
||||
const onEditLocationAPI = (locationId: string, newAttributes: EditPrivateLocationAttributes) => {
|
||||
dispatch(editPrivateLocationAction.get({ locationId, newAttributes }));
|
||||
};
|
||||
|
||||
const onDeleteLocationAPI = (id: string) => {
|
||||
const onDelete = (id: string) => {
|
||||
dispatch(deletePrivateLocationAction.get(id));
|
||||
};
|
||||
|
||||
return {
|
||||
onCreateLocationAPI,
|
||||
onEditLocationAPI,
|
||||
onDeleteLocationAPI,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
deleteLoading,
|
||||
loading,
|
||||
createLoading,
|
||||
|
|
|
@ -18,13 +18,7 @@ import { PrivateLocation } from '../../../../../../common/runtime_types';
|
|||
import { AgentPolicyNeeded } from './agent_policy_needed';
|
||||
import { PolicyHostsField } from './policy_hosts';
|
||||
|
||||
export const LocationForm = ({
|
||||
privateLocations,
|
||||
privateLocationToEdit,
|
||||
}: {
|
||||
privateLocations: PrivateLocation[];
|
||||
privateLocationToEdit?: PrivateLocation;
|
||||
}) => {
|
||||
export const LocationForm = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => {
|
||||
const { data } = useSelector(selectAgentPolicies);
|
||||
const { control, register } = useFormContext<PrivateLocation>();
|
||||
const { errors } = useFormState();
|
||||
|
@ -34,8 +28,6 @@ export const LocationForm = ({
|
|||
return [...acc, ...tags];
|
||||
}, [] as string[]);
|
||||
|
||||
const isEditingLocation = privateLocationToEdit !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.length === 0 && <AgentPolicyNeeded />}
|
||||
|
@ -56,8 +48,7 @@ export const LocationForm = ({
|
|||
message: NAME_REQUIRED,
|
||||
},
|
||||
validate: (val: string) => {
|
||||
return privateLocations.some((loc) => loc.label === val) &&
|
||||
val !== privateLocationToEdit?.label
|
||||
return privateLocations.some((loc) => loc.label === val)
|
||||
? NAME_ALREADY_EXISTS
|
||||
: undefined;
|
||||
},
|
||||
|
@ -65,13 +56,13 @@ export const LocationForm = ({
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<PolicyHostsField privateLocations={privateLocations} isDisabled={isEditingLocation} />
|
||||
<PolicyHostsField privateLocations={privateLocations} />
|
||||
<EuiSpacer />
|
||||
<TagsField tagsList={tagsList} control={control} errors={errors} />
|
||||
<EuiSpacer />
|
||||
<BrowserMonitorCallout />
|
||||
<EuiSpacer />
|
||||
<SpaceSelector helpText={LOCATION_HELP_TEXT} isDisabled={isEditingLocation} />
|
||||
<SpaceSelector helpText={LOCATION_HELP_TEXT} />
|
||||
</EuiForm>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -32,7 +31,7 @@ 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 { setIsPrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
|
||||
import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
interface ListItem extends PrivateLocation {
|
||||
|
@ -42,12 +41,10 @@ interface ListItem extends PrivateLocation {
|
|||
export const PrivateLocationsTable = ({
|
||||
deleteLoading,
|
||||
onDelete,
|
||||
onEdit,
|
||||
privateLocations,
|
||||
}: {
|
||||
deleteLoading?: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
onEdit: (privateLocation: PrivateLocation) => void;
|
||||
privateLocations: PrivateLocation[];
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -68,7 +65,7 @@ export const PrivateLocationsTable = ({
|
|||
return new Set([...acc, ...tags]);
|
||||
}, new Set<string>());
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ListItem>> = [
|
||||
const columns = [
|
||||
{
|
||||
field: 'label',
|
||||
name: LOCATION_NAME_LABEL,
|
||||
|
@ -117,15 +114,6 @@ export const PrivateLocationsTable = ({
|
|||
{
|
||||
name: ACTIONS_LABEL,
|
||||
actions: [
|
||||
{
|
||||
name: EDIT_LOCATION,
|
||||
description: EDIT_LOCATION,
|
||||
isPrimary: true,
|
||||
'data-test-subj': 'action-edit',
|
||||
onClick: onEdit,
|
||||
icon: 'pencil',
|
||||
type: 'icon',
|
||||
},
|
||||
{
|
||||
name: DELETE_LOCATION,
|
||||
description: DELETE_LOCATION,
|
||||
|
@ -150,7 +138,7 @@ export const PrivateLocationsTable = ({
|
|||
monitors: locationMonitors?.find((l) => l.id === location.id)?.count ?? 0,
|
||||
}));
|
||||
|
||||
const openFlyout = () => dispatch(setIsPrivateLocationFlyoutVisible(true));
|
||||
const setIsAddingNew = (val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val));
|
||||
|
||||
const renderToolRight = () => {
|
||||
return [
|
||||
|
@ -164,7 +152,7 @@ export const PrivateLocationsTable = ({
|
|||
data-test-subj={'addPrivateLocationButton'}
|
||||
isLoading={loading}
|
||||
disabled={!canSave || !canManagePrivateLocations}
|
||||
onClick={openFlyout}
|
||||
onClick={() => setIsAddingNew(true)}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{ADD_LABEL}
|
||||
|
@ -248,10 +236,6 @@ const DELETE_LOCATION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const EDIT_LOCATION = i18n.translate('xpack.synthetics.settingsRoute.privateLocations.editLabel', {
|
||||
defaultMessage: 'Edit private location',
|
||||
});
|
||||
|
||||
const ADD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
|
||||
defaultMessage: 'Create location',
|
||||
});
|
||||
|
|
|
@ -15,14 +15,14 @@ import { selectAgentPolicies } from '../../../state/agent_policies';
|
|||
export const ManageEmptyState: FC<
|
||||
PropsWithChildren<{
|
||||
privateLocations: PrivateLocation[];
|
||||
setIsFlyoutOpen?: (val: boolean) => void;
|
||||
setIsAddingNew?: (val: boolean) => void;
|
||||
showNeedAgentPolicy?: boolean;
|
||||
showEmptyLocations?: boolean;
|
||||
}>
|
||||
> = ({
|
||||
children,
|
||||
privateLocations,
|
||||
setIsFlyoutOpen,
|
||||
setIsAddingNew,
|
||||
showNeedAgentPolicy = true,
|
||||
showEmptyLocations = true,
|
||||
}) => {
|
||||
|
@ -33,7 +33,7 @@ export const ManageEmptyState: FC<
|
|||
}
|
||||
|
||||
if (privateLocations.length === 0 && showEmptyLocations) {
|
||||
return <EmptyLocations setIsFlyoutOpen={setIsFlyoutOpen} />;
|
||||
return <EmptyLocations setIsAddingNew={setIsAddingNew} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
|
|
@ -28,10 +28,9 @@ describe('<ManagePrivateLocations />', () => {
|
|||
});
|
||||
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
|
||||
loading: false,
|
||||
onCreateLocationAPI: jest.fn(),
|
||||
onEditLocationAPI: jest.fn(),
|
||||
onSubmit: jest.fn(),
|
||||
privateLocations: [],
|
||||
onDeleteLocationAPI: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
deleteLoading: false,
|
||||
createLoading: false,
|
||||
});
|
||||
|
@ -61,7 +60,7 @@ describe('<ManagePrivateLocations />', () => {
|
|||
error: null,
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: false,
|
||||
isCreatePrivateLocationFlyoutVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -96,7 +95,7 @@ describe('<ManagePrivateLocations />', () => {
|
|||
error: null,
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: false,
|
||||
isCreatePrivateLocationFlyoutVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -126,8 +125,7 @@ describe('<ManagePrivateLocations />', () => {
|
|||
|
||||
jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
|
||||
loading: false,
|
||||
onCreateLocationAPI: jest.fn(),
|
||||
onEditLocationAPI: jest.fn(),
|
||||
onSubmit: jest.fn(),
|
||||
privateLocations: [
|
||||
{
|
||||
label: privateLocationName,
|
||||
|
@ -136,7 +134,7 @@ describe('<ManagePrivateLocations />', () => {
|
|||
isServiceManaged: false,
|
||||
},
|
||||
],
|
||||
onDeleteLocationAPI: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
deleteLoading: false,
|
||||
createLoading: false,
|
||||
});
|
||||
|
@ -148,7 +146,7 @@ describe('<ManagePrivateLocations />', () => {
|
|||
error: null,
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: false,
|
||||
isCreatePrivateLocationFlyoutVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,23 +8,15 @@ import React, { useEffect, useCallback, useMemo } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { PrivateLocation } from '../../../../../../common/runtime_types';
|
||||
import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout';
|
||||
import { PrivateLocationsTable } from './locations_table';
|
||||
import { ManageEmptyState } from './manage_empty_state';
|
||||
import { AddOrEditLocationFlyout, NewLocation } from './add_or_edit_location_flyout';
|
||||
import { AddLocationFlyout, NewLocation } from './add_location_flyout';
|
||||
import { usePrivateLocationsAPI } from './hooks/use_locations_api';
|
||||
import {
|
||||
selectPrivateLocationFlyoutVisible,
|
||||
selectPrivateLocationToEdit,
|
||||
} from '../../../state/private_locations/selectors';
|
||||
import { selectAddingNewPrivateLocation } from '../../../state/private_locations/selectors';
|
||||
import { getServiceLocations } from '../../../state';
|
||||
import { getAgentPoliciesAction } from '../../../state/agent_policies';
|
||||
import {
|
||||
setIsPrivateLocationFlyoutVisible as setIsPrivateLocationFlyoutVisible,
|
||||
setPrivateLocationToEdit,
|
||||
} from '../../../state/private_locations/actions';
|
||||
import { setIsCreatePrivateLocationFlyoutVisible } from '../../../state/private_locations/actions';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
@ -41,53 +33,23 @@ export const ManagePrivateLocations = () => {
|
|||
[spacesApi]
|
||||
);
|
||||
|
||||
const isPrivateLocationFlyoutVisible = useSelector(selectPrivateLocationFlyoutVisible);
|
||||
const privateLocationToEdit = useSelector(selectPrivateLocationToEdit);
|
||||
const setIsFlyoutOpen = useCallback(
|
||||
(val: boolean) => dispatch(setIsPrivateLocationFlyoutVisible(val)),
|
||||
const isAddingNew = useSelector(selectAddingNewPrivateLocation);
|
||||
const setIsAddingNew = useCallback(
|
||||
(val: boolean) => dispatch(setIsCreatePrivateLocationFlyoutVisible(val)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const {
|
||||
onCreateLocationAPI,
|
||||
onEditLocationAPI,
|
||||
loading,
|
||||
privateLocations,
|
||||
onDeleteLocationAPI,
|
||||
deleteLoading,
|
||||
} = usePrivateLocationsAPI();
|
||||
const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = usePrivateLocationsAPI();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAgentPoliciesAction.get());
|
||||
dispatch(getServiceLocations());
|
||||
// make sure flyout is closed when first visiting the page
|
||||
dispatch(setIsPrivateLocationFlyoutVisible(false));
|
||||
dispatch(setIsCreatePrivateLocationFlyoutVisible(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = (formData: NewLocation) => {
|
||||
if (privateLocationToEdit) {
|
||||
const isLabelChanged = formData.label !== privateLocationToEdit.label;
|
||||
const areTagsChanged = !isEqual(formData.tags, privateLocationToEdit.tags);
|
||||
if (!isLabelChanged && !areTagsChanged) {
|
||||
onCloseFlyout();
|
||||
} else {
|
||||
onEditLocationAPI(privateLocationToEdit.id, { label: formData.label, tags: formData.tags });
|
||||
}
|
||||
} else {
|
||||
onCreateLocationAPI(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const onEditLocation = (privateLocation: PrivateLocation) => {
|
||||
dispatch(setPrivateLocationToEdit(privateLocation));
|
||||
setIsFlyoutOpen(true);
|
||||
};
|
||||
|
||||
const onCloseFlyout = () => {
|
||||
if (privateLocationToEdit) {
|
||||
dispatch(setPrivateLocationToEdit(undefined));
|
||||
}
|
||||
setIsFlyoutOpen(false);
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -95,22 +57,20 @@ export const ManagePrivateLocations = () => {
|
|||
{loading ? (
|
||||
<LoadingState />
|
||||
) : (
|
||||
<ManageEmptyState privateLocations={privateLocations} setIsFlyoutOpen={setIsFlyoutOpen}>
|
||||
<ManageEmptyState privateLocations={privateLocations} setIsAddingNew={setIsAddingNew}>
|
||||
<PrivateLocationsTable
|
||||
privateLocations={privateLocations}
|
||||
onDelete={onDeleteLocationAPI}
|
||||
onEdit={onEditLocation}
|
||||
onDelete={onDelete}
|
||||
deleteLoading={deleteLoading}
|
||||
/>
|
||||
</ManageEmptyState>
|
||||
)}
|
||||
|
||||
{isPrivateLocationFlyoutVisible ? (
|
||||
<AddOrEditLocationFlyout
|
||||
onCloseFlyout={onCloseFlyout}
|
||||
{isAddingNew ? (
|
||||
<AddLocationFlyout
|
||||
setIsOpen={setIsAddingNew}
|
||||
onSubmit={handleSubmit}
|
||||
privateLocations={privateLocations}
|
||||
privateLocationToEdit={privateLocationToEdit}
|
||||
/>
|
||||
) : null}
|
||||
</SpacesContextProvider>
|
||||
|
|
|
@ -29,13 +29,7 @@ import { selectAgentPolicies } from '../../../state/agent_policies';
|
|||
|
||||
export const AGENT_POLICY_FIELD_NAME = 'agentPolicyId';
|
||||
|
||||
export const PolicyHostsField = ({
|
||||
privateLocations,
|
||||
isDisabled,
|
||||
}: {
|
||||
privateLocations: PrivateLocation[];
|
||||
isDisabled?: boolean;
|
||||
}) => {
|
||||
export const PolicyHostsField = ({ privateLocations }: { privateLocations: PrivateLocation[] }) => {
|
||||
const { data } = useSelector(selectAgentPolicies);
|
||||
const { basePath } = useSyntheticsSettingsContext();
|
||||
|
||||
|
@ -59,7 +53,7 @@ export const PolicyHostsField = ({
|
|||
inputDisplay: (
|
||||
<EuiHealth
|
||||
color={item.status === 'active' ? 'success' : 'warning'}
|
||||
css={{ lineHeight: 'inherit' }}
|
||||
style={{ lineHeight: 'inherit' }}
|
||||
>
|
||||
{item.name}
|
||||
</EuiHealth>
|
||||
|
@ -80,7 +74,7 @@ export const PolicyHostsField = ({
|
|||
<>
|
||||
<EuiHealth
|
||||
color={item.status === 'active' ? 'success' : 'warning'}
|
||||
css={{ lineHeight: 'inherit' }}
|
||||
style={{ lineHeight: 'inherit' }}
|
||||
>
|
||||
<strong>{item.name}</strong>
|
||||
</EuiHealth>
|
||||
|
@ -130,7 +124,6 @@ export const PolicyHostsField = ({
|
|||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<SuperSelect
|
||||
disabled={isDisabled}
|
||||
fullWidth
|
||||
aria-label={SELECT_POLICY_HOSTS}
|
||||
placeholder={SELECT_POLICY_HOSTS}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { NewLocation } from '../../components/settings/private_locations/add_or_edit_location_flyout';
|
||||
import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
|
||||
import { AgentPolicyInfo } from '../../../../../common/types';
|
||||
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||
import { SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
*/
|
||||
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { NewLocation } from '../../components/settings/private_locations/add_or_edit_location_flyout';
|
||||
import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
|
||||
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
import type { EditPrivateLocationAttributes } from '../../../../../server/routes/settings/private_locations/edit_private_location';
|
||||
|
||||
export const getPrivateLocationsAction = createAsyncAction<void, SyntheticsPrivateLocations>(
|
||||
'[PRIVATE LOCATIONS] GET'
|
||||
|
@ -19,24 +18,12 @@ export const createPrivateLocationAction = createAsyncAction<NewLocation, Privat
|
|||
'CREATE PRIVATE LOCATION'
|
||||
);
|
||||
|
||||
export const editPrivateLocationAction = createAsyncAction<
|
||||
{
|
||||
locationId: string;
|
||||
newAttributes: EditPrivateLocationAttributes;
|
||||
},
|
||||
PrivateLocation
|
||||
>('EDIT PRIVATE LOCATION');
|
||||
|
||||
export const deletePrivateLocationAction = createAsyncAction<string, SyntheticsPrivateLocations>(
|
||||
'DELETE PRIVATE LOCATION'
|
||||
);
|
||||
|
||||
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');
|
||||
|
||||
export const setIsPrivateLocationFlyoutVisible = createAction<boolean>(
|
||||
'SET IS CREATE OR EDIT PRIVATE LOCATION FLYOUT VISIBLE'
|
||||
);
|
||||
|
||||
export const setPrivateLocationToEdit = createAction<PrivateLocation | undefined>(
|
||||
'SET PRIVATE LOCATION TO EDIT'
|
||||
export const setIsCreatePrivateLocationFlyoutVisible = createAction<boolean>(
|
||||
'SET IS CREATE PRIVATE LOCATION FLYOUT VISIBLE'
|
||||
);
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EditPrivateLocationAttributes } from '../../../../../server/routes/settings/private_locations/edit_private_location';
|
||||
import { NewLocation } from '../../components/settings/private_locations/add_or_edit_location_flyout';
|
||||
import { NewLocation } from '../../components/settings/private_locations/add_location_flyout';
|
||||
import { AgentPolicyInfo } from '../../../../../common/types';
|
||||
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||
|
@ -24,23 +23,6 @@ export const createSyntheticsPrivateLocation = async (
|
|||
});
|
||||
};
|
||||
|
||||
export const editSyntheticsPrivateLocation = async ({
|
||||
locationId,
|
||||
newAttributes,
|
||||
}: {
|
||||
locationId: string;
|
||||
newAttributes: EditPrivateLocationAttributes;
|
||||
}): Promise<PrivateLocation> => {
|
||||
return apiService.put(
|
||||
`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${locationId}`,
|
||||
newAttributes,
|
||||
undefined,
|
||||
{
|
||||
version: INITIAL_REST_VERSION,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getSyntheticsPrivateLocations = async (): Promise<SyntheticsPrivateLocations> => {
|
||||
return await apiService.get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS, {
|
||||
version: INITIAL_REST_VERSION,
|
||||
|
|
|
@ -11,13 +11,11 @@ import { fetchEffectFactory } from '../utils/fetch_effect';
|
|||
import {
|
||||
createSyntheticsPrivateLocation,
|
||||
deleteSyntheticsPrivateLocation,
|
||||
editSyntheticsPrivateLocation,
|
||||
getSyntheticsPrivateLocations,
|
||||
} from './api';
|
||||
import {
|
||||
createPrivateLocationAction,
|
||||
deletePrivateLocationAction,
|
||||
editPrivateLocationAction,
|
||||
getPrivateLocationsAction,
|
||||
} from './actions';
|
||||
|
||||
|
@ -49,23 +47,6 @@ export function* createPrivateLocationEffect() {
|
|||
);
|
||||
}
|
||||
|
||||
export function* editPrivateLocationEffect() {
|
||||
yield takeLeading(
|
||||
editPrivateLocationAction.get,
|
||||
fetchEffectFactory(
|
||||
editSyntheticsPrivateLocation,
|
||||
editPrivateLocationAction.success,
|
||||
editPrivateLocationAction.fail,
|
||||
i18n.translate('xpack.synthetics.editPrivateLocationSuccess', {
|
||||
defaultMessage: 'Successfully edited private location.',
|
||||
}),
|
||||
i18n.translate('xpack.synthetics.editPrivateLocationFailure', {
|
||||
defaultMessage: 'Failed to edit private location.',
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function* deletePrivateLocationEffect() {
|
||||
yield takeLeading(
|
||||
deletePrivateLocationAction.get,
|
||||
|
@ -80,6 +61,5 @@ export function* deletePrivateLocationEffect() {
|
|||
export const privateLocationsEffects = [
|
||||
fetchPrivateLocationsEffect,
|
||||
createPrivateLocationEffect,
|
||||
editPrivateLocationEffect,
|
||||
deletePrivateLocationEffect,
|
||||
];
|
||||
|
|
|
@ -7,26 +7,19 @@
|
|||
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { PrivateLocation, SyntheticsPrivateLocations } from '../../../../../common/runtime_types';
|
||||
import {
|
||||
createPrivateLocationAction,
|
||||
deletePrivateLocationAction,
|
||||
editPrivateLocationAction,
|
||||
setPrivateLocationToEdit,
|
||||
} from './actions';
|
||||
import { setIsPrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
|
||||
import { createPrivateLocationAction, deletePrivateLocationAction } from './actions';
|
||||
import { setIsCreatePrivateLocationFlyoutVisible, getPrivateLocationsAction } from './actions';
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
||||
export interface PrivateLocationsState {
|
||||
data?: SyntheticsPrivateLocations | null;
|
||||
loading: boolean;
|
||||
createLoading?: boolean;
|
||||
editLoading?: boolean;
|
||||
deleteLoading?: boolean;
|
||||
error: IHttpSerializedFetchError | null;
|
||||
isManageFlyoutOpen?: boolean;
|
||||
isPrivateLocationFlyoutVisible?: boolean;
|
||||
isCreatePrivateLocationFlyoutVisible?: boolean;
|
||||
newLocation?: PrivateLocation;
|
||||
privateLocationToEdit?: PrivateLocation;
|
||||
}
|
||||
|
||||
const initialState: PrivateLocationsState = {
|
||||
|
@ -34,10 +27,8 @@ const initialState: PrivateLocationsState = {
|
|||
loading: false,
|
||||
error: null,
|
||||
isManageFlyoutOpen: false,
|
||||
isPrivateLocationFlyoutVisible: false,
|
||||
isCreatePrivateLocationFlyoutVisible: false,
|
||||
createLoading: false,
|
||||
editLoading: false,
|
||||
privateLocationToEdit: undefined,
|
||||
};
|
||||
|
||||
export const privateLocationsStateReducer = createReducer(initialState, (builder) => {
|
||||
|
@ -60,26 +51,12 @@ export const privateLocationsStateReducer = createReducer(initialState, (builder
|
|||
state.newLocation = action.payload;
|
||||
state.createLoading = false;
|
||||
state.data = null;
|
||||
state.isPrivateLocationFlyoutVisible = false;
|
||||
state.isCreatePrivateLocationFlyoutVisible = false;
|
||||
})
|
||||
.addCase(createPrivateLocationAction.fail, (state, action) => {
|
||||
state.error = action.payload;
|
||||
state.createLoading = false;
|
||||
})
|
||||
.addCase(editPrivateLocationAction.get, (state) => {
|
||||
state.editLoading = true;
|
||||
})
|
||||
.addCase(editPrivateLocationAction.success, (state, action) => {
|
||||
state.editLoading = false;
|
||||
state.privateLocationToEdit = undefined;
|
||||
state.data = null;
|
||||
state.isPrivateLocationFlyoutVisible = false;
|
||||
})
|
||||
.addCase(editPrivateLocationAction.fail, (state, action) => {
|
||||
state.editLoading = false;
|
||||
state.privateLocationToEdit = undefined;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(deletePrivateLocationAction.get, (state) => {
|
||||
state.deleteLoading = true;
|
||||
})
|
||||
|
@ -91,10 +68,7 @@ export const privateLocationsStateReducer = createReducer(initialState, (builder
|
|||
state.error = action.payload;
|
||||
state.deleteLoading = false;
|
||||
})
|
||||
.addCase(setIsPrivateLocationFlyoutVisible, (state, action) => {
|
||||
state.isPrivateLocationFlyoutVisible = action.payload;
|
||||
})
|
||||
.addCase(setPrivateLocationToEdit, (state, action) => {
|
||||
state.privateLocationToEdit = action.payload;
|
||||
.addCase(setIsCreatePrivateLocationFlyoutVisible, (state, action) => {
|
||||
state.isCreatePrivateLocationFlyoutVisible = action.payload;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,11 +11,8 @@ import { AppState } from '..';
|
|||
const getState = (appState: AppState) => appState.privateLocations;
|
||||
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
||||
|
||||
export const selectPrivateLocationFlyoutVisible = (state: AppState) =>
|
||||
state.privateLocations.isPrivateLocationFlyoutVisible ?? false;
|
||||
|
||||
export const selectPrivateLocationToEdit = (state: AppState) =>
|
||||
state.privateLocations.privateLocationToEdit;
|
||||
export const selectAddingNewPrivateLocation = (state: AppState) =>
|
||||
state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
|
||||
|
||||
export const selectPrivateLocationsLoading = (state: AppState) =>
|
||||
state.privateLocations.loading ?? false;
|
||||
|
|
|
@ -14,7 +14,7 @@ const getState = (appState: AppState) => appState.agentPolicies;
|
|||
export const selectAgentPolicies = createSelector(getState, (state) => state);
|
||||
|
||||
export const selectAddingNewPrivateLocation = (state: AppState) =>
|
||||
state.privateLocations.isPrivateLocationFlyoutVisible ?? false;
|
||||
state.privateLocations.isCreatePrivateLocationFlyoutVisible ?? false;
|
||||
|
||||
export const selectLocationMonitors = (state: AppState) => ({
|
||||
locationMonitors: state.dynamicSettings.locationMonitors,
|
||||
|
|
|
@ -116,7 +116,7 @@ export const mockState: SyntheticsAppState = {
|
|||
data: null,
|
||||
},
|
||||
privateLocations: {
|
||||
isPrivateLocationFlyoutVisible: false,
|
||||
isCreatePrivateLocationFlyoutVisible: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
data: [],
|
||||
|
|
|
@ -12,7 +12,6 @@ import { PrivateLocationAttributes } from '../runtime_types/private_locations';
|
|||
import { PrivateLocationObject } from '../routes/settings/private_locations/add_private_location';
|
||||
import { RouteContext } from '../routes/types';
|
||||
import { privateLocationSavedObjectName } from '../../common/saved_objects/private_locations';
|
||||
import { EditPrivateLocationAttributes } from '../routes/settings/private_locations/edit_private_location';
|
||||
|
||||
export class PrivateLocationRepository {
|
||||
internalSOClient: ISavedObjectsRepository;
|
||||
|
@ -34,26 +33,6 @@ export class PrivateLocationRepository {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getPrivateLocation(locationId: string) {
|
||||
const { savedObjectsClient } = this.routeContext;
|
||||
|
||||
return savedObjectsClient.get<PrivateLocationAttributes>(
|
||||
privateLocationSavedObjectName,
|
||||
locationId
|
||||
);
|
||||
}
|
||||
|
||||
async editPrivateLocation(locationId: string, newAttributes: EditPrivateLocationAttributes) {
|
||||
const { savedObjectsClient } = this.routeContext;
|
||||
|
||||
return savedObjectsClient.update<PrivateLocationAttributes>(
|
||||
privateLocationSavedObjectName,
|
||||
locationId,
|
||||
newAttributes
|
||||
);
|
||||
}
|
||||
|
||||
async validatePrivateLocation() {
|
||||
const { response, request, server } = this.routeContext;
|
||||
|
||||
|
|
|
@ -55,7 +55,6 @@ import { getDefaultAlertingRoute } from './default_alerts/get_default_alert';
|
|||
import { createNetworkEventsRoute } from './network_events';
|
||||
import { addPrivateLocationRoute } from './settings/private_locations/add_private_location';
|
||||
import { deletePrivateLocationRoute } from './settings/private_locations/delete_private_location';
|
||||
import { editPrivateLocationRoute } from './settings/private_locations/edit_private_location';
|
||||
import { getPrivateLocationsRoute } from './settings/private_locations/get_private_locations';
|
||||
import { getSyntheticsFilters } from './filters/filters';
|
||||
import { getAllSyntheticsMonitorRoute } from './monitor_cruds/get_monitors_list';
|
||||
|
@ -113,7 +112,6 @@ export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] =
|
|||
deleteSyntheticsParamsRoute,
|
||||
addPrivateLocationRoute,
|
||||
deletePrivateLocationRoute,
|
||||
editPrivateLocationRoute,
|
||||
getPrivateLocationsRoute,
|
||||
getAllSyntheticsMonitorRoute,
|
||||
getSyntheticsMonitorRoute,
|
||||
|
|
|
@ -83,10 +83,7 @@ export const syncEditedMonitorBulk = async ({
|
|||
} as unknown as MonitorFields,
|
||||
}));
|
||||
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
|
||||
monitorConfigRepository.bulkUpdate({
|
||||
monitors: data,
|
||||
namespace: spaceId !== routeContext.spaceId ? spaceId : undefined,
|
||||
}),
|
||||
monitorConfigRepository.bulkUpdate({ monitors: data }),
|
||||
syncUpdatedMonitors({ monitorsToUpdate, routeContext, spaceId, privateLocations }),
|
||||
]);
|
||||
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeOf, schema } from '@kbn/config-schema';
|
||||
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEqual } from 'lodash';
|
||||
import { getPrivateLocations } from '../../../synthetics_service/get_private_locations';
|
||||
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
|
||||
import { PrivateLocationRepository } from '../../../repositories/private_location_repository';
|
||||
import { PRIVATE_LOCATION_WRITE_API } from '../../../feature';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { toClientContract, updatePrivateLocationMonitors } from './helpers';
|
||||
import { PrivateLocation } from '../../../../common/runtime_types';
|
||||
import { parseArrayFilters } from '../../common';
|
||||
|
||||
const EditPrivateLocationSchema = schema.object({
|
||||
label: schema.maybe(
|
||||
schema.string({
|
||||
minLength: 1,
|
||||
})
|
||||
),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
const EditPrivateLocationQuery = schema.object({
|
||||
locationId: schema.string(),
|
||||
});
|
||||
|
||||
export type EditPrivateLocationAttributes = Pick<
|
||||
PrivateLocationAttributes,
|
||||
keyof TypeOf<typeof EditPrivateLocationSchema>
|
||||
>;
|
||||
|
||||
const isPrivateLocationLabelChanged = (oldLabel: string, newLabel?: string): newLabel is string => {
|
||||
return typeof newLabel === 'string' && oldLabel !== newLabel;
|
||||
};
|
||||
|
||||
const isPrivateLocationChanged = ({
|
||||
privateLocation,
|
||||
newParams,
|
||||
}: {
|
||||
privateLocation: SavedObject<PrivateLocationAttributes>;
|
||||
newParams: TypeOf<typeof EditPrivateLocationSchema>;
|
||||
}) => {
|
||||
const isLabelChanged = isPrivateLocationLabelChanged(
|
||||
privateLocation.attributes.label,
|
||||
newParams.label
|
||||
);
|
||||
const areTagsChanged =
|
||||
Array.isArray(newParams.tags) &&
|
||||
(!privateLocation.attributes.tags ||
|
||||
(privateLocation.attributes.tags &&
|
||||
!isEqual(privateLocation.attributes.tags, newParams.tags)));
|
||||
|
||||
return isLabelChanged || areTagsChanged;
|
||||
};
|
||||
|
||||
export const editPrivateLocationRoute: SyntheticsRestApiRouteFactory<
|
||||
PrivateLocation,
|
||||
TypeOf<typeof EditPrivateLocationQuery>,
|
||||
any,
|
||||
TypeOf<typeof EditPrivateLocationSchema>
|
||||
> = () => ({
|
||||
method: 'PUT',
|
||||
path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS + '/{locationId}',
|
||||
validate: {},
|
||||
validation: {
|
||||
request: {
|
||||
body: EditPrivateLocationSchema,
|
||||
params: EditPrivateLocationQuery,
|
||||
},
|
||||
},
|
||||
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
|
||||
handler: async (routeContext) => {
|
||||
const { response, request, savedObjectsClient, server } = routeContext;
|
||||
const { locationId } = request.params;
|
||||
const { label: newLocationLabel, tags: newTags } = request.body;
|
||||
|
||||
const repo = new PrivateLocationRepository(routeContext);
|
||||
|
||||
try {
|
||||
const { filtersStr } = parseArrayFilters({
|
||||
locations: [locationId],
|
||||
});
|
||||
const [existingLocation, monitorsInLocation] = await Promise.all([
|
||||
repo.getPrivateLocation(locationId),
|
||||
routeContext.monitorConfigRepository.findDecryptedMonitors({
|
||||
spaceId: ALL_SPACES_ID,
|
||||
filter: filtersStr,
|
||||
}),
|
||||
]);
|
||||
|
||||
let newLocation: Awaited<ReturnType<typeof repo.editPrivateLocation>> | undefined;
|
||||
|
||||
if (
|
||||
isPrivateLocationChanged({ privateLocation: existingLocation, newParams: request.body })
|
||||
) {
|
||||
// This privileges check is done only when changing the label, because changing the label will update also the monitors in that location
|
||||
if (isPrivateLocationLabelChanged(existingLocation.attributes.label, newLocationLabel)) {
|
||||
const monitorsSpaces = monitorsInLocation.map(({ namespaces }) => namespaces![0]);
|
||||
|
||||
const checkSavedObjectsPrivileges =
|
||||
server.security.authz.checkSavedObjectsPrivilegesWithRequest(request);
|
||||
|
||||
const { hasAllRequested } = await checkSavedObjectsPrivileges(
|
||||
'saved_object:synthetics-monitor/bulk_update',
|
||||
monitorsSpaces
|
||||
);
|
||||
|
||||
if (!hasAllRequested) {
|
||||
return response.forbidden({
|
||||
body: {
|
||||
message: i18n.translate('xpack.synthetics.editPrivateLocation.forbidden', {
|
||||
defaultMessage:
|
||||
'You do not have sufficient permissions to update monitors in all required spaces. This private location is used by monitors in spaces where you lack update privileges.',
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
newLocation = await repo.editPrivateLocation(locationId, {
|
||||
label: newLocationLabel || existingLocation.attributes.label,
|
||||
tags: newTags || existingLocation.attributes.tags,
|
||||
});
|
||||
|
||||
if (isPrivateLocationLabelChanged(existingLocation.attributes.label, newLocationLabel)) {
|
||||
await updatePrivateLocationMonitors({
|
||||
locationId,
|
||||
newLocationLabel,
|
||||
allPrivateLocations: await getPrivateLocations(savedObjectsClient),
|
||||
routeContext,
|
||||
monitorsInLocation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return toClientContract({
|
||||
...existingLocation,
|
||||
attributes: {
|
||||
...existingLocation.attributes,
|
||||
...(newLocation ? newLocation.attributes : {}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
|
||||
return response.notFound({
|
||||
body: {
|
||||
message: `Private location with id ${locationId} does not exist.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -5,20 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { allLocationsToClientContract, updatePrivateLocationMonitors } from './helpers';
|
||||
import { RouteContext } from '../../types';
|
||||
|
||||
// Mock the syncEditedMonitorBulk module
|
||||
jest.mock('../../monitor_cruds/bulk_cruds/edit_monitor_bulk', () => ({
|
||||
syncEditedMonitorBulk: jest.fn().mockResolvedValue({
|
||||
failedConfigs: [],
|
||||
errors: [],
|
||||
editedMonitors: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Import the mocked function
|
||||
import { syncEditedMonitorBulk } from '../../monitor_cruds/bulk_cruds/edit_monitor_bulk';
|
||||
import { allLocationsToClientContract } from './helpers';
|
||||
|
||||
const testLocations = {
|
||||
locations: [
|
||||
|
@ -127,107 +114,3 @@ describe('toClientContract', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePrivateLocationMonitors', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const LOCATION_ID = 'test-location-id';
|
||||
const NEW_LABEL = 'New location label';
|
||||
const FIRST_SPACE_ID = 'firstSpaceId';
|
||||
const SECOND_SPACE_ID = 'secondSpaceId';
|
||||
const FIRST_MONITOR_ID = 'monitor-1';
|
||||
const SECOND_MONITOR_ID = 'monitor-2';
|
||||
const mockMonitors = [
|
||||
{
|
||||
id: FIRST_MONITOR_ID,
|
||||
attributes: {
|
||||
name: 'Test Monitor 1',
|
||||
locations: [{ id: LOCATION_ID, label: 'Old Label' }],
|
||||
// Other required monitor fields
|
||||
type: 'http',
|
||||
enabled: true,
|
||||
schedule: { number: '10', unit: 'm' },
|
||||
namespace: FIRST_SPACE_ID,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SECOND_MONITOR_ID,
|
||||
attributes: {
|
||||
name: 'Test Monitor 2',
|
||||
locations: [
|
||||
{ id: LOCATION_ID, label: 'Old Label' },
|
||||
{ id: 'different-location', label: 'Different Location' },
|
||||
],
|
||||
// Other required monitor fields
|
||||
type: 'http',
|
||||
enabled: true,
|
||||
schedule: { number: '5', unit: 'm' },
|
||||
namespace: SECOND_SPACE_ID,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('updates monitor locations with the new label', async () => {
|
||||
const PRIVATE_LOCATIONS = [] as any[];
|
||||
const ROUTE_CONTEXT = {} as RouteContext;
|
||||
// Call the function
|
||||
await updatePrivateLocationMonitors({
|
||||
locationId: LOCATION_ID,
|
||||
newLocationLabel: NEW_LABEL,
|
||||
allPrivateLocations: PRIVATE_LOCATIONS,
|
||||
routeContext: ROUTE_CONTEXT,
|
||||
monitorsInLocation: mockMonitors as any,
|
||||
});
|
||||
|
||||
// Verify that syncEditedMonitorBulk was called
|
||||
expect(syncEditedMonitorBulk).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Check first call for first space
|
||||
expect(syncEditedMonitorBulk).toHaveBeenCalledWith({
|
||||
monitorsToUpdate: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
decryptedPreviousMonitor: mockMonitors[0],
|
||||
normalizedMonitor: expect.any(Object),
|
||||
monitorWithRevision: expect.objectContaining({
|
||||
locations: [
|
||||
expect.objectContaining({
|
||||
id: LOCATION_ID,
|
||||
label: NEW_LABEL,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
privateLocations: PRIVATE_LOCATIONS,
|
||||
routeContext: ROUTE_CONTEXT,
|
||||
spaceId: FIRST_SPACE_ID,
|
||||
});
|
||||
|
||||
// Check second call for second space
|
||||
expect(syncEditedMonitorBulk).toHaveBeenCalledWith({
|
||||
monitorsToUpdate: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
decryptedPreviousMonitor: mockMonitors[1],
|
||||
normalizedMonitor: expect.any(Object),
|
||||
monitorWithRevision: expect.objectContaining({
|
||||
locations: [
|
||||
expect.objectContaining({
|
||||
id: LOCATION_ID,
|
||||
label: NEW_LABEL,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'different-location',
|
||||
label: 'Different Location',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
privateLocations: PRIVATE_LOCATIONS,
|
||||
routeContext: ROUTE_CONTEXT,
|
||||
spaceId: SECOND_SPACE_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,24 +4,14 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { SavedObject, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import { formatSecrets, normalizeSecrets } from '../../../synthetics_service/utils';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import { AgentPolicyInfo } from '../../../../common/types';
|
||||
import type {
|
||||
SyntheticsMonitor,
|
||||
SyntheticsMonitorWithSecretsAttributes,
|
||||
SyntheticsPrivateLocations,
|
||||
} from '../../../../common/runtime_types';
|
||||
import type { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
|
||||
import type {
|
||||
SyntheticsPrivateLocationsAttributes,
|
||||
PrivateLocationAttributes,
|
||||
} from '../../../runtime_types/private_locations';
|
||||
import { PrivateLocation } from '../../../../common/runtime_types';
|
||||
import {
|
||||
MonitorConfigUpdate,
|
||||
syncEditedMonitorBulk,
|
||||
} from '../../monitor_cruds/bulk_cruds/edit_monitor_bulk';
|
||||
import { RouteContext } from '../../types';
|
||||
|
||||
export const toClientContract = (
|
||||
locationObject: SavedObject<PrivateLocationAttributes>
|
||||
|
@ -70,54 +60,3 @@ export const toSavedObjectContract = (location: PrivateLocation): PrivateLocatio
|
|||
spaces: location.spaces,
|
||||
};
|
||||
};
|
||||
|
||||
// This should be called when changing the label of a private location because the label is also stored
|
||||
// in the locations array of monitors attributes
|
||||
export const updatePrivateLocationMonitors = async ({
|
||||
locationId,
|
||||
newLocationLabel,
|
||||
allPrivateLocations,
|
||||
routeContext,
|
||||
monitorsInLocation,
|
||||
}: {
|
||||
locationId: string;
|
||||
newLocationLabel: string;
|
||||
allPrivateLocations: SyntheticsPrivateLocations;
|
||||
routeContext: RouteContext;
|
||||
monitorsInLocation: Array<SavedObjectsFindResult<SyntheticsMonitorWithSecretsAttributes>>;
|
||||
}) => {
|
||||
const updatedMonitorsPerSpace = monitorsInLocation.reduce<Record<string, MonitorConfigUpdate[]>>(
|
||||
(acc, m) => {
|
||||
const decryptedMonitorsWithNormalizedSecrets: SavedObject<SyntheticsMonitor> =
|
||||
normalizeSecrets(m);
|
||||
const normalizedMonitor = decryptedMonitorsWithNormalizedSecrets.attributes;
|
||||
const newLocations = m.attributes.locations.map((l) =>
|
||||
l.id !== locationId ? l : { ...l, label: newLocationLabel }
|
||||
);
|
||||
const monitorWithRevision = formatSecrets({ ...normalizedMonitor, locations: newLocations });
|
||||
const monitorToUpdate: MonitorConfigUpdate = {
|
||||
normalizedMonitor,
|
||||
decryptedPreviousMonitor: m,
|
||||
monitorWithRevision,
|
||||
};
|
||||
|
||||
const namespace = m.attributes.namespace;
|
||||
return {
|
||||
...acc,
|
||||
[namespace]: [...(acc[namespace] || []), monitorToUpdate],
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const promises = Object.keys(updatedMonitorsPerSpace).map((namespace) => [
|
||||
syncEditedMonitorBulk({
|
||||
monitorsToUpdate: updatedMonitorsPerSpace[namespace],
|
||||
privateLocations: allPrivateLocations,
|
||||
routeContext,
|
||||
spaceId: namespace,
|
||||
}),
|
||||
]);
|
||||
|
||||
return Promise.all(promises.flat());
|
||||
};
|
||||
|
|
|
@ -82,20 +82,17 @@ export class MonitorConfigRepository {
|
|||
|
||||
async bulkUpdate({
|
||||
monitors,
|
||||
namespace,
|
||||
}: {
|
||||
monitors: Array<{
|
||||
attributes: MonitorFields;
|
||||
id: string;
|
||||
}>;
|
||||
namespace?: string;
|
||||
}) {
|
||||
return this.soClient.bulkUpdate<MonitorFields>(
|
||||
return await this.soClient.bulkUpdate<MonitorFields>(
|
||||
monitors.map(({ attributes, id }) => ({
|
||||
type: syntheticsMonitorType,
|
||||
id,
|
||||
attributes,
|
||||
namespace,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export function normalizeMonitorSecretAttributes(
|
|||
const normalizedMonitorAttributes = {
|
||||
...defaultFields,
|
||||
...monitor,
|
||||
...JSON.parse(monitor.secrets || '{}'),
|
||||
...JSON.parse(monitor.secrets || ''),
|
||||
};
|
||||
delete normalizedMonitorAttributes.secrets;
|
||||
return normalizedMonitorAttributes;
|
||||
|
|
|
@ -27,7 +27,7 @@ export const GETTING_STARTED_ROUTE = '/monitors/getting-started';
|
|||
|
||||
export const SETTINGS_ROUTE = '/settings';
|
||||
|
||||
export const PRIVATE_LOCATIONS_ROUTE = '/settings/private-locations';
|
||||
export const PRIVATE_LOCATIOSN_ROUTE = '/settings/private-locations';
|
||||
|
||||
export const SYNTHETICS_SETTINGS_ROUTE = '/settings/:tabId';
|
||||
|
||||
|
|
|
@ -39,10 +39,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
let resp;
|
||||
const { statusCodes, SPACE_ID, username, password, writeAccess, readUser } = options;
|
||||
let tags = !writeAccess ? '[uptime-read]' : options.tags ?? '[uptime-read,uptime-write]';
|
||||
if (
|
||||
(method === 'POST' || method === 'DELETE' || method === 'PUT') &&
|
||||
path.includes('private_locations')
|
||||
) {
|
||||
if ((method === 'POST' || method === 'DELETE') && path.includes('private_locations')) {
|
||||
tags = readUser
|
||||
? '[private-location-write,uptime-write]'
|
||||
: '[uptime-read,private-location-write,uptime-write]';
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RoleCredentials } from '@kbn/ftr-common-functional-services';
|
||||
import { PrivateLocation, ServiceLocation } from '@kbn/synthetics-plugin/common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||
import expect from '@kbn/expect';
|
||||
import rawExpect from 'expect';
|
||||
import { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { getFixtureJson } from './helpers/get_fixture_json';
|
||||
import { PrivateLocationTestService } from '../../../services/synthetics_private_location';
|
||||
import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor';
|
||||
import { SupertestWithRoleScopeType } from '../../../services';
|
||||
|
||||
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('EditPrivateLocation', function () {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const samlAuth = getService('samlAuth');
|
||||
const roleScopedSupertest = getService('roleScopedSupertest');
|
||||
let supertestEditorWithApiKey: SupertestWithRoleScopeType;
|
||||
|
||||
let testFleetPolicyID: string;
|
||||
let editorUser: RoleCredentials;
|
||||
let privateLocations: PrivateLocation[] = [];
|
||||
const testPolicyName = 'Fleet test server policy' + Date.now();
|
||||
|
||||
let newMonitor: { id: string; name: string };
|
||||
const testPrivateLocations = new PrivateLocationTestService(getService);
|
||||
const NEW_LOCATION_LABEL = 'Barcelona';
|
||||
const NEW_TAGS = ['myAwesomeTag'];
|
||||
|
||||
before(async () => {
|
||||
supertestEditorWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('editor', {
|
||||
withInternalHeaders: true,
|
||||
});
|
||||
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await testPrivateLocations.installSyntheticsPackage();
|
||||
editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertestEditorWithApiKey.destroy();
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(editorUser);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('adds a test fleet policy', async () => {
|
||||
const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName);
|
||||
testFleetPolicyID = apiResponse.body.item.id;
|
||||
});
|
||||
|
||||
it('add a test private location', async () => {
|
||||
privateLocations = await testPrivateLocations.setTestLocations([testFleetPolicyID]);
|
||||
|
||||
const apiResponse = await supertestEditorWithApiKey
|
||||
.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS)
|
||||
.expect(200);
|
||||
|
||||
const testResponse: Array<PrivateLocation | ServiceLocation> = [
|
||||
{
|
||||
id: testFleetPolicyID,
|
||||
isServiceManaged: false,
|
||||
isInvalid: false,
|
||||
label: privateLocations[0].label,
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
agentPolicyId: testFleetPolicyID,
|
||||
spaces: ['default'],
|
||||
},
|
||||
];
|
||||
|
||||
rawExpect(apiResponse.body.locations).toEqual(rawExpect.arrayContaining(testResponse));
|
||||
});
|
||||
|
||||
it('adds a monitor in private location', async () => {
|
||||
newMonitor = {
|
||||
...getFixtureJson('http_monitor'),
|
||||
namespace: 'default',
|
||||
locations: [privateLocations[0]],
|
||||
};
|
||||
|
||||
const { body, rawBody } = await addMonitorAPIHelper(
|
||||
supertestWithoutAuth,
|
||||
newMonitor,
|
||||
200,
|
||||
editorUser,
|
||||
samlAuth
|
||||
);
|
||||
expect(body).eql(omitMonitorKeys(newMonitor));
|
||||
newMonitor.id = rawBody.id;
|
||||
});
|
||||
|
||||
it('successfully edits a private location label', async () => {
|
||||
const privateLocation = privateLocations[0];
|
||||
|
||||
// Edit the private location
|
||||
const editResponse = await supertestEditorWithApiKey
|
||||
.put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocation.id}`)
|
||||
.send({ label: NEW_LOCATION_LABEL, tags: NEW_TAGS })
|
||||
.expect(200);
|
||||
|
||||
// Verify the response contains the updated label
|
||||
expect(editResponse.body.label).to.be(NEW_LOCATION_LABEL);
|
||||
expect(editResponse.body.tags).to.eql(NEW_TAGS);
|
||||
expect(editResponse.body.id).to.be(privateLocation.id);
|
||||
expect(editResponse.body.agentPolicyId).to.be(privateLocation.agentPolicyId);
|
||||
|
||||
// Verify the location was actually updated by getting it
|
||||
const getResponse = await supertestEditorWithApiKey
|
||||
.get(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocation.id}`)
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body.label).to.be(NEW_LOCATION_LABEL);
|
||||
});
|
||||
|
||||
it('verifies that monitor location label is updated when the private location label changes', async () => {
|
||||
// Get the monitor with the updated location label
|
||||
const getMonitorResponse = await supertestEditorWithApiKey
|
||||
.get(`${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${newMonitor.id}`)
|
||||
.expect(200);
|
||||
|
||||
// Verify the monitor's location has the updated label
|
||||
const monitor = getMonitorResponse.body;
|
||||
expect(monitor.locations).to.have.length(1);
|
||||
expect(monitor.locations[0].id).to.be(privateLocations[0].id);
|
||||
expect(monitor.locations[0].label).to.be(NEW_LOCATION_LABEL);
|
||||
});
|
||||
|
||||
it('verifies that package policies are updated when the private location label changes', async () => {
|
||||
const apiResponse = await supertestEditorWithApiKey.get(
|
||||
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
|
||||
);
|
||||
|
||||
const packagePolicy: PackagePolicy = apiResponse.body.items.find(
|
||||
(pkgPolicy: PackagePolicy) =>
|
||||
pkgPolicy.id === newMonitor.id + '-' + testFleetPolicyID + '-default'
|
||||
);
|
||||
expect(packagePolicy.name).to.contain(NEW_LOCATION_LABEL);
|
||||
});
|
||||
|
||||
it('returns 404 when trying to edit a non-existent private location', async () => {
|
||||
const nonExistentId = 'non-existent-id';
|
||||
|
||||
const response = await supertestEditorWithApiKey
|
||||
.put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${nonExistentId}`)
|
||||
.send({ label: NEW_LOCATION_LABEL })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.message).to.contain(
|
||||
`Private location with id ${nonExistentId} does not exist.`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 400 when trying to edit with an empty label', async () => {
|
||||
const response = await supertestEditorWithApiKey
|
||||
.put(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${privateLocations[0].id}`)
|
||||
.send({ label: '' })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.message).to.contain(
|
||||
'[request body.label]: value has length [0] but it must have a minimum length of [1].'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -31,6 +31,5 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
|||
loadTestFile(require.resolve('./sync_global_params'));
|
||||
loadTestFile(require.resolve('./synthetics_enablement'));
|
||||
loadTestFile(require.resolve('./test_now_monitor'));
|
||||
loadTestFile(require.resolve('./edit_private_location'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue