mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Synthetics] Added ability to hide public locations (#164863)
This commit is contained in:
parent
39cf3718b3
commit
519c4d6249
30 changed files with 470 additions and 196 deletions
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './locations';
|
||||
export * from './state';
|
||||
|
|
|
@ -1,31 +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 * as t from 'io-ts';
|
||||
import { CheckGeoType, SummaryType } from '../common';
|
||||
|
||||
// IO type for validation
|
||||
export const MonitorLocationType = t.type({
|
||||
up_history: t.number,
|
||||
down_history: t.number,
|
||||
timestamp: t.string,
|
||||
summary: SummaryType,
|
||||
geo: CheckGeoType,
|
||||
});
|
||||
|
||||
// Typescript type for type checking
|
||||
export type MonitorLocation = t.TypeOf<typeof MonitorLocationType>;
|
||||
|
||||
export const MonitorLocationsType = t.intersection([
|
||||
t.type({
|
||||
monitorId: t.string,
|
||||
up_history: t.number,
|
||||
down_history: t.number,
|
||||
}),
|
||||
t.partial({ locations: t.array(MonitorLocationType) }),
|
||||
]);
|
||||
export type MonitorLocations = t.TypeOf<typeof MonitorLocationsType>;
|
|
@ -46,6 +46,10 @@ export const TLSSensitiveFieldsCodec = t.partial({
|
|||
|
||||
export const TLSCodec = t.intersection([TLSFieldsCodec, TLSSensitiveFieldsCodec]);
|
||||
|
||||
const MonitorLocationsCodec = t.array(t.union([MonitorServiceLocationCodec, PrivateLocationCodec]));
|
||||
|
||||
export type MonitorLocations = t.TypeOf<typeof MonitorLocationsCodec>;
|
||||
|
||||
// CommonFields
|
||||
export const CommonFieldsCodec = t.intersection([
|
||||
t.interface({
|
||||
|
@ -56,7 +60,7 @@ export const CommonFieldsCodec = t.intersection([
|
|||
[ConfigKey.SCHEDULE]: ScheduleCodec,
|
||||
[ConfigKey.APM_SERVICE_NAME]: t.string,
|
||||
[ConfigKey.TAGS]: t.array(t.string),
|
||||
[ConfigKey.LOCATIONS]: t.array(t.union([MonitorServiceLocationCodec, PrivateLocationCodec])),
|
||||
[ConfigKey.LOCATIONS]: MonitorLocationsCodec,
|
||||
[ConfigKey.MONITOR_QUERY_ID]: t.string,
|
||||
[ConfigKey.CONFIG_ID]: t.string,
|
||||
}),
|
||||
|
|
|
@ -30,12 +30,14 @@ export const FleetPermissionsCallout = () => {
|
|||
*/
|
||||
export const NoPermissionsTooltip = ({
|
||||
canEditSynthetics = true,
|
||||
canUsePublicLocations = true,
|
||||
children,
|
||||
}: {
|
||||
canEditSynthetics?: boolean;
|
||||
canUsePublicLocations?: boolean;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const disabledMessage = getRestrictionReasonLabel(canEditSynthetics);
|
||||
const disabledMessage = getRestrictionReasonLabel(canEditSynthetics, canUsePublicLocations);
|
||||
if (disabledMessage) {
|
||||
return (
|
||||
<EuiToolTip content={disabledMessage}>
|
||||
|
@ -47,8 +49,16 @@ export const NoPermissionsTooltip = ({
|
|||
return <>{children}</>;
|
||||
};
|
||||
|
||||
function getRestrictionReasonLabel(canEditSynthetics = true): string | undefined {
|
||||
return !canEditSynthetics ? CANNOT_PERFORM_ACTION_SYNTHETICS : undefined;
|
||||
function getRestrictionReasonLabel(
|
||||
canEditSynthetics = true,
|
||||
canUsePublicLocations = true
|
||||
): string | undefined {
|
||||
const message = !canEditSynthetics ? CANNOT_PERFORM_ACTION_SYNTHETICS : undefined;
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return !canUsePublicLocations ? CANNOT_PERFORM_ACTION_PUBLIC_LOCATIONS : undefined;
|
||||
}
|
||||
|
||||
export const NEED_PERMISSIONS_PRIVATE_LOCATIONS = i18n.translate(
|
||||
|
@ -83,3 +93,10 @@ export const CANNOT_PERFORM_ACTION_SYNTHETICS = i18n.translate(
|
|||
defaultMessage: 'You do not have sufficient permissions to perform this action.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANNOT_PERFORM_ACTION_PUBLIC_LOCATIONS = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.canUsePublicLocations',
|
||||
{
|
||||
defaultMessage: 'You do not have sufficient permissions to use Elastic managed locations.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -78,6 +78,7 @@ import {
|
|||
ResponseCheckJSON,
|
||||
ThrottlingConfig,
|
||||
RequestBodyCheck,
|
||||
SourceType,
|
||||
} from '../types';
|
||||
import { AlertConfigKey, ALLOWED_SCHEDULES_IN_MINUTES } from '../constants';
|
||||
import { getDefaultFormFields } from './defaults';
|
||||
|
@ -404,8 +405,8 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
props: ({ field, setValue, locations, trigger }) => {
|
||||
return {
|
||||
options: Object.values(locations).map((location) => ({
|
||||
label: locations?.find((loc) => location.id === loc.id)?.label || '',
|
||||
id: location.id || '',
|
||||
label: location.label,
|
||||
id: location.id,
|
||||
isServiceManaged: location.isServiceManaged || false,
|
||||
isInvalid: location.isInvalid,
|
||||
disabled: location.isInvalid,
|
||||
|
@ -417,7 +418,9 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
: location.isServiceManaged
|
||||
? 'default'
|
||||
: 'primary',
|
||||
label: locations?.find((loc) => location.id === loc.id)?.label ?? location.id,
|
||||
label:
|
||||
(location.label || locations?.find((loc) => location.id === loc.id)?.label) ??
|
||||
location.id,
|
||||
id: location.id || '',
|
||||
isServiceManaged: location.isServiceManaged || false,
|
||||
})),
|
||||
|
@ -483,66 +486,78 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
|
|||
helpText: i18n.translate('xpack.synthetics.monitorConfig.edit.enabled.label', {
|
||||
defaultMessage: `When disabled, the monitor doesn't run any tests. You can enable it at any time.`,
|
||||
}),
|
||||
props: ({ setValue, field, trigger }): EuiSwitchProps => ({
|
||||
id: 'syntheticsMontiorConfigIsEnabled',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.enabled.label', {
|
||||
defaultMessage: 'Enable Monitor',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: async (event) => {
|
||||
setValue(ConfigKey.ENABLED, !!event.target.checked);
|
||||
await trigger(ConfigKey.ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsEnableSwitch',
|
||||
// enabled is an allowed field for read only
|
||||
// isDisabled: readOnly,
|
||||
}),
|
||||
props: ({ setValue, field, trigger, formState }): EuiSwitchProps => {
|
||||
const isProjectMonitor =
|
||||
formState.defaultValues?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
return {
|
||||
id: 'syntheticsMontiorConfigIsEnabled',
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.enabled.label', {
|
||||
defaultMessage: 'Enable Monitor',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: async (event) => {
|
||||
setValue(ConfigKey.ENABLED, !!event.target.checked);
|
||||
await trigger(ConfigKey.ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsEnableSwitch',
|
||||
// enabled is an allowed field for read only
|
||||
disabled: !isProjectMonitor && readOnly,
|
||||
};
|
||||
},
|
||||
},
|
||||
[AlertConfigKey.STATUS_ENABLED]: {
|
||||
fieldKey: AlertConfigKey.STATUS_ENABLED,
|
||||
component: Switch,
|
||||
controlled: true,
|
||||
props: ({ setValue, field, trigger }): EuiSwitchProps => ({
|
||||
id: 'syntheticsMonitorConfigIsAlertEnabled',
|
||||
label: field?.value
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.enabledAlerting.label', {
|
||||
defaultMessage: 'Disable status alerts on this monitor',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.monitorConfig.disabledAlerting.label', {
|
||||
defaultMessage: 'Enable status alerts on this monitor',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: async (event) => {
|
||||
setValue(AlertConfigKey.STATUS_ENABLED, !!event.target.checked);
|
||||
await trigger(AlertConfigKey.STATUS_ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsAlertStatusSwitch',
|
||||
// alert config is an allowed field for read only
|
||||
// isDisabled: readOnly,
|
||||
}),
|
||||
props: ({ setValue, field, trigger, formState }): EuiSwitchProps => {
|
||||
const isProjectMonitor =
|
||||
formState.defaultValues?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
return {
|
||||
id: 'syntheticsMonitorConfigIsAlertEnabled',
|
||||
label: field?.value
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.enabledAlerting.label', {
|
||||
defaultMessage: 'Disable status alerts on this monitor',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.monitorConfig.disabledAlerting.label', {
|
||||
defaultMessage: 'Enable status alerts on this monitor',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: async (event) => {
|
||||
setValue(AlertConfigKey.STATUS_ENABLED, !!event.target.checked);
|
||||
await trigger(AlertConfigKey.STATUS_ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsAlertStatusSwitch',
|
||||
// alert config is an allowed field for read only
|
||||
disabled: !isProjectMonitor && readOnly,
|
||||
};
|
||||
},
|
||||
},
|
||||
[AlertConfigKey.TLS_ENABLED]: {
|
||||
fieldKey: AlertConfigKey.TLS_ENABLED,
|
||||
component: Switch,
|
||||
controlled: true,
|
||||
props: ({ setValue, field, trigger }): EuiSwitchProps => ({
|
||||
id: 'syntheticsMonitorConfigIsTlsAlertEnabled',
|
||||
label: field?.value
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.edit.alertTlsEnabled.label', {
|
||||
defaultMessage: 'Disable TLS alerts on this monitor.',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.monitorConfig.create.alertTlsEnabled.label', {
|
||||
defaultMessage: 'Enable TLS alerts on this monitor.',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: async (event) => {
|
||||
setValue(AlertConfigKey.TLS_ENABLED, !!event.target.checked);
|
||||
await trigger(AlertConfigKey.TLS_ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsAlertStatusSwitch',
|
||||
// alert config is an allowed field for read only
|
||||
// isDisabled: readOnly,
|
||||
}),
|
||||
props: ({ setValue, field, trigger, formState }): EuiSwitchProps => {
|
||||
const isProjectMonitor =
|
||||
formState.defaultValues?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
return {
|
||||
id: 'syntheticsMonitorConfigIsTlsAlertEnabled',
|
||||
label: field?.value
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.edit.alertTlsEnabled.label', {
|
||||
defaultMessage: 'Disable TLS alerts on this monitor.',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.monitorConfig.create.alertTlsEnabled.label', {
|
||||
defaultMessage: 'Enable TLS alerts on this monitor.',
|
||||
}),
|
||||
checked: field?.value || false,
|
||||
onChange: async (event) => {
|
||||
setValue(AlertConfigKey.TLS_ENABLED, !!event.target.checked);
|
||||
await trigger(AlertConfigKey.TLS_ENABLED);
|
||||
},
|
||||
'data-test-subj': 'syntheticsAlertStatusSwitch',
|
||||
// alert config is an allowed field for read only
|
||||
disabled: !isProjectMonitor && readOnly,
|
||||
};
|
||||
},
|
||||
},
|
||||
[ConfigKey.TAGS]: {
|
||||
fieldKey: ConfigKey.TAGS,
|
||||
|
|
|
@ -18,7 +18,8 @@ export const MonitorForm: React.FC<{
|
|||
defaultValues?: SyntheticsMonitor;
|
||||
space?: string;
|
||||
readOnly?: boolean;
|
||||
}> = ({ children, defaultValues, space, readOnly = false }) => {
|
||||
canUsePublicLocations: boolean;
|
||||
}> = ({ children, defaultValues, space, readOnly = false, canUsePublicLocations }) => {
|
||||
const methods = useFormWrapped({
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onSubmit',
|
||||
|
@ -43,7 +44,7 @@ export const MonitorForm: React.FC<{
|
|||
>
|
||||
{children}
|
||||
<EuiSpacer />
|
||||
<ActionBar readOnly={readOnly} />
|
||||
<ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} />
|
||||
</EuiForm>
|
||||
<Disclaimer />
|
||||
</FormProvider>
|
||||
|
|
|
@ -16,7 +16,11 @@ import { format } from './formatter';
|
|||
import { MonitorFields as MonitorFieldsType } from '../../../../../../common/runtime_types';
|
||||
import { runOnceMonitor } from '../../../state/manual_test_runs/api';
|
||||
|
||||
export const RunTestButton = () => {
|
||||
export const RunTestButton = ({
|
||||
canUsePublicLocations = true,
|
||||
}: {
|
||||
canUsePublicLocations?: boolean;
|
||||
}) => {
|
||||
const { formState, getValues, handleSubmit } = useFormContext();
|
||||
|
||||
const [inProgress, setInProgress] = useState(false);
|
||||
|
@ -56,7 +60,7 @@ export const RunTestButton = () => {
|
|||
<EuiButton
|
||||
data-test-subj="syntheticsRunTestBtn"
|
||||
color="success"
|
||||
disabled={isDisabled}
|
||||
disabled={isDisabled || !canUsePublicLocations}
|
||||
aria-label={TEST_NOW_ARIA_LABEL}
|
||||
iconType="play"
|
||||
onClick={handleSubmit(handleTestNow)}
|
||||
|
|
|
@ -21,7 +21,13 @@ import { format } from './formatter';
|
|||
|
||||
import { MONITORS_ROUTE } from '../../../../../../common/constants';
|
||||
|
||||
export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => {
|
||||
export const ActionBar = ({
|
||||
readOnly = false,
|
||||
canUsePublicLocations = true,
|
||||
}: {
|
||||
readOnly: boolean;
|
||||
canUsePublicLocations: boolean;
|
||||
}) => {
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
const history = useHistory();
|
||||
const {
|
||||
|
@ -59,6 +65,7 @@ export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => {
|
|||
onClick={() => {
|
||||
setMonitorPendingDeletion(defaultValues as SyntheticsMonitor);
|
||||
}}
|
||||
isDisabled={!canEditSynthetics || !canUsePublicLocations}
|
||||
>
|
||||
{DELETE_MONITOR_LABEL}
|
||||
</EuiButton>
|
||||
|
@ -75,16 +82,19 @@ export const ActionBar = ({ readOnly = false }: { readOnly: boolean }) => {
|
|||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RunTestButton />
|
||||
<RunTestButton canUsePublicLocations={canUsePublicLocations} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={{ marginLeft: 'auto' }}>
|
||||
<NoPermissionsTooltip canEditSynthetics={canEditSynthetics}>
|
||||
<NoPermissionsTooltip
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
canUsePublicLocations={canUsePublicLocations}
|
||||
>
|
||||
<EuiButton
|
||||
fill
|
||||
isLoading={loading}
|
||||
onClick={handleSubmit(formSubmitter)}
|
||||
data-test-subj="syntheticsMonitorConfigSubmitButton"
|
||||
disabled={!canEditSynthetics}
|
||||
disabled={!canEditSynthetics || !canUsePublicLocations}
|
||||
>
|
||||
{isEdit ? UPDATE_MONITOR_LABEL : CREATE_MONITOR_LABEL}
|
||||
</EuiButton>
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiEmptyPrompt } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { useTrackPageview, useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
|
||||
import { useCanUsePublicLocations } from '../../../../hooks/use_capabilities';
|
||||
import { EditMonitorNotFound } from './edit_monitor_not_found';
|
||||
import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout';
|
||||
import { ConfigKey, SourceType } from '../../../../../common/runtime_types';
|
||||
|
@ -50,11 +51,15 @@ export const MonitorEditPage: React.FC = () => {
|
|||
data?.id
|
||||
);
|
||||
|
||||
const canUsePublicLocations = useCanUsePublicLocations(data?.[ConfigKey.LOCATIONS]);
|
||||
|
||||
if (monitorNotFoundError) {
|
||||
return <EditMonitorNotFound />;
|
||||
}
|
||||
|
||||
const isReadOnly = data?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
const isReadOnly =
|
||||
data?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT || !canUsePublicLocations;
|
||||
|
||||
const projectId = data?.[ConfigKey.PROJECT_ID];
|
||||
|
||||
if (locationsError) {
|
||||
|
@ -87,8 +92,13 @@ export const MonitorEditPage: React.FC = () => {
|
|||
return data && locationsLoaded && !loading && !error ? (
|
||||
<>
|
||||
<AlertingCallout isAlertingEnabled={data[ConfigKey.ALERT_CONFIG]?.status?.enabled} />
|
||||
<MonitorForm defaultValues={data} readOnly={isReadOnly}>
|
||||
<MonitorForm
|
||||
defaultValues={data}
|
||||
readOnly={isReadOnly}
|
||||
canUsePublicLocations={canUsePublicLocations}
|
||||
>
|
||||
<MonitorSteps
|
||||
canUsePublicLocations={canUsePublicLocations}
|
||||
stepMap={EDIT_MONITOR_STEPS(isReadOnly)}
|
||||
isEditFlow={true}
|
||||
readOnly={isReadOnly}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MonitorTypePortal } from './monitor_type_portal';
|
|||
import { ReadOnlyCallout } from './read_only_callout';
|
||||
|
||||
export const MonitorSteps = ({
|
||||
canUsePublicLocations,
|
||||
stepMap,
|
||||
projectId,
|
||||
isEditFlow = false,
|
||||
|
@ -23,6 +24,7 @@ export const MonitorSteps = ({
|
|||
}: {
|
||||
stepMap: StepMap;
|
||||
readOnly?: boolean;
|
||||
canUsePublicLocations?: boolean;
|
||||
isEditFlow?: boolean;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
|
@ -32,12 +34,9 @@ export const MonitorSteps = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{readOnly ? (
|
||||
<>
|
||||
<ReadOnlyCallout projectId={projectId} />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
{isEditFlow && (
|
||||
<ReadOnlyCallout projectId={projectId} canUsePublicLocations={canUsePublicLocations} />
|
||||
)}
|
||||
{isEditFlow ? (
|
||||
steps.map((step) => (
|
||||
<div key={step.title}>
|
||||
|
|
|
@ -5,27 +5,65 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const ReadOnlyCallout = ({ projectId }: { projectId?: string }) => {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.project.readOnly.callout.title"
|
||||
defaultMessage="This configuration is read-only"
|
||||
/>
|
||||
}
|
||||
iconType="document"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.project.readOnly.callout.content"
|
||||
defaultMessage="This monitor was added from an external project: {projectId}. From this page, you can only enable and disable the monitor and its alerts, or remove it. To make configuration changes, you have to edit its source file and push it again from that project."
|
||||
values={{ projectId: <strong>{projectId}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
export const ReadOnlyCallout = ({
|
||||
projectId,
|
||||
canUsePublicLocations,
|
||||
}: {
|
||||
projectId?: string;
|
||||
canUsePublicLocations?: boolean;
|
||||
}) => {
|
||||
if (projectId) {
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.project.readOnly.callout.title"
|
||||
defaultMessage="This configuration is read-only"
|
||||
/>
|
||||
}
|
||||
iconType="document"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.project.readOnly.callout.content"
|
||||
defaultMessage="This monitor was added from an external project: {projectId}. From this page, you can only enable and disable the monitor and its alerts, or remove it. To make configuration changes, you have to edit its source file and push it again from that project."
|
||||
values={{ projectId: <strong>{projectId}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canUsePublicLocations) {
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.publicLocations.readOnly.callout.title"
|
||||
defaultMessage="You do not have permission to use Elastic managed locations"
|
||||
/>
|
||||
}
|
||||
iconType="alert"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.publicLocations.readOnly.callout.content"
|
||||
defaultMessage="This monitor contains a Elastic managed location. To edit this monitor, you need to have permission to use Elastic managed locations."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ServiceLocation } from '../../../../../../common/runtime_types';
|
||||
import { useSelectedMonitor } from './use_selected_monitor';
|
||||
import { selectSelectedLocationId, setMonitorDetailsLocationAction } from '../../../state';
|
||||
import { useUrlParams, useLocations } from '../../../hooks';
|
||||
|
@ -41,8 +42,12 @@ export const useSelectedLocation = (updateUrl = true) => {
|
|||
monitor?.locations,
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() => locations.find((loc) => loc.id === urlLocationId) ?? null,
|
||||
[urlLocationId, locations]
|
||||
);
|
||||
return useMemo(() => {
|
||||
let selLoc = locations.find((loc) => loc.id === urlLocationId) ?? null;
|
||||
if (!selLoc) {
|
||||
selLoc =
|
||||
(monitor?.locations?.find((loc) => loc.id === urlLocationId) as ServiceLocation) ?? null;
|
||||
}
|
||||
return selLoc;
|
||||
}, [locations, urlLocationId, monitor?.locations]);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,9 @@ import { EuiButton, EuiToolTip } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CANNOT_PERFORM_ACTION_PUBLIC_LOCATIONS } from '../common/components/permissions';
|
||||
import { useCanUsePublicLocations } from '../../../../hooks/use_capabilities';
|
||||
import { ConfigKey } from '../../../../../common/constants/monitor_management';
|
||||
import { TEST_NOW_ARIA_LABEL, TEST_SCHEDULED_LABEL } from '../monitor_add_edit/form/run_test_btn';
|
||||
import { useSelectedMonitor } from './hooks/use_selected_monitor';
|
||||
import {
|
||||
|
@ -22,7 +25,13 @@ export const RunTestManually = () => {
|
|||
const { monitor } = useSelectedMonitor();
|
||||
const testInProgress = useSelector(manualTestRunInProgressSelector(monitor?.config_id));
|
||||
|
||||
const content = testInProgress ? TEST_SCHEDULED_LABEL : TEST_NOW_ARIA_LABEL;
|
||||
const canUsePublicLocations = useCanUsePublicLocations(monitor?.[ConfigKey.LOCATIONS]);
|
||||
|
||||
const content = !canUsePublicLocations
|
||||
? CANNOT_PERFORM_ACTION_PUBLIC_LOCATIONS
|
||||
: testInProgress
|
||||
? TEST_SCHEDULED_LABEL
|
||||
: TEST_NOW_ARIA_LABEL;
|
||||
|
||||
return (
|
||||
<EuiToolTip content={content} key={content}>
|
||||
|
@ -31,6 +40,7 @@ export const RunTestManually = () => {
|
|||
color="success"
|
||||
iconType="beaker"
|
||||
isLoading={!Boolean(monitor) || testInProgress}
|
||||
isDisabled={!canUsePublicLocations}
|
||||
onClick={() => {
|
||||
if (monitor) {
|
||||
dispatch(
|
||||
|
|
|
@ -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 { useSelector } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { selectOverviewState } from '../../../state';
|
||||
|
||||
export const useCanUsePublicLocById = (configId: string) => {
|
||||
const {
|
||||
data: { monitors },
|
||||
} = useSelector(selectOverviewState);
|
||||
|
||||
const hasManagedLocation = monitors?.filter(
|
||||
(mon) => mon.configId === configId && mon.location.isServiceManaged
|
||||
);
|
||||
|
||||
const canUsePublicLocations =
|
||||
useKibana().services?.application?.capabilities.uptime.elasticManagedLocationsEnabled ?? true;
|
||||
|
||||
return hasManagedLocation ? !!canUsePublicLocations : true;
|
||||
};
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
|
||||
import {
|
||||
isStatusEnabled,
|
||||
|
@ -55,6 +56,15 @@ export function useMonitorListColumns({
|
|||
return alertStatus(fields[ConfigKey.CONFIG_ID]) === FETCH_STATUS.LOADING;
|
||||
};
|
||||
|
||||
const canUsePublicLocations =
|
||||
useKibana().services?.application?.capabilities.uptime.elasticManagedLocationsEnabled ?? true;
|
||||
|
||||
const isPublicLocationsAllowed = (fields: EncryptedSyntheticsSavedMonitor) => {
|
||||
const publicLocations = fields.locations.some((loc) => loc.isServiceManaged);
|
||||
|
||||
return publicLocations ? Boolean(canUsePublicLocations) : true;
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> = [
|
||||
{
|
||||
align: 'left' as const,
|
||||
|
@ -166,14 +176,18 @@ export function useMonitorListColumns({
|
|||
'data-test-subj': 'syntheticsMonitorEditAction',
|
||||
isPrimary: true,
|
||||
name: (fields) => (
|
||||
<NoPermissionsTooltip canEditSynthetics={canEditSynthetics}>
|
||||
<NoPermissionsTooltip
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
canUsePublicLocations={isPublicLocationsAllowed(fields)}
|
||||
>
|
||||
{labels.EDIT_LABEL}
|
||||
</NoPermissionsTooltip>
|
||||
),
|
||||
description: labels.EDIT_LABEL,
|
||||
icon: 'pencil' as const,
|
||||
type: 'icon' as const,
|
||||
enabled: (fields) => canEditSynthetics && !isActionLoading(fields),
|
||||
enabled: (fields) =>
|
||||
canEditSynthetics && !isActionLoading(fields) && isPublicLocationsAllowed(fields),
|
||||
onClick: (fields) => {
|
||||
history.push({
|
||||
pathname: `/edit-monitor/${fields[ConfigKey.CONFIG_ID]}`,
|
||||
|
@ -184,7 +198,10 @@ export function useMonitorListColumns({
|
|||
'data-test-subj': 'syntheticsMonitorDeleteAction',
|
||||
isPrimary: true,
|
||||
name: (fields) => (
|
||||
<NoPermissionsTooltip canEditSynthetics={canEditSynthetics}>
|
||||
<NoPermissionsTooltip
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
canUsePublicLocations={isPublicLocationsAllowed(fields)}
|
||||
>
|
||||
{labels.DELETE_LABEL}
|
||||
</NoPermissionsTooltip>
|
||||
),
|
||||
|
@ -192,7 +209,8 @@ export function useMonitorListColumns({
|
|||
icon: 'trash' as const,
|
||||
type: 'icon' as const,
|
||||
color: 'danger' as const,
|
||||
enabled: (fields) => canEditSynthetics && !isActionLoading(fields),
|
||||
enabled: (fields) =>
|
||||
canEditSynthetics && !isActionLoading(fields) && isPublicLocationsAllowed(fields),
|
||||
onClick: (fields) => {
|
||||
setMonitorPendingDeletion(fields);
|
||||
},
|
||||
|
@ -207,7 +225,8 @@ export function useMonitorListColumns({
|
|||
isStatusEnabled(fields[ConfigKey.ALERT_CONFIG]) ? 'bellSlash' : 'bell',
|
||||
type: 'icon' as const,
|
||||
color: 'danger' as const,
|
||||
enabled: (fields) => canEditSynthetics && !isActionLoading(fields),
|
||||
enabled: (fields) =>
|
||||
canEditSynthetics && !isActionLoading(fields) && isPublicLocationsAllowed(fields),
|
||||
onClick: (fields) => {
|
||||
updateAlertEnabledState({
|
||||
monitor: {
|
||||
|
|
|
@ -10,7 +10,10 @@ import { EuiSwitch, EuiSwitchEvent, EuiLoadingSpinner } from '@elastic/eui';
|
|||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitor } from '../../../../../../../common/runtime_types';
|
||||
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
|
||||
import {
|
||||
useCanEditSynthetics,
|
||||
useCanUsePublicLocations,
|
||||
} from '../../../../../../hooks/use_capabilities';
|
||||
import { useMonitorEnableHandler } from '../../../../hooks';
|
||||
import { NoPermissionsTooltip } from '../../../common/components/permissions';
|
||||
import * as labels from './labels';
|
||||
|
@ -32,6 +35,8 @@ export const MonitorEnabled = ({
|
|||
}: Props) => {
|
||||
const canEditSynthetics = useCanEditSynthetics();
|
||||
|
||||
const canUsePublicLocations = useCanUsePublicLocations(monitor?.[ConfigKey.LOCATIONS]);
|
||||
|
||||
const monitorName = monitor[ConfigKey.NAME];
|
||||
const statusLabels = useMemo(() => {
|
||||
return {
|
||||
|
@ -63,11 +68,14 @@ export const MonitorEnabled = ({
|
|||
{isLoading || initialLoading ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
<NoPermissionsTooltip canEditSynthetics={canEditSynthetics}>
|
||||
<NoPermissionsTooltip
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
canUsePublicLocations={canUsePublicLocations}
|
||||
>
|
||||
<SwitchWithCursor
|
||||
compressed={true}
|
||||
checked={enabled}
|
||||
disabled={isLoading || !canEditSynthetics}
|
||||
disabled={isLoading || !canEditSynthetics || !canUsePublicLocations}
|
||||
showLabel={false}
|
||||
label={enabledDisableLabel}
|
||||
title={enabledDisableLabel}
|
||||
|
|
|
@ -23,12 +23,19 @@ export const MonitorLocations = ({ locations, monitorId, status }: Props) => {
|
|||
|
||||
const locationsToDisplay = locations
|
||||
.map((loc) => {
|
||||
if (loc.label) {
|
||||
return {
|
||||
id: loc.id,
|
||||
label: loc.label,
|
||||
...getLocationStatusColor(theme, loc.id, monitorId, status),
|
||||
};
|
||||
}
|
||||
const fullLoc = allLocations.find((l) => l.id === loc.id);
|
||||
if (fullLoc) {
|
||||
return {
|
||||
id: fullLoc.id,
|
||||
label: fullLoc.label,
|
||||
...getLocationStatusColor(theme, fullLoc.label, monitorId, status),
|
||||
...getLocationStatusColor(theme, fullLoc.id, monitorId, status),
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -41,7 +48,7 @@ export const MonitorLocations = ({ locations, monitorId, status }: Props) => {
|
|||
|
||||
function getLocationStatusColor(
|
||||
euiTheme: ReturnType<typeof useTheme>,
|
||||
locationLabel: string | undefined,
|
||||
locationId: string,
|
||||
monitorId: string,
|
||||
overviewStatus: OverviewStatusState | null
|
||||
) {
|
||||
|
@ -49,7 +56,7 @@ function getLocationStatusColor(
|
|||
eui: { euiColorVis9, euiColorVis0, euiColorDisabled },
|
||||
} = euiTheme;
|
||||
|
||||
const locById = `${monitorId}-${locationLabel}`;
|
||||
const locById = `${monitorId}-${locationId}`;
|
||||
|
||||
if (overviewStatus?.downConfigs[locById]) {
|
||||
return { status: 'down', color: euiColorVis9 };
|
||||
|
|
|
@ -14,10 +14,13 @@ import {
|
|||
EuiPanel,
|
||||
EuiLoadingSpinner,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { TEST_SCHEDULED_LABEL } from '../../../monitor_add_edit/form/run_test_btn';
|
||||
import { useCanUsePublicLocById } from '../../hooks/use_can_use_public_loc_id';
|
||||
import { toggleStatusAlert } from '../../../../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import {
|
||||
manualTestMonitorAction,
|
||||
|
@ -101,8 +104,7 @@ export function ActionsPopover({
|
|||
}: Props) {
|
||||
const euiShadow = useEuiShadow('l');
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocationName({ locationId });
|
||||
const locationName = location?.label || monitor.location.id;
|
||||
const locationName = useLocationName(monitor);
|
||||
|
||||
const detailUrl = useMonitorDetailLocator({
|
||||
configId: monitor.configId,
|
||||
|
@ -112,6 +114,8 @@ export function ActionsPopover({
|
|||
|
||||
const canEditSynthetics = useCanEditSynthetics();
|
||||
|
||||
const canUsePublicLocations = useCanUsePublicLocById(monitor.configId);
|
||||
|
||||
const labels = useMemo(
|
||||
() => ({
|
||||
enabledSuccessLabel: enabledSuccessLabel(monitor.name),
|
||||
|
@ -163,7 +167,6 @@ export function ActionsPopover({
|
|||
};
|
||||
|
||||
const alertLoading = alertStatus(monitor.configId) === FETCH_STATUS.LOADING;
|
||||
|
||||
let popoverItems: EuiContextMenuPanelItemDescriptor[] = [
|
||||
{
|
||||
name: actionsMenuGoToMonitorName,
|
||||
|
@ -172,9 +175,17 @@ export function ActionsPopover({
|
|||
},
|
||||
quickInspectPopoverItem,
|
||||
{
|
||||
name: runTestManually,
|
||||
name: testInProgress ? (
|
||||
<EuiToolTip content={TEST_SCHEDULED_LABEL}>
|
||||
<span>{runTestManually}</span>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<NoPermissionsTooltip canUsePublicLocations={canUsePublicLocations}>
|
||||
{runTestManually}
|
||||
</NoPermissionsTooltip>
|
||||
),
|
||||
icon: 'beaker',
|
||||
disabled: testInProgress,
|
||||
disabled: testInProgress || !canUsePublicLocations,
|
||||
onClick: () => {
|
||||
dispatch(manualTestMonitorAction.get({ configId: monitor.configId, name: monitor.name }));
|
||||
dispatch(setFlyoutConfig(null));
|
||||
|
@ -193,12 +204,15 @@ export function ActionsPopover({
|
|||
},
|
||||
{
|
||||
name: (
|
||||
<NoPermissionsTooltip canEditSynthetics={canEditSynthetics}>
|
||||
<NoPermissionsTooltip
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
canUsePublicLocations={canUsePublicLocations}
|
||||
>
|
||||
{enableLabel}
|
||||
</NoPermissionsTooltip>
|
||||
),
|
||||
icon: 'invert',
|
||||
disabled: !canEditSynthetics,
|
||||
disabled: !canEditSynthetics || !canUsePublicLocations,
|
||||
onClick: () => {
|
||||
if (status !== FETCH_STATUS.LOADING) {
|
||||
updateMonitorEnabledState(!monitor.isEnabled);
|
||||
|
@ -207,11 +221,14 @@ export function ActionsPopover({
|
|||
},
|
||||
{
|
||||
name: (
|
||||
<NoPermissionsTooltip canEditSynthetics={canEditSynthetics}>
|
||||
<NoPermissionsTooltip
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
canUsePublicLocations={canUsePublicLocations}
|
||||
>
|
||||
{monitor.isStatusAlertEnabled ? disableAlertLabel : enableMonitorAlertLabel}
|
||||
</NoPermissionsTooltip>
|
||||
),
|
||||
disabled: !canEditSynthetics,
|
||||
disabled: !canEditSynthetics || !canUsePublicLocations,
|
||||
icon: alertLoading ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : monitor.isStatusAlertEnabled ? (
|
||||
|
|
|
@ -64,8 +64,7 @@ export const MetricItem = ({
|
|||
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const isErrorPopoverOpen = useSelector(selectErrorPopoverState);
|
||||
const locationName =
|
||||
useLocationName({ locationId: monitor.location.id })?.label || monitor.location?.id;
|
||||
const locationName = useLocationName(monitor);
|
||||
const { status, timestamp, ping, configIdByLocation } = useStatusByLocationOverview(
|
||||
monitor.configId,
|
||||
monitor.location.id
|
||||
|
|
|
@ -27,8 +27,7 @@ export const OverviewGridItem = ({
|
|||
monitor: MonitorOverviewItem;
|
||||
onClick: (params: FlyoutParamProps) => void;
|
||||
}) => {
|
||||
const locationName =
|
||||
useLocationName({ locationId: monitor.location?.id })?.label || monitor.location?.id;
|
||||
const locationName = useLocationName(monitor);
|
||||
|
||||
const { timestamp } = useStatusByLocationOverview(monitor.configId, locationName);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
|||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useLocationName } from './use_location_name';
|
||||
import { WrappedHelper } from '../utils/testing';
|
||||
import { MonitorOverviewItem } from '../../../../common/runtime_types';
|
||||
|
||||
describe('useLocationName', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -47,16 +48,11 @@ describe('useLocationName', () => {
|
|||
const { result } = renderHook(
|
||||
() =>
|
||||
useLocationName({
|
||||
locationId: 'us_central',
|
||||
}),
|
||||
location: { id: 'us_central' },
|
||||
} as MonitorOverviewItem),
|
||||
{ wrapper: WrapperWithState }
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
id: 'us_central',
|
||||
isServiceManaged: true,
|
||||
label: 'US Central',
|
||||
url: 'mockUrl',
|
||||
});
|
||||
expect(result.current).toEqual('US Central');
|
||||
});
|
||||
|
||||
it('returns the location id if matching location cannot be found', () => {
|
||||
|
@ -92,10 +88,10 @@ describe('useLocationName', () => {
|
|||
const { result } = renderHook(
|
||||
() =>
|
||||
useLocationName({
|
||||
locationId: 'us_west',
|
||||
}),
|
||||
location: { id: 'us_west' },
|
||||
} as MonitorOverviewItem),
|
||||
{ wrapper: WrapperWithState }
|
||||
);
|
||||
expect(result.current).toEqual(undefined);
|
||||
expect(result.current).toEqual('us_west');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { MonitorOverviewItem } from '../../../../common/runtime_types';
|
||||
import { selectServiceLocationsState, getServiceLocations } from '../state';
|
||||
|
||||
export function useLocationName({ locationId }: { locationId: string }) {
|
||||
export function useLocationName(monitor: MonitorOverviewItem) {
|
||||
const dispatch = useDispatch();
|
||||
const { locationsLoaded, locations } = useSelector(selectServiceLocationsState);
|
||||
useEffect(() => {
|
||||
|
@ -17,12 +18,14 @@ export function useLocationName({ locationId }: { locationId: string }) {
|
|||
dispatch(getServiceLocations());
|
||||
}
|
||||
});
|
||||
const locationId = monitor?.location.id;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!locationsLoaded) {
|
||||
return undefined;
|
||||
if (!locationsLoaded || monitor.location.label) {
|
||||
return monitor.location.label ?? monitor.location.id;
|
||||
} else {
|
||||
return locations.find((location) => location.id === locationId);
|
||||
const location = locations.find((loc) => loc.id === locationId);
|
||||
return location?.label ?? (monitor.location.label || monitor.location.id);
|
||||
}
|
||||
}, [locationsLoaded, locations, locationId]);
|
||||
}, [locationsLoaded, locations, locationId, monitor]);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,20 @@
|
|||
*/
|
||||
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { MonitorLocations } from '../../common/runtime_types';
|
||||
|
||||
export const useCanEditSynthetics = () => {
|
||||
return !!useKibana().services?.application?.capabilities.uptime.save;
|
||||
};
|
||||
|
||||
export const useCanUsePublicLocations = (monLocations?: MonitorLocations) => {
|
||||
const canUsePublicLocations =
|
||||
useKibana().services?.application?.capabilities.uptime.elasticManagedLocationsEnabled ?? true;
|
||||
const publicLocations = monLocations?.some((loc) => loc.isServiceManaged);
|
||||
|
||||
if (!publicLocations) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!canUsePublicLocations;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SubFeaturePrivilegeGroupConfig,
|
||||
SubFeaturePrivilegeGroupType,
|
||||
} from '@kbn/features-plugin/common';
|
||||
import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects';
|
||||
import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts';
|
||||
import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations';
|
||||
|
@ -27,6 +32,24 @@ const ruleTypes = [
|
|||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
];
|
||||
|
||||
const elasticManagedLocationsEnabledPrivilege: SubFeaturePrivilegeGroupConfig = {
|
||||
groupType: 'independent' as SubFeaturePrivilegeGroupType,
|
||||
privileges: [
|
||||
{
|
||||
id: 'elastic_managed_locations_enabled',
|
||||
name: i18n.translate('xpack.synthetics.features.elasticManagedLocations', {
|
||||
defaultMessage: 'Elastic managed locations enabled',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['elasticManagedLocationsEnabled'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const uptimeFeature = {
|
||||
id: PLUGIN.ID,
|
||||
name: PLUGIN.NAME,
|
||||
|
@ -94,4 +117,12 @@ export const uptimeFeature = {
|
|||
ui: ['show', 'alerting:save'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.synthetics.features.app', {
|
||||
defaultMessage: 'Synthetics',
|
||||
}),
|
||||
privilegeGroups: [elasticManagedLocationsEnabledPrivilege],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { validatePermissions } from './edit_monitor';
|
||||
import { SyntheticsServerSetup } from '../../types';
|
||||
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
|
||||
|
@ -39,10 +40,13 @@ export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
|
|||
const { monitorId } = request.params;
|
||||
|
||||
try {
|
||||
const errors = await deleteMonitor({
|
||||
const { errors, res } = await deleteMonitor({
|
||||
routeContext,
|
||||
monitorId,
|
||||
});
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
return response.ok({
|
||||
|
@ -68,7 +72,7 @@ export const deleteMonitor = async ({
|
|||
routeContext: RouteContext;
|
||||
monitorId: string;
|
||||
}) => {
|
||||
const { spaceId, savedObjectsClient, server, syntheticsMonitorClient } = routeContext;
|
||||
const { response, spaceId, savedObjectsClient, server, syntheticsMonitorClient } = routeContext;
|
||||
const { logger, telemetry, stackVersion } = server;
|
||||
|
||||
const { monitor, monitorWithSecret } = await getMonitorToDelete(
|
||||
|
@ -78,6 +82,17 @@ export const deleteMonitor = async ({
|
|||
spaceId
|
||||
);
|
||||
|
||||
const err = await validatePermissions(routeContext, monitor.attributes.locations);
|
||||
if (err) {
|
||||
return {
|
||||
res: response.forbidden({
|
||||
body: {
|
||||
message: err,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
let deletePromise;
|
||||
|
||||
try {
|
||||
|
@ -113,7 +128,7 @@ export const deleteMonitor = async ({
|
|||
)
|
||||
);
|
||||
|
||||
return errors;
|
||||
return { errors };
|
||||
} catch (e) {
|
||||
if (deletePromise) {
|
||||
await deletePromise;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema';
|
|||
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
|
||||
import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
|
||||
import { mergeSourceMonitor } from './helper';
|
||||
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
|
||||
|
@ -18,6 +19,7 @@ import {
|
|||
SyntheticsMonitorWithSecretsAttributes,
|
||||
SyntheticsMonitor,
|
||||
ConfigKey,
|
||||
MonitorLocations,
|
||||
} from '../../../common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { validateMonitor } from './monitor_validation';
|
||||
|
@ -39,10 +41,10 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
}),
|
||||
body: schema.any(),
|
||||
},
|
||||
writeAccess: true,
|
||||
handler: async (routeContext): Promise<any> => {
|
||||
const { request, response, savedObjectsClient, server } = routeContext;
|
||||
const { encryptedSavedObjects, logger } = server;
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
|
||||
const { logger } = server;
|
||||
const monitor = request.body as SyntheticsMonitor;
|
||||
const { monitorId } = request.params;
|
||||
|
||||
|
@ -55,14 +57,11 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
/* Decrypting the previous monitor before editing ensures that all existing fields remain
|
||||
* on the object, even in flows where decryption does not take place, such as the enabled tab
|
||||
* on the monitor list table. We do not decrypt monitors in bulk for the monitor list table */
|
||||
const decryptedPreviousMonitor =
|
||||
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
|
||||
syntheticsMonitorType,
|
||||
monitorId,
|
||||
{
|
||||
namespace: previousMonitor.namespaces?.[0],
|
||||
}
|
||||
);
|
||||
const decryptedPreviousMonitor = await getDecryptedMonitor(
|
||||
server,
|
||||
monitorId,
|
||||
previousMonitor.namespaces?.[0]!
|
||||
);
|
||||
const normalizedPreviousMonitor = normalizeSecrets(decryptedPreviousMonitor).attributes;
|
||||
|
||||
const editedMonitor = mergeSourceMonitor(normalizedPreviousMonitor, monitor);
|
||||
|
@ -74,6 +73,15 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
|
|||
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
|
||||
}
|
||||
|
||||
const err = await validatePermissions(routeContext, editedMonitor.locations);
|
||||
if (err) {
|
||||
return response.forbidden({
|
||||
body: {
|
||||
message: err,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const monitorWithRevision = {
|
||||
...validationResult.decodedMonitor,
|
||||
/* reset config hash to empty string. Ensures that the synthetics agent is able
|
||||
|
@ -230,3 +238,22 @@ export const syncEditedMonitor = async ({
|
|||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const validatePermissions = async (
|
||||
{ server, response, request }: RouteContext,
|
||||
monitorLocations: MonitorLocations
|
||||
) => {
|
||||
const hasPublicLocations = monitorLocations?.some((loc) => loc.isServiceManaged);
|
||||
if (!hasPublicLocations) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elasticManagedLocationsEnabled =
|
||||
Boolean(
|
||||
(await server.coreStart?.capabilities.resolveCapabilities(request)).uptime
|
||||
.elasticManagedLocationsEnabled
|
||||
) ?? true;
|
||||
if (!elasticManagedLocationsEnabled) {
|
||||
return "You don't have permission to use public locations";
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { toClientContract } from '../settings/private_locations/helpers';
|
||||
import { getPrivateLocationsAndAgentPolicies } from '../settings/private_locations/get_private_locations';
|
||||
import { SyntheticsRestApiRouteFactory } from '../types';
|
||||
import { getAllLocations } from '../../synthetics_service/get_all_locations';
|
||||
import { SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
|
@ -13,16 +15,37 @@ export const getServiceLocationsRoute: SyntheticsRestApiRouteFactory = () => ({
|
|||
method: 'GET',
|
||||
path: SYNTHETICS_API_URLS.SERVICE_LOCATIONS,
|
||||
validate: {},
|
||||
handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
|
||||
const { throttling, allLocations } = await getAllLocations({
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
savedObjectsClient,
|
||||
});
|
||||
handler: async ({
|
||||
request,
|
||||
server,
|
||||
savedObjectsClient,
|
||||
syntheticsMonitorClient,
|
||||
}): Promise<any> => {
|
||||
const elasticManagedLocationsEnabled =
|
||||
Boolean(
|
||||
(await server.coreStart?.capabilities.resolveCapabilities(request)).uptime
|
||||
.elasticManagedLocationsEnabled
|
||||
) ?? true;
|
||||
|
||||
return {
|
||||
locations: allLocations,
|
||||
throttling,
|
||||
};
|
||||
if (elasticManagedLocationsEnabled) {
|
||||
const { throttling, allLocations } = await getAllLocations({
|
||||
server,
|
||||
syntheticsMonitorClient,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
return {
|
||||
locations: allLocations,
|
||||
throttling,
|
||||
};
|
||||
} else {
|
||||
const { locations: privateLocations, agentPolicies } =
|
||||
await getPrivateLocationsAndAgentPolicies(savedObjectsClient, syntheticsMonitorClient);
|
||||
|
||||
const result = toClientContract({ locations: privateLocations }, agentPolicies).locations;
|
||||
return {
|
||||
locations: result,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -47,6 +47,11 @@ const getServicePublicLocations = async (
|
|||
server: SyntheticsServerSetup,
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient
|
||||
) => {
|
||||
if (!syntheticsMonitorClient.syntheticsService.isAllowed) {
|
||||
return {
|
||||
locations: [],
|
||||
};
|
||||
}
|
||||
if (syntheticsMonitorClient.syntheticsService.locations.length === 0) {
|
||||
return await getServiceLocations(server);
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'file_operations_all',
|
||||
'execute_operations_all',
|
||||
],
|
||||
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
uptime: ['all', 'read', 'minimal_all', 'minimal_read', 'elastic_managed_locations_enabled'],
|
||||
securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
|
|
@ -130,7 +130,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'file_operations_all',
|
||||
'execute_operations_all',
|
||||
],
|
||||
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
uptime: [
|
||||
'all',
|
||||
'elastic_managed_locations_enabled',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
],
|
||||
securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue