diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx
index 04d6d94d6624..a2a0ffdde34a 100644
--- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx
@@ -20,7 +20,7 @@ import * as i18n from './translations';
const InspectContainer = styled.div<{ showInspect: boolean }>`
.euiButtonIcon {
- ${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0')}
+ ${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0;')}
transition: opacity ${props => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease;
}
`;
diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap
index 098f54640e4b..5ed750b519cb 100644
--- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap
@@ -105,7 +105,7 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] =
showInspect={false}
>
{
- const selectedTimeline = options.filter(
- (option: { checked: string }) => option.checked === 'on'
- );
- if (selectedTimeline != null && selectedTimeline.length > 0 && onTimelineChange != null) {
- onTimelineChange(
- isEmpty(selectedTimeline[0].title)
- ? i18nTimeline.UNTITLED_TIMELINE
- : selectedTimeline[0].title,
- selectedTimeline[0].id
+ const handleTimelineChange = useCallback(
+ options => {
+ const selectedTimeline = options.filter(
+ (option: { checked: string }) => option.checked === 'on'
);
- }
- setIsPopoverOpen(false);
- }, []);
+ if (selectedTimeline != null && selectedTimeline.length > 0) {
+ onTimelineChange(
+ isEmpty(selectedTimeline[0].title)
+ ? i18nTimeline.UNTITLED_TIMELINE
+ : selectedTimeline[0].title,
+ selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id
+ );
+ }
+ setIsPopoverOpen(false);
+ },
+ [onTimelineChange]
+ );
const handleOnScroll = useCallback(
(
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
index f9611995cdb0..b69a8de29e04 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
@@ -15,9 +15,13 @@ import {
NewRule,
Rule,
FetchRuleProps,
+ BasicFetchProps,
} from './types';
import { throwIfNotOk } from '../../../hooks/api/api';
-import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants';
+import {
+ DETECTION_ENGINE_RULES_URL,
+ DETECTION_ENGINE_PREPACKAGED_URL,
+} from '../../../../common/constants';
/**
* Add provided Rule
@@ -199,3 +203,22 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise>(response => response.json())
);
};
+
+/**
+ * Create Prepackaged Rules
+ *
+ * @param signal AbortSignal for cancelling request
+ */
+export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => {
+ const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, {
+ method: 'PUT',
+ credentials: 'same-origin',
+ headers: {
+ 'content-type': 'application/json',
+ 'kbn-xsrf': 'true',
+ },
+ signal,
+ });
+ await throwIfNotOk(response);
+ return true;
+};
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts
index 655299c4a2a3..a329d96d444a 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts
@@ -132,3 +132,7 @@ export interface DeleteRulesProps {
export interface DuplicateRulesProps {
rules: Rules;
}
+
+export interface BasicFetchProps {
+ signal: AbortSignal;
+}
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts
index 5b5dc9e9699f..2b8f54e5438d 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts
@@ -12,3 +12,24 @@ export const SIGNAL_FETCH_FAILURE = i18n.translate(
defaultMessage: 'Failed to query signals',
}
);
+
+export const PRIVILEGE_FETCH_FAILURE = i18n.translate(
+ 'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription',
+ {
+ defaultMessage: 'Failed to query signals',
+ }
+);
+
+export const SIGNAL_GET_NAME_FAILURE = i18n.translate(
+ 'xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription',
+ {
+ defaultMessage: 'Failed to get signal index name',
+ }
+);
+
+export const SIGNAL_POST_FAILURE = i18n.translate(
+ 'xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription',
+ {
+ defaultMessage: 'Failed to create signal index',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx
index aa66df53d9fd..792ff29ad248 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx
@@ -6,10 +6,18 @@
import { useEffect, useState } from 'react';
+import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
+import { useStateToaster } from '../../../components/toasters';
import { getUserPrivilege } from './api';
+import * as i18n from './translations';
-type Return = [boolean, boolean | null, boolean | null];
-
+interface Return {
+ loading: boolean;
+ isAuthenticated: boolean | null;
+ hasIndexManage: boolean | null;
+ hasManageApiKey: boolean | null;
+ hasIndexWrite: boolean | null;
+}
/**
* Hook to get user privilege from
*
@@ -17,7 +25,10 @@ type Return = [boolean, boolean | null, boolean | null];
export const usePrivilegeUser = (): Return => {
const [loading, setLoading] = useState(true);
const [isAuthenticated, setAuthenticated] = useState(null);
- const [hasWrite, setHasWrite] = useState(null);
+ const [hasIndexManage, setHasIndexManage] = useState(null);
+ const [hasIndexWrite, setHasIndexWrite] = useState(null);
+ const [hasManageApiKey, setHasManageApiKey] = useState(null);
+ const [, dispatchToaster] = useStateToaster();
useEffect(() => {
let isSubscribed = true;
@@ -34,13 +45,21 @@ export const usePrivilegeUser = (): Return => {
setAuthenticated(privilege.isAuthenticated);
if (privilege.index != null && Object.keys(privilege.index).length > 0) {
const indexName = Object.keys(privilege.index)[0];
- setHasWrite(privilege.index[indexName].create_index);
+ setHasIndexManage(privilege.index[indexName].manage);
+ setHasIndexWrite(privilege.index[indexName].write);
+ setHasManageApiKey(
+ privilege.cluster.manage_security ||
+ privilege.cluster.manage_api_key ||
+ privilege.cluster.manage_own_api_key
+ );
}
}
} catch (error) {
if (isSubscribed) {
setAuthenticated(false);
- setHasWrite(false);
+ setHasIndexManage(false);
+ setHasIndexWrite(false);
+ errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
@@ -55,5 +74,5 @@ export const usePrivilegeUser = (): Return => {
};
}, []);
- return [loading, isAuthenticated, hasWrite];
+ return { loading, isAuthenticated, hasIndexManage, hasManageApiKey, hasIndexWrite };
};
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx
index 1ff4422cf641..189d8a1bf3f7 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx
@@ -8,9 +8,10 @@ import { useEffect, useState, useRef } from 'react';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../../components/toasters';
+import { createPrepackagedRules } from '../rules';
import { createSignalIndex, getSignalIndex } from './api';
import * as i18n from './translations';
-import { PostSignalError } from './types';
+import { PostSignalError, SignalIndexError } from './types';
type Func = () => void;
@@ -40,11 +41,15 @@ export const useSignalIndex = (): Return => {
if (isSubscribed && signal != null) {
setSignalIndexName(signal.name);
setSignalIndexExists(true);
+ createPrepackagedRules({ signal: abortCtrl.signal });
}
} catch (error) {
if (isSubscribed) {
setSignalIndexName(null);
setSignalIndexExists(false);
+ if (error instanceof SignalIndexError && error.statusCode !== 404) {
+ errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster });
+ }
}
}
if (isSubscribed) {
@@ -69,7 +74,7 @@ export const useSignalIndex = (): Return => {
} else {
setSignalIndexName(null);
setSignalIndexExists(false);
- errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster });
+ errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster });
}
}
}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx
new file mode 100644
index 000000000000..195053199845
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiCallOut, EuiButton } from '@elastic/eui';
+import React, { memo, useCallback, useState } from 'react';
+
+import * as i18n from './translations';
+
+const NoWriteSignalsCallOutComponent = () => {
+ const [showCallOut, setShowCallOut] = useState(true);
+ const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);
+
+ return showCallOut ? (
+
+ {i18n.NO_WRITE_SIGNALS_CALLOUT_MSG}
+
+ {i18n.DISMISS_CALLOUT}
+
+
+ ) : null;
+};
+
+export const NoWriteSignalsCallOut = memo(NoWriteSignalsCallOutComponent);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts
new file mode 100644
index 000000000000..065d775e1dc6
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const NO_WRITE_SIGNALS_CALLOUT_TITLE = i18n.translate(
+ 'xpack.siem.detectionEngine.noWriteSignalsCallOutTitle',
+ {
+ defaultMessage: 'Signals index permissions required',
+ }
+);
+
+export const NO_WRITE_SIGNALS_CALLOUT_MSG = i18n.translate(
+ 'xpack.siem.detectionEngine.noWriteSignalsCallOutMsg',
+ {
+ defaultMessage:
+ 'You are currently missing the required permissions to update signals. Please contact your administrator for further assistance.',
+ }
+);
+
+export const DISMISS_CALLOUT = i18n.translate(
+ 'xpack.siem.detectionEngine.dismissNoWriteSignalButton',
+ {
+ defaultMessage: 'Dismiss',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx
index 1a7ad5822a24..83b6ba690ec5 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx
@@ -168,55 +168,66 @@ export const requiredFieldsForActions = [
];
export const getSignalsActions = ({
+ canUserCRUD,
+ hasIndexWrite,
setEventsLoading,
setEventsDeleted,
createTimeline,
status,
}: {
+ canUserCRUD: boolean;
+ hasIndexWrite: boolean;
setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
createTimeline: CreateTimeline;
status: 'open' | 'closed';
-}): TimelineAction[] => [
- {
- getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => (
-
- sendSignalsToTimelineAction({ createTimeline, data: [data] })}
- iconType="tableDensityNormal"
- aria-label="Next"
- />
-
- ),
- id: 'sendSignalToTimeline',
- width: 26,
- },
- {
- getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => (
-
-
- updateSignalStatusAction({
- signalIds: [eventId],
- status,
- setEventsLoading,
- setEventsDeleted,
- })
- }
- iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'}
- aria-label="Next"
- />
-
- ),
- id: 'updateSignalStatus',
- width: 26,
- },
-];
+}): TimelineAction[] => {
+ const actions = [
+ {
+ getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => (
+
+ sendSignalsToTimelineAction({ createTimeline, data: [data] })}
+ iconType="tableDensityNormal"
+ aria-label="Next"
+ />
+
+ ),
+ id: 'sendSignalToTimeline',
+ width: 26,
+ },
+ ];
+ return canUserCRUD && hasIndexWrite
+ ? [
+ ...actions,
+ {
+ getAction: ({ eventId, data }: TimelineActionProps): JSX.Element => (
+
+
+ updateSignalStatusAction({
+ signalIds: [eventId],
+ status,
+ setEventsLoading,
+ setEventsDeleted,
+ })
+ }
+ iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'}
+ aria-label="Next"
+ />
+
+ ),
+ id: 'updateSignalStatus',
+ width: 26,
+ },
+ ]
+ : actions;
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx
index 47a78482cfb6..d149eb700ad0 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
@@ -46,6 +47,8 @@ import { useFetchIndexPatterns } from '../../../../containers/detection_engine/r
import { InputsRange } from '../../../../store/inputs/model';
import { Query } from '../../../../../../../../../src/plugins/data/common/query';
+import { HeaderSection } from '../../../../components/header_section';
+
const SIGNALS_PAGE_TIMELINE_ID = 'signals-page';
interface ReduxProps {
@@ -88,8 +91,11 @@ interface DispatchProps {
}
interface OwnProps {
+ canUserCRUD: boolean;
defaultFilters?: esFilters.Filter[];
+ hasIndexWrite: boolean;
from: number;
+ loading: boolean;
signalsIndex: string;
to: number;
}
@@ -98,6 +104,7 @@ type SignalsTableComponentProps = OwnProps & ReduxProps & DispatchProps;
export const SignalsTableComponent = React.memo(
({
+ canUserCRUD,
createTimeline,
clearEventsDeleted,
clearEventsLoading,
@@ -106,7 +113,9 @@ export const SignalsTableComponent = React.memo(
from,
globalFilters,
globalQuery,
+ hasIndexWrite,
isSelectAllChecked,
+ loading,
loadingEventIds,
removeTimelineLinkTo,
selectedEventIds,
@@ -228,8 +237,10 @@ export const SignalsTableComponent = React.memo(
(totalCount: number) => {
return (
0}
clearSelection={clearSelectionCallback}
+ hasIndexWrite={hasIndexWrite}
isFilteredToOpen={filterGroup === FILTER_OPEN}
selectAll={selectAllCallback}
selectedEventIds={selectedEventIds}
@@ -241,6 +252,8 @@ export const SignalsTableComponent = React.memo(
);
},
[
+ canUserCRUD,
+ hasIndexWrite,
clearSelectionCallback,
filterGroup,
loadingEventIds.length,
@@ -254,12 +267,14 @@ export const SignalsTableComponent = React.memo(
const additionalActions = useMemo(
() =>
getSignalsActions({
+ canUserCRUD,
+ hasIndexWrite,
createTimeline: createTimelineCallback,
setEventsLoading: setEventsLoadingCallback,
setEventsDeleted: setEventsDeletedCallback,
status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN,
}),
- [createTimelineCallback, filterGroup]
+ [canUserCRUD, createTimelineCallback, filterGroup]
);
const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]);
@@ -279,11 +294,20 @@ export const SignalsTableComponent = React.memo(
queryFields: requiredFieldsForActions,
timelineActions: additionalActions,
title: i18n.SIGNALS_TABLE_TITLE,
- selectAll,
+ selectAll: canUserCRUD ? selectAll : false,
}),
- [additionalActions, selectAll]
+ [additionalActions, canUserCRUD, selectAll]
);
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
return (
>;
+ updateSignalsStatus: UpdateSignalsStatus;
+ sendSignalsToTimeline: SendSignalsToTimeline;
+ closePopover: () => void;
+ isFilteredToOpen: boolean;
+}
/**
* Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel
*
@@ -22,15 +31,15 @@ import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group';
* @param closePopover
* @param isFilteredToOpen currently selected filter options
*/
-export const getBatchItems = (
- areEventsLoading: boolean,
- allEventsSelected: boolean,
- selectedEventIds: Readonly>,
- updateSignalsStatus: UpdateSignalsStatus,
- sendSignalsToTimeline: SendSignalsToTimeline,
- closePopover: () => void,
- isFilteredToOpen: boolean
-) => {
+export const getBatchItems = ({
+ areEventsLoading,
+ allEventsSelected,
+ selectedEventIds,
+ updateSignalsStatus,
+ sendSignalsToTimeline,
+ closePopover,
+ isFilteredToOpen,
+}: GetBatchItems) => {
const allDisabled = areEventsLoading || Object.keys(selectedEventIds).length === 0;
const sendToTimelineDisabled = allEventsSelected || uniqueRuleCount(selectedEventIds) > 1;
const filterString = isFilteredToOpen
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx
index f80de053b59b..e28fb3e06870 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx
@@ -22,6 +22,8 @@ import { TimelineNonEcsData } from '../../../../../graphql/types';
import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types';
interface SignalsUtilityBarProps {
+ canUserCRUD: boolean;
+ hasIndexWrite: boolean;
areEventsLoading: boolean;
clearSelection: () => void;
isFilteredToOpen: boolean;
@@ -34,6 +36,8 @@ interface SignalsUtilityBarProps {
}
const SignalsUtilityBarComponent: React.FC = ({
+ canUserCRUD,
+ hasIndexWrite,
areEventsLoading,
clearSelection,
totalCount,
@@ -49,15 +53,15 @@ const SignalsUtilityBarComponent: React.FC = ({
const getBatchItemsPopoverContent = useCallback(
(closePopover: () => void) => (
),
[
@@ -66,6 +70,7 @@ const SignalsUtilityBarComponent: React.FC = ({
updateSignalsStatus,
sendSignalsToTimeline,
isFilteredToOpen,
+ hasIndexWrite,
]
);
@@ -83,7 +88,7 @@ const SignalsUtilityBarComponent: React.FC = ({
- {totalCount > 0 && (
+ {canUserCRUD && hasIndexWrite && (
<>
{i18n.SELECTED_SIGNALS(
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx
new file mode 100644
index 000000000000..bbaccb788248
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx
@@ -0,0 +1,238 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { noop } from 'lodash/fp';
+import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react';
+
+import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user';
+import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index';
+import { useKibana } from '../../../../lib/kibana';
+
+export interface State {
+ canUserCRUD: boolean | null;
+ hasIndexManage: boolean | null;
+ hasIndexWrite: boolean | null;
+ hasManageApiKey: boolean | null;
+ isSignalIndexExists: boolean | null;
+ isAuthenticated: boolean | null;
+ loading: boolean;
+ signalIndexName: string | null;
+}
+
+const initialState: State = {
+ canUserCRUD: null,
+ hasIndexManage: null,
+ hasIndexWrite: null,
+ hasManageApiKey: null,
+ isSignalIndexExists: null,
+ isAuthenticated: null,
+ loading: true,
+ signalIndexName: null,
+};
+
+export type Action =
+ | { type: 'updateLoading'; loading: boolean }
+ | {
+ type: 'updateHasManageApiKey';
+ hasManageApiKey: boolean | null;
+ }
+ | {
+ type: 'updateHasIndexManage';
+ hasIndexManage: boolean | null;
+ }
+ | {
+ type: 'updateHasIndexWrite';
+ hasIndexWrite: boolean | null;
+ }
+ | {
+ type: 'updateIsSignalIndexExists';
+ isSignalIndexExists: boolean | null;
+ }
+ | {
+ type: 'updateIsAuthenticated';
+ isAuthenticated: boolean | null;
+ }
+ | {
+ type: 'updateCanUserCRUD';
+ canUserCRUD: boolean | null;
+ }
+ | {
+ type: 'updateSignalIndexName';
+ signalIndexName: string | null;
+ };
+
+export const userInfoReducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'updateLoading': {
+ return {
+ ...state,
+ loading: action.loading,
+ };
+ }
+ case 'updateHasIndexManage': {
+ return {
+ ...state,
+ hasIndexManage: action.hasIndexManage,
+ };
+ }
+ case 'updateHasIndexWrite': {
+ return {
+ ...state,
+ hasIndexWrite: action.hasIndexWrite,
+ };
+ }
+ case 'updateHasManageApiKey': {
+ return {
+ ...state,
+ hasManageApiKey: action.hasManageApiKey,
+ };
+ }
+ case 'updateIsSignalIndexExists': {
+ return {
+ ...state,
+ isSignalIndexExists: action.isSignalIndexExists,
+ };
+ }
+ case 'updateIsAuthenticated': {
+ return {
+ ...state,
+ isAuthenticated: action.isAuthenticated,
+ };
+ }
+ case 'updateCanUserCRUD': {
+ return {
+ ...state,
+ canUserCRUD: action.canUserCRUD,
+ };
+ }
+ case 'updateSignalIndexName': {
+ return {
+ ...state,
+ signalIndexName: action.signalIndexName,
+ };
+ }
+ default:
+ return state;
+ }
+};
+
+const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]);
+
+const useUserData = () => useContext(StateUserInfoContext);
+
+interface ManageUserInfoProps {
+ children: React.ReactNode;
+}
+
+export const ManageUserInfo = ({ children }: ManageUserInfoProps) => (
+
+ {children}
+
+);
+
+export const useUserInfo = (): State => {
+ const [
+ {
+ canUserCRUD,
+ hasIndexManage,
+ hasIndexWrite,
+ hasManageApiKey,
+ isSignalIndexExists,
+ isAuthenticated,
+ loading,
+ signalIndexName,
+ },
+ dispatch,
+ ] = useUserData();
+ const {
+ loading: privilegeLoading,
+ isAuthenticated: isApiAuthenticated,
+ hasIndexManage: hasApiIndexManage,
+ hasIndexWrite: hasApiIndexWrite,
+ hasManageApiKey: hasApiManageApiKey,
+ } = usePrivilegeUser();
+ const [
+ indexNameLoading,
+ isApiSignalIndexExists,
+ apiSignalIndexName,
+ createSignalIndex,
+ ] = useSignalIndex();
+
+ const uiCapabilities = useKibana().services.application.capabilities;
+ const capabilitiesCanUserCRUD: boolean =
+ typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
+
+ useEffect(() => {
+ if (loading !== privilegeLoading || indexNameLoading) {
+ dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading });
+ }
+ }, [loading, privilegeLoading, indexNameLoading]);
+
+ useEffect(() => {
+ if (hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) {
+ dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage });
+ }
+ }, [hasIndexManage, hasApiIndexManage]);
+
+ useEffect(() => {
+ if (hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) {
+ dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite });
+ }
+ }, [hasIndexWrite, hasApiIndexWrite]);
+
+ useEffect(() => {
+ if (hasManageApiKey !== hasApiManageApiKey && hasApiManageApiKey != null) {
+ dispatch({ type: 'updateHasManageApiKey', hasManageApiKey: hasApiManageApiKey });
+ }
+ }, [hasManageApiKey, hasApiManageApiKey]);
+
+ useEffect(() => {
+ if (isSignalIndexExists !== isApiSignalIndexExists && isApiSignalIndexExists != null) {
+ dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists });
+ }
+ }, [isSignalIndexExists, isApiSignalIndexExists]);
+
+ useEffect(() => {
+ if (isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) {
+ dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated });
+ }
+ }, [isAuthenticated, isApiAuthenticated]);
+
+ useEffect(() => {
+ if (canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) {
+ dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD });
+ }
+ }, [canUserCRUD, capabilitiesCanUserCRUD]);
+
+ useEffect(() => {
+ if (signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) {
+ dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName });
+ }
+ }, [signalIndexName, apiSignalIndexName]);
+
+ useEffect(() => {
+ if (
+ isAuthenticated &&
+ hasIndexManage &&
+ isSignalIndexExists != null &&
+ !isSignalIndexExists &&
+ createSignalIndex != null
+ ) {
+ createSignalIndex();
+ }
+ }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexManage]);
+
+ return {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasIndexManage,
+ hasIndexWrite,
+ hasManageApiKey,
+ signalIndexName,
+ };
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
index 2a91a559ec0e..e638cf89e77b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButton, EuiLoadingContent, EuiPanel, EuiSpacer } from '@elastic/eui';
+import { EuiButton, EuiSpacer } from '@elastic/eui';
import React, { useCallback } from 'react';
import { StickyContainer } from 'react-sticky';
@@ -18,30 +18,23 @@ import { GlobalTime } from '../../containers/global_time';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { SpyRoute } from '../../utils/route/spy_routes';
-import { SignalsTable } from './components/signals';
-import * as signalsI18n from './components/signals/translations';
-import { SignalsHistogramPanel } from './components/signals_histogram_panel';
import { Query } from '../../../../../../../src/plugins/data/common/query';
import { esFilters } from '../../../../../../../src/plugins/data/common/es_query';
-import { inputsSelectors } from '../../store/inputs';
import { State } from '../../store';
+import { inputsSelectors } from '../../store/inputs';
+import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
+import { InputsModelId } from '../../store/inputs/constants';
import { InputsRange } from '../../store/inputs/model';
-import { signalsHistogramOptions } from './components/signals_histogram_panel/config';
import { useSignalInfo } from './components/signals_info';
+import { SignalsTable } from './components/signals';
+import { NoWriteSignalsCallOut } from './components/no_write_signals_callout';
+import { SignalsHistogramPanel } from './components/signals_histogram_panel';
+import { signalsHistogramOptions } from './components/signals_histogram_panel/config';
+import { useUserInfo } from './components/user_info';
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
import { DetectionEngineNoIndex } from './detection_engine_no_signal_index';
import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated';
import * as i18n from './translations';
-import { HeaderSection } from '../../components/header_section';
-import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
-import { InputsModelId } from '../../store/inputs/constants';
-
-interface OwnProps {
- loading: boolean;
- isSignalIndexExists: boolean | null;
- isUserAuthenticated: boolean | null;
- signalsIndex: string | null;
-}
interface ReduxProps {
filters: esFilters.Filter[];
@@ -56,18 +49,19 @@ export interface DispatchProps {
}>;
}
-type DetectionEngineComponentProps = OwnProps & ReduxProps & DispatchProps;
-
-export const DetectionEngineComponent = React.memo(
- ({
- filters,
- loading,
- isSignalIndexExists,
- isUserAuthenticated,
- query,
- setAbsoluteRangeDatePicker,
- signalsIndex,
- }) => {
+type DetectionEngineComponentProps = ReduxProps & DispatchProps;
+
+const DetectionEngineComponent = React.memo(
+ ({ filters, query, setAbsoluteRangeDatePicker }) => {
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated: isUserAuthenticated,
+ canUserCRUD,
+ signalIndexName,
+ hasIndexWrite,
+ } = useUserInfo();
+
const [lastSignals] = useSignalInfo({});
const updateDateRangeCallback = useCallback(
@@ -95,6 +89,7 @@ export const DetectionEngineComponent = React.memo
+ {hasIndexWrite != null && !hasIndexWrite && }
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
@@ -102,7 +97,6 @@ export const DetectionEngineComponent = React.memo
-
- {!loading ? (
- isSignalIndexExists && (
-
- )
- ) : (
-
-
-
-
- )}
+
>
)}
@@ -160,7 +152,6 @@ export const DetectionEngineComponent = React.memo
-
>
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
index c32cab7f933b..c4e83429aebd 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
@@ -4,70 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useEffect } from 'react';
+import React from 'react';
import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom';
-import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index';
-import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user';
-
import { CreateRuleComponent } from './rules/create';
import { DetectionEngine } from './detection_engine';
import { EditRuleComponent } from './rules/edit';
import { RuleDetails } from './rules/details';
import { RulesComponent } from './rules';
+import { ManageUserInfo } from './components/user_info';
const detectionEnginePath = `/:pageName(detection-engine)`;
type Props = Partial> & { url: string };
-export const DetectionEngineContainer = React.memo(() => {
- const [privilegeLoading, isAuthenticated, hasWrite] = usePrivilegeUser();
- const [
- indexNameLoading,
- isSignalIndexExists,
- signalIndexName,
- createSignalIndex,
- ] = useSignalIndex();
-
- useEffect(() => {
- if (
- isAuthenticated &&
- hasWrite &&
- isSignalIndexExists != null &&
- !isSignalIndexExists &&
- createSignalIndex != null
- ) {
- createSignalIndex();
- }
- }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasWrite]);
-
- return (
+export const DetectionEngineContainer = React.memo(() => (
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
- {isSignalIndexExists && isAuthenticated && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
(
@@ -75,6 +43,6 @@ export const DetectionEngineContainer = React.memo(() => {
)}
/>
- );
-});
+
+));
DetectionEngineContainer.displayName = 'DetectionEngineContainer';
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx
index 42c4bb1d0ef9..95b9c9324894 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx
@@ -68,111 +68,121 @@ const getActions = (dispatch: React.Dispatch, history: H.History) => [
},
];
+type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType;
+
// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes?
export const getColumns = (
dispatch: React.Dispatch,
- history: H.History
-): Array | EuiTableActionsColumnType> => [
- {
- field: 'rule',
- name: i18n.COLUMN_RULE,
- render: (value: TableData['rule']) => {value.name},
- truncateText: true,
- width: '24%',
- },
- {
- field: 'method',
- name: i18n.COLUMN_METHOD,
- truncateText: true,
- },
- {
- field: 'severity',
- name: i18n.COLUMN_SEVERITY,
- render: (value: TableData['severity']) => (
-
- {value}
-
- ),
- truncateText: true,
- },
- {
- field: 'lastCompletedRun',
- name: i18n.COLUMN_LAST_COMPLETE_RUN,
- render: (value: TableData['lastCompletedRun']) => {
- return value == null ? (
- getEmptyTagValue()
- ) : (
-
- );
+ history: H.History,
+ hasNoPermissions: boolean
+): RulesColumns[] => {
+ const cols: RulesColumns[] = [
+ {
+ field: 'rule',
+ name: i18n.COLUMN_RULE,
+ render: (value: TableData['rule']) => {value.name},
+ truncateText: true,
+ width: '24%',
},
- sortable: true,
- truncateText: true,
- width: '16%',
- },
- {
- field: 'lastResponse',
- name: i18n.COLUMN_LAST_RESPONSE,
- render: (value: TableData['lastResponse']) => {
- return value == null ? (
- getEmptyTagValue()
- ) : (
- <>
- {value.type === 'Fail' ? (
-
- {value.type}
-
- ) : (
- {value.type}
- )}
- >
- );
+ {
+ field: 'method',
+ name: i18n.COLUMN_METHOD,
+ truncateText: true,
},
- truncateText: true,
- },
- {
- field: 'tags',
- name: i18n.COLUMN_TAGS,
- render: (value: TableData['tags']) => (
-
- <>
- {value.map((tag, i) => (
-
- {tag}
-
- ))}
- >
-
- ),
- truncateText: true,
- width: '20%',
- },
- {
- align: 'center',
- field: 'activate',
- name: i18n.COLUMN_ACTIVATE,
- render: (value: TableData['activate'], item: TableData) => (
-
- ),
- sortable: true,
- width: '85px',
- },
- {
- actions: getActions(dispatch, history),
- width: '40px',
- } as EuiTableActionsColumnType,
-];
+ {
+ field: 'severity',
+ name: i18n.COLUMN_SEVERITY,
+ render: (value: TableData['severity']) => (
+
+ {value}
+
+ ),
+ truncateText: true,
+ },
+ {
+ field: 'lastCompletedRun',
+ name: i18n.COLUMN_LAST_COMPLETE_RUN,
+ render: (value: TableData['lastCompletedRun']) => {
+ return value == null ? (
+ getEmptyTagValue()
+ ) : (
+
+ );
+ },
+ sortable: true,
+ truncateText: true,
+ width: '16%',
+ },
+ {
+ field: 'lastResponse',
+ name: i18n.COLUMN_LAST_RESPONSE,
+ render: (value: TableData['lastResponse']) => {
+ return value == null ? (
+ getEmptyTagValue()
+ ) : (
+ <>
+ {value.type === 'Fail' ? (
+
+ {value.type}
+
+ ) : (
+ {value.type}
+ )}
+ >
+ );
+ },
+ truncateText: true,
+ },
+ {
+ field: 'tags',
+ name: i18n.COLUMN_TAGS,
+ render: (value: TableData['tags']) => (
+
+ <>
+ {value.map((tag, i) => (
+
+ {tag}
+
+ ))}
+ >
+
+ ),
+ truncateText: true,
+ width: '20%',
+ },
+ {
+ align: 'center',
+ field: 'activate',
+ name: i18n.COLUMN_ACTIVATE,
+ render: (value: TableData['activate'], item: TableData) => (
+
+ ),
+ sortable: true,
+ width: '85px',
+ },
+ ];
+ const actions: RulesColumns[] = [
+ {
+ actions: getActions(dispatch, history),
+ width: '40px',
+ } as EuiTableActionsColumnType,
+ ];
+
+ return hasNoPermissions ? cols : [...cols, ...actions];
+};
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx
index 060f8baccc3b..e900058b6c53 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx
@@ -11,7 +11,7 @@ import {
EuiLoadingContent,
EuiSpacer,
} from '@elastic/eui';
-import React, { useCallback, useEffect, useReducer, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { useHistory } from 'react-router-dom';
import uuid from 'uuid';
@@ -60,7 +60,11 @@ const initialState: State = {
* * Delete
* * Import/Export
*/
-export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => {
+export const AllRules = React.memo<{
+ hasNoPermissions: boolean;
+ importCompleteToggle: boolean;
+ loading: boolean;
+}>(({ hasNoPermissions, importCompleteToggle, loading }) => {
const [
{
exportPayload,
@@ -111,6 +115,15 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp
});
}, [rulesData]);
+ const euiBasicTableSelectionProps = useMemo(
+ () => ({
+ selectable: (item: TableData) => !item.isLoading,
+ onSelectionChange: (selected: TableData[]) =>
+ dispatch({ type: 'setSelected', selectedItems: selected }),
+ }),
+ []
+ );
+
return (
<>
(importComp
{i18n.SELECTED_RULES(selectedItems.length)}
-
- {i18n.BATCH_ACTIONS}
-
+ {!hasNoPermissions && (
+
+ {i18n.BATCH_ACTIONS}
+
+ )}
(importComp
{
@@ -204,14 +219,12 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp
totalItemCount: pagination.total,
pageSizeOptions: [5, 10, 20],
}}
- selection={{
- selectable: (item: TableData) => !item.isLoading,
- onSelectionChange: (selected: TableData[]) =>
- dispatch({ type: 'setSelected', selectedItems: selected }),
- }}
sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }}
+ selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps}
/>
- {isLoading && }
+ {(isLoading || loading) && (
+
+ )}
>
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx
new file mode 100644
index 000000000000..6ec76bacc232
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiCallOut, EuiButton } from '@elastic/eui';
+import React, { memo, useCallback, useState } from 'react';
+
+import * as i18n from './translations';
+
+const ReadOnlyCallOutComponent = () => {
+ const [showCallOut, setShowCallOut] = useState(true);
+ const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);
+
+ return showCallOut ? (
+
+ {i18n.READ_ONLY_CALLOUT_MSG}
+
+ {i18n.DISMISS_CALLOUT}
+
+
+ ) : null;
+};
+
+export const ReadOnlyCallOut = memo(ReadOnlyCallOutComponent);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts
new file mode 100644
index 000000000000..c3429f436503
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const READ_ONLY_CALLOUT_TITLE = i18n.translate(
+ 'xpack.siem.detectionEngine.readOnlyCallOutTitle',
+ {
+ defaultMessage: 'Rule permissions required',
+ }
+);
+
+export const READ_ONLY_CALLOUT_MSG = i18n.translate(
+ 'xpack.siem.detectionEngine.readOnlyCallOutMsg',
+ {
+ defaultMessage:
+ 'You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance.',
+ }
+);
+
+export const DISMISS_CALLOUT = i18n.translate('xpack.siem.detectionEngine.dismissButton', {
+ defaultMessage: 'Dismiss',
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap
index f264dde07c59..604f86866d56 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap
@@ -11,7 +11,6 @@ exports[`RuleSwitch renders correctly against snapshot 1`] = `
;
id: string;
enabled: boolean;
+ isDisabled?: boolean;
isLoading?: boolean;
optionLabel?: string;
}
@@ -42,6 +43,7 @@ export interface RuleSwitchProps {
export const RuleSwitchComponent = ({
dispatch,
id,
+ isDisabled,
isLoading,
enabled,
optionLabel,
@@ -92,7 +94,7 @@ export const RuleSwitchComponent = ({
data-test-subj="rule-switch"
label={optionLabel ?? ''}
showLabel={!isEmpty(optionLabel)}
- disabled={false}
+ disabled={isDisabled}
checked={myEnabled}
onChange={onRuleStateChange}
/>
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts
index 12bbdbdfff3e..ce91e15cdcf0 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts
@@ -85,8 +85,12 @@ const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson =>
false_positives: falsePositives.filter(item => !isEmpty(item)),
references: references.filter(item => !isEmpty(item)),
risk_score: riskScore,
- timeline_id: timeline.id,
- timeline_title: timeline.title,
+ ...(timeline.id != null && timeline.title != null
+ ? {
+ timeline_id: timeline.id,
+ timeline_title: timeline.title,
+ }
+ : {}),
threats: threats
.filter(threat => threat.tactic.name !== 'none')
.map(threat => ({
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
index 848b17aadbff..9a0f41bbd8c5 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx
@@ -14,6 +14,7 @@ import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redir
import { WrapperPage } from '../../../../components/wrapper_page';
import { usePersistRule } from '../../../../containers/detection_engine/rules';
import { SpyRoute } from '../../../../utils/route/spy_routes';
+import { useUserInfo } from '../../components/user_info';
import { AccordionTitle } from '../components/accordion_title';
import { FormData, FormHook } from '../components/shared_imports';
import { StepAboutRule } from '../components/step_about_rule';
@@ -56,6 +57,13 @@ const MyEuiPanel = styled(EuiPanel)`
`;
export const CreateRuleComponent = React.memo(() => {
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ } = useUserInfo();
const [heightAccordion, setHeightAccordion] = useState(-1);
const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule);
const defineRuleRef = useRef(null);
@@ -77,6 +85,18 @@ export const CreateRuleComponent = React.memo(() => {
[RuleStep.scheduleRule]: false,
});
const [{ isLoading, isSaved }, setRule] = usePersistRule();
+ const userHasNoPermissions =
+ canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
+
+ if (
+ isSignalIndexExists != null &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ } else if (userHasNoPermissions) {
+ return ;
+ }
const setStepData = useCallback(
(step: RuleStep, data: unknown, isValid: boolean) => {
@@ -216,7 +236,7 @@ export const CreateRuleComponent = React.memo(() => {
@@ -242,7 +262,7 @@ export const CreateRuleComponent = React.memo(() => {
setHeightAccordion(height)}
@@ -273,7 +293,7 @@ export const CreateRuleComponent = React.memo(() => {
@@ -303,7 +323,7 @@ export const CreateRuleComponent = React.memo(() => {
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
index 9b6998ab4a13..679f42f02519 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
@@ -7,7 +7,7 @@
import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { memo, useCallback, useMemo } from 'react';
-import { useParams } from 'react-router-dom';
+import { Redirect, useParams } from 'react-router-dom';
import { StickyContainer } from 'react-sticky';
import { ActionCreator } from 'typescript-fsa';
@@ -28,13 +28,16 @@ import { SpyRoute } from '../../../../utils/route/spy_routes';
import { SignalsHistogramPanel } from '../../components/signals_histogram_panel';
import { SignalsTable } from '../../components/signals';
+import { useUserInfo } from '../../components/user_info';
import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page';
import { useSignalInfo } from '../../components/signals_info';
import { StepAboutRule } from '../components/step_about_rule';
import { StepDefineRule } from '../components/step_define_rule';
import { StepScheduleRule } from '../components/step_schedule_rule';
import { buildSignalsRuleIdFilter } from '../../components/signals/default_config';
+import { NoWriteSignalsCallOut } from '../../components/no_write_signals_callout';
import * as detectionI18n from '../../translations';
+import { ReadOnlyCallOut } from '../components/read_only_callout';
import { RuleSwitch } from '../components/rule_switch';
import { StepPanel } from '../components/step_panel';
import { getStepsData } from '../helpers';
@@ -50,10 +53,6 @@ import { State } from '../../../../store';
import { InputsRange } from '../../../../store/inputs/model';
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions';
-interface OwnProps {
- signalsIndex: string | null;
-}
-
interface ReduxProps {
filters: esFilters.Filter[];
query: Query;
@@ -67,22 +66,41 @@ export interface DispatchProps {
}>;
}
-type RuleDetailsComponentProps = OwnProps & ReduxProps & DispatchProps;
+type RuleDetailsComponentProps = ReduxProps & DispatchProps;
const RuleDetailsComponent = memo(
- ({ filters, query, setAbsoluteRangeDatePicker, signalsIndex }) => {
+ ({ filters, query, setAbsoluteRangeDatePicker }) => {
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ hasIndexWrite,
+ signalIndexName,
+ } = useUserInfo();
const { ruleId } = useParams();
- const [loading, rule] = useRule(ruleId);
+ const [isLoading, rule] = useRule(ruleId);
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
rule,
detailsView: true,
});
const [lastSignals] = useSignalInfo({ ruleId });
+ const userHasNoPermissions =
+ canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
+
+ if (
+ isSignalIndexExists != null &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ }
- const title = loading === true || rule === null ? : rule.name;
+ const title = isLoading === true || rule === null ? : rule.name;
const subTitle = useMemo(
() =>
- loading === true || rule === null ? (
+ isLoading === true || rule === null ? (
) : (
[
@@ -118,7 +136,7 @@ const RuleDetailsComponent = memo(
),
]
),
- [loading, rule]
+ [isLoading, rule]
);
const signalDefaultFilters = useMemo(
@@ -140,6 +158,8 @@ const RuleDetailsComponent = memo(
return (
<>
+ {hasIndexWrite != null && !hasIndexWrite && }
+ {userHasNoPermissions && }
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
@@ -175,6 +195,7 @@ const RuleDetailsComponent = memo(
@@ -186,7 +207,7 @@ const RuleDetailsComponent = memo(
{ruleI18n.EDIT_RULE_SETTINGS}
@@ -200,7 +221,7 @@ const RuleDetailsComponent = memo(
-
+
{defineRuleData != null && (
(
-
+
{aboutRuleData != null && (
(
-
+
{scheduleRuleData != null && (
(
{ruleId != null && (
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx
index 10b7f0e832f1..e583461f5243 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx
@@ -22,6 +22,7 @@ import { WrapperPage } from '../../../../components/wrapper_page';
import { SpyRoute } from '../../../../utils/route/spy_routes';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
+import { useUserInfo } from '../../components/user_info';
import { FormHook, FormData } from '../components/shared_imports';
import { StepPanel } from '../components/step_panel';
import { StepAboutRule } from '../components/step_about_rule';
@@ -47,9 +48,28 @@ interface ScheduleStepRuleForm extends StepRuleForm {
}
export const EditRuleComponent = memo(() => {
+ const {
+ loading: initLoading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ } = useUserInfo();
const { ruleId } = useParams();
const [loading, rule] = useRule(ruleId);
+ const userHasNoPermissions =
+ canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
+ if (
+ isSignalIndexExists != null &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ } else if (userHasNoPermissions) {
+ return ;
+ }
+
const [initForm, setInitForm] = useState(false);
const [myAboutRuleForm, setMyAboutRuleForm] = useState({
data: null,
@@ -89,7 +109,7 @@ export const EditRuleComponent = memo(() => {
content: (
<>
-
+
{myDefineRuleForm.data != null && (
{
content: (
<>
-
+
{myAboutRuleForm.data != null && (
{
content: (
<>
-
+
{myScheduleRuleForm.data != null && (
{
],
[
loading,
+ initLoading,
isLoading,
myAboutRuleForm,
myDefineRuleForm,
@@ -310,7 +331,13 @@ export const EditRuleComponent = memo(() => {
-
+
{i18n.SAVE_CHANGES}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx
index 47b5c1051bcf..cc0882dd7e42 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx
@@ -5,6 +5,7 @@
*/
import { pick } from 'lodash/fp';
+import { useLocation } from 'react-router-dom';
import { esFilters } from '../../../../../../../../src/plugins/data/public';
import { Rule } from '../../../containers/detection_engine/rules';
@@ -64,3 +65,5 @@ export const getStepsData = ({
return { aboutRuleData, defineRuleData, scheduleRuleData };
};
+
+export const useQuery = () => new URLSearchParams(useLocation().search);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx
index ef67f0a7d22c..dd46b33ca725 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx
@@ -5,8 +5,9 @@
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
+import React, { useState } from 'react';
+import { Redirect } from 'react-router-dom';
import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine';
import { FormattedRelativePreferenceDate } from '../../../components/formatted_date';
@@ -17,15 +18,34 @@ import { SpyRoute } from '../../../utils/route/spy_routes';
import { AllRules } from './all';
import { ImportRuleModal } from './components/import_rule_modal';
+import { ReadOnlyCallOut } from './components/read_only_callout';
+import { useUserInfo } from '../components/user_info';
import * as i18n from './translations';
export const RulesComponent = React.memo(() => {
const [showImportModal, setShowImportModal] = useState(false);
const [importCompleteToggle, setImportCompleteToggle] = useState(false);
+ const {
+ loading,
+ isSignalIndexExists,
+ isAuthenticated,
+ canUserCRUD,
+ hasManageApiKey,
+ } = useUserInfo();
+ if (
+ isSignalIndexExists != null &&
+ isAuthenticated != null &&
+ (!isSignalIndexExists || !isAuthenticated)
+ ) {
+ return ;
+ }
+ const userHasNoPermissions =
+ canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
const lastCompletedRun = undefined;
return (
<>
+ {userHasNoPermissions && }
setShowImportModal(false)}
@@ -56,6 +76,7 @@ export const RulesComponent = React.memo(() => {
{
setShowImportModal(true);
}}
@@ -63,20 +84,23 @@ export const RulesComponent = React.memo(() => {
{i18n.IMPORT_RULE}
-
{i18n.ADD_NEW_RULE}
-
-
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts
index ec4206623bad..541b058951be 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts
@@ -109,8 +109,8 @@ export interface AboutStepRuleJson {
references: string[];
false_positives: string[];
tags: string[];
- timeline_id: string | null;
- timeline_title: string | null;
+ timeline_id?: string;
+ timeline_title?: string;
threats: IMitreEnterpriseAttack[];
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts
new file mode 100644
index 000000000000..cb358c15e5fa
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getIndexExists } from './get_index_exists';
+
+class StatusCode extends Error {
+ status: number = -1;
+ constructor(status: number, message: string) {
+ super(message);
+ this.status = status;
+ }
+}
+
+describe('get_index_exists', () => {
+ test('it should return a true if you have _shards', async () => {
+ const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 1 } });
+ const indexExists = await getIndexExists(callWithRequest, 'some-index');
+ expect(indexExists).toEqual(true);
+ });
+
+ test('it should return a false if you do NOT have _shards', async () => {
+ const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 0 } });
+ const indexExists = await getIndexExists(callWithRequest, 'some-index');
+ expect(indexExists).toEqual(false);
+ });
+
+ test('it should return a false if it encounters a 404', async () => {
+ const callWithRequest = jest.fn().mockImplementation(() => {
+ throw new StatusCode(404, 'I am a 404 error');
+ });
+ const indexExists = await getIndexExists(callWithRequest, 'some-index');
+ expect(indexExists).toEqual(false);
+ });
+
+ test('it should reject if it encounters a non 404', async () => {
+ const callWithRequest = jest.fn().mockImplementation(() => {
+ throw new StatusCode(500, 'I am a 500 error');
+ });
+ await expect(getIndexExists(callWithRequest, 'some-index')).rejects.toThrow('I am a 500 error');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts
index ff65caa59a86..705f542b5012 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts
@@ -4,15 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IndicesExistsParams } from 'elasticsearch';
-import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
import { CallWithRequest } from '../types';
export const getIndexExists = async (
- callWithRequest: CallWithRequest,
+ callWithRequest: CallWithRequest<
+ { index: string; size: number; terminate_after: number; allow_no_indices: boolean },
+ {},
+ { _shards: { total: number } }
+ >,
index: string
): Promise => {
- return callWithRequest('indices.exists', {
- index,
- });
+ try {
+ const response = await callWithRequest('search', {
+ index,
+ size: 0,
+ terminate_after: 1,
+ allow_no_indices: true,
+ });
+ return response._shards.total > 0;
+ } catch (err) {
+ if (err.status === 404) {
+ return false;
+ } else {
+ throw err;
+ }
+ }
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
index 094449a5f61a..10dc14f7ed61 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
@@ -28,7 +28,9 @@ describe('create_rules', () => {
jest.resetAllMocks();
({ server, alertsClient, actionsClient, elasticsearch } = createMockServer());
elasticsearch.getCluster = jest.fn().mockImplementation(() => ({
- callWithRequest: jest.fn().mockImplementation(() => true),
+ callWithRequest: jest
+ .fn()
+ .mockImplementation((endpoint, params) => ({ _shards: { total: 1 } })),
}));
createRulesRoute(server);
diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts
index 8a47aa2a2708..90ae79ef19d5 100644
--- a/x-pack/legacy/plugins/siem/server/plugin.ts
+++ b/x-pack/legacy/plugins/siem/server/plugin.ts
@@ -60,7 +60,7 @@ export class Plugin {
],
read: ['config'],
},
- ui: ['show'],
+ ui: ['show', 'crud'],
},
read: {
api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'],
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts
index 79a4eeb6dc48..777471e209ad 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts
@@ -99,7 +99,7 @@ export const setup = async (): Promise => {
const tabs = ['snapshots', 'repositories'];
testBed
- .find('tab')
+ .find(`${tab}_tab`)
.at(tabs.indexOf(tab))
.simulate('click');
};
@@ -360,7 +360,10 @@ export type TestSubjects =
| 'state'
| 'state.title'
| 'state.value'
- | 'tab'
+ | 'repositories_tab'
+ | 'snapshots_tab'
+ | 'policies_tab'
+ | 'restore_status_tab'
| 'tableHeaderCell_durationInMillis_3'
| 'tableHeaderCell_durationInMillis_3.tableHeaderSortButton'
| 'tableHeaderCell_indices_4'
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
index d9f2c1b510a1..cb2e94df7560 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
@@ -95,6 +95,16 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
+ const setCleanupRepositoryResponse = (response?: HttpResponse, error?: any) => {
+ const status = error ? error.status || 503 : 200;
+
+ server.respondWith('POST', `${API_BASE_PATH}repositories/:name/cleanup`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
const setGetPolicyResponse = (response?: HttpResponse) => {
server.respondWith('GET', `${API_BASE_PATH}policy/:name`, [
200,
@@ -113,6 +123,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setLoadIndicesResponse,
setAddPolicyResponse,
setGetPolicyResponse,
+ setCleanupRepositoryResponse,
};
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
index aa659441043a..517c7a0059a7 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
@@ -88,8 +88,15 @@ describe('', () => {
test('should have 4 tabs', () => {
const { find } = testBed;
- expect(find('tab').length).toBe(4);
- expect(find('tab').map(t => t.text())).toEqual([
+ const tabs = [
+ find('snapshots_tab'),
+ find('repositories_tab'),
+ find('policies_tab'),
+ find('restore_status_tab'),
+ ];
+
+ expect(tabs.length).toBe(4);
+ expect(tabs.map(t => t.text())).toEqual([
'Snapshots',
'Repositories',
'Policies',
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts
index 5900d53afa0b..b9b26c559032 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/types/repository.ts
@@ -157,3 +157,15 @@ export interface InvalidRepositoryVerification {
}
export type RepositoryVerification = ValidRepositoryVerification | InvalidRepositoryVerification;
+
+export interface SuccessfulRepositoryCleanup {
+ cleaned: true;
+ response: object;
+}
+
+export interface FailedRepositoryCleanup {
+ cleaned: false;
+ error: object;
+}
+
+export type RepositoryCleanup = FailedRepositoryCleanup | SuccessfulRepositoryCleanup;
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts
index 844394deb4f8..481516479df4 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts
@@ -103,6 +103,7 @@ export const UIM_REPOSITORY_DELETE = 'repository_delete';
export const UIM_REPOSITORY_DELETE_MANY = 'repository_delete_many';
export const UIM_REPOSITORY_SHOW_DETAILS_CLICK = 'repository_show_details_click';
export const UIM_REPOSITORY_DETAIL_PANEL_VERIFY = 'repository_detail_panel_verify';
+export const UIM_REPOSITORY_DETAIL_PANEL_CLEANUP = 'repository_detail_panel_cleanup';
export const UIM_SNAPSHOT_LIST_LOAD = 'snapshot_list_load';
export const UIM_SNAPSHOT_SHOW_DETAILS_CLICK = 'snapshot_show_details_click';
export const UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB = 'snapshot_detail_panel_summary_tab';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx
index 35d5c0b610b3..f89aa869b336 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/home.tsx
@@ -150,7 +150,7 @@ export const SnapshotRestoreHome: React.FunctionComponent onSectionChange(tab.id)}
isSelected={tab.id === section}
key={tab.id}
- data-test-subj="tab"
+ data-test-subj={tab.id.toLowerCase() + '_tab'}
>
{tab.name}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx
index c03162bae8bd..0a3fcfc2ec6e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx
@@ -8,7 +8,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
- EuiCodeEditor,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
@@ -19,6 +18,8 @@ import {
EuiLink,
EuiSpacer,
EuiTitle,
+ EuiCodeBlock,
+ EuiText,
} from '@elastic/eui';
import 'brace/theme/textmate';
@@ -28,12 +29,17 @@ import { documentationLinksService } from '../../../../services/documentation';
import {
useLoadRepository,
verifyRepository as verifyRepositoryRequest,
+ cleanupRepository as cleanupRepositoryRequest,
} from '../../../../services/http';
import { textService } from '../../../../services/text';
import { linkToSnapshots, linkToEditRepository } from '../../../../services/navigation';
import { REPOSITORY_TYPES } from '../../../../../../common/constants';
-import { Repository, RepositoryVerification } from '../../../../../../common/types';
+import {
+ Repository,
+ RepositoryVerification,
+ RepositoryCleanup,
+} from '../../../../../../common/types';
import {
RepositoryDeleteProvider,
SectionError,
@@ -61,7 +67,9 @@ export const RepositoryDetails: React.FunctionComponent = ({
const { FormattedMessage } = i18n;
const { error, data: repositoryDetails } = useLoadRepository(repositoryName);
const [verification, setVerification] = useState(undefined);
+ const [cleanup, setCleanup] = useState(undefined);
const [isLoadingVerification, setIsLoadingVerification] = useState(false);
+ const [isLoadingCleanup, setIsLoadingCleanup] = useState(false);
const verifyRepository = async () => {
setIsLoadingVerification(true);
@@ -70,11 +78,20 @@ export const RepositoryDetails: React.FunctionComponent = ({
setIsLoadingVerification(false);
};
- // Reset verification state when repository name changes, either from adjust URL or clicking
+ const cleanupRepository = async () => {
+ setIsLoadingCleanup(true);
+ const { data } = await cleanupRepositoryRequest(repositoryName);
+ setCleanup(data.cleanup);
+ setIsLoadingCleanup(false);
+ };
+
+ // Reset verification state and cleanup when repository name changes, either from adjust URL or clicking
// into a different repository in table list.
useEffect(() => {
setVerification(undefined);
setIsLoadingVerification(false);
+ setCleanup(undefined);
+ setIsLoadingCleanup(false);
}, [repositoryName]);
const renderBody = () => {
@@ -231,6 +248,8 @@ export const RepositoryDetails: React.FunctionComponent = ({
{renderVerification()}
+
+ {renderCleanup()}
);
};
@@ -260,36 +279,13 @@ export const RepositoryDetails: React.FunctionComponent = ({
{verification ? (
-
+ {JSON.stringify(
verification.valid ? verification.response : verification.error,
null,
2
)}
- setOptions={{
- showLineNumbers: false,
- tabSize: 2,
- maxLines: Infinity,
- }}
- editorProps={{
- $blockScrolling: Infinity,
- }}
- showGutter={false}
- minLines={6}
- aria-label={
-
- }
- />
+
) : null}
@@ -318,6 +314,78 @@ export const RepositoryDetails: React.FunctionComponent = ({
);
+ const renderCleanup = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {cleanup ? (
+ <>
+
+ {cleanup.cleaned ? (
+
+
+
+
+
+
+
+ {JSON.stringify(cleanup.response, null, 2)}
+
+
+ ) : (
+
+
+ {cleanup.error
+ ? JSON.stringify(cleanup.error)
+ : i18n.translate('xpack.snapshotRestore.repositoryDetails.cleanupUnknownError', {
+ defaultMessage: '503: Unknown error',
+ })}
+
+
+ )}
+ >
+ ) : null}
+
+
+
+
+ >
+ );
+
const renderFooter = () => {
return (
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx
index 4b5270b44d59..1df06f67c35b 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx
@@ -96,6 +96,7 @@ export const RepositoryTable: React.FunctionComponent = ({
},
},
{
+ field: 'actions',
name: i18n.translate('xpack.snapshotRestore.repositoryList.table.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
@@ -302,8 +303,8 @@ export const RepositoryTable: React.FunctionComponent = ({
rowProps={() => ({
'data-test-subj': 'row',
})}
- cellProps={() => ({
- 'data-test-subj': 'cell',
+ cellProps={(item, field) => ({
+ 'data-test-subj': `${field.name}_cell`,
})}
data-test-subj="repositoryTable"
/>
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts
index 171e949ccee7..b92f21ea6a9b 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/repository_requests.ts
@@ -11,6 +11,7 @@ import {
UIM_REPOSITORY_DELETE,
UIM_REPOSITORY_DELETE_MANY,
UIM_REPOSITORY_DETAIL_PANEL_VERIFY,
+ UIM_REPOSITORY_DETAIL_PANEL_CLEANUP,
} from '../../constants';
import { uiMetricService } from '../ui_metric';
import { httpService } from './http';
@@ -44,6 +45,20 @@ export const verifyRepository = async (name: Repository['name']) => {
return result;
};
+export const cleanupRepository = async (name: Repository['name']) => {
+ const result = await sendRequest({
+ path: httpService.addBasePath(
+ `${API_BASE_PATH}repositories/${encodeURIComponent(name)}/cleanup`
+ ),
+ method: 'post',
+ body: undefined,
+ });
+
+ const { trackUiMetric } = uiMetricService;
+ trackUiMetric(UIM_REPOSITORY_DETAIL_PANEL_CLEANUP);
+ return result;
+};
+
export const useLoadRepositoryTypes = () => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}repository_types`),
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_sr.ts
similarity index 73%
rename from x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts
rename to x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_sr.ts
index 82fe30aaa7d2..794bf99c3d91 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_sr.ts
@@ -7,10 +7,10 @@
export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => {
const ca = components.clientAction.factory;
- Client.prototype.slm = components.clientAction.namespaceFactory();
- const slm = Client.prototype.slm.prototype;
+ Client.prototype.sr = components.clientAction.namespaceFactory();
+ const sr = Client.prototype.sr.prototype;
- slm.policies = ca({
+ sr.policies = ca({
urls: [
{
fmt: '/_slm/policy',
@@ -19,7 +19,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'GET',
});
- slm.policy = ca({
+ sr.policy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>',
@@ -33,7 +33,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'GET',
});
- slm.deletePolicy = ca({
+ sr.deletePolicy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>',
@@ -47,7 +47,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'DELETE',
});
- slm.executePolicy = ca({
+ sr.executePolicy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>/_execute',
@@ -61,7 +61,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'PUT',
});
- slm.updatePolicy = ca({
+ sr.updatePolicy = ca({
urls: [
{
fmt: '/_slm/policy/<%=name%>',
@@ -75,7 +75,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
method: 'PUT',
});
- slm.executeRetention = ca({
+ sr.executeRetention = ca({
urls: [
{
fmt: '/_slm/_execute_retention',
@@ -83,4 +83,18 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
],
method: 'POST',
});
+
+ sr.cleanupRepository = ca({
+ urls: [
+ {
+ fmt: '/_snapshot/<%=name%>/_cleanup',
+ req: {
+ name: {
+ type: 'string',
+ },
+ },
+ },
+ ],
+ method: 'POST',
+ });
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts
index bbfc82b8a6de..9f434ac10c16 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts
@@ -40,7 +40,7 @@ export const getAllHandler: RouterRouteHandler = async (
// Get policies
const policiesByName: {
[key: string]: SlmPolicyEs;
- } = await callWithRequest('slm.policies', {
+ } = await callWithRequest('sr.policies', {
human: true,
});
@@ -62,7 +62,7 @@ export const getOneHandler: RouterRouteHandler = async (
const { name } = req.params;
const policiesByName: {
[key: string]: SlmPolicyEs;
- } = await callWithRequest('slm.policy', {
+ } = await callWithRequest('sr.policy', {
name,
human: true,
});
@@ -82,7 +82,7 @@ export const getOneHandler: RouterRouteHandler = async (
export const executeHandler: RouterRouteHandler = async (req, callWithRequest) => {
const { name } = req.params;
- const { snapshot_name: snapshotName } = await callWithRequest('slm.executePolicy', {
+ const { snapshot_name: snapshotName } = await callWithRequest('sr.executePolicy', {
name,
});
return { snapshotName };
@@ -98,7 +98,7 @@ export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) =>
await Promise.all(
policyNames.map(name => {
- return callWithRequest('slm.deletePolicy', { name })
+ return callWithRequest('sr.deletePolicy', { name })
.then(() => response.itemsDeleted.push(name))
.catch(e =>
response.errors.push({
@@ -122,7 +122,7 @@ export const createHandler: RouterRouteHandler = async (req, callWithRequest) =>
// Check that policy with the same name doesn't already exist
try {
- const policyByName = await callWithRequest('slm.policy', { name });
+ const policyByName = await callWithRequest('sr.policy', { name });
if (policyByName[name]) {
throw conflictError;
}
@@ -134,7 +134,7 @@ export const createHandler: RouterRouteHandler = async (req, callWithRequest) =>
}
// Otherwise create new policy
- return await callWithRequest('slm.updatePolicy', {
+ return await callWithRequest('sr.updatePolicy', {
name,
body: serializePolicy(policy),
});
@@ -146,10 +146,10 @@ export const updateHandler: RouterRouteHandler = async (req, callWithRequest) =>
// Check that policy with the given name exists
// If it doesn't exist, 404 will be thrown by ES and will be returned
- await callWithRequest('slm.policy', { name });
+ await callWithRequest('sr.policy', { name });
// Otherwise update policy
- return await callWithRequest('slm.updatePolicy', {
+ return await callWithRequest('sr.updatePolicy', {
name,
body: serializePolicy(policy),
});
@@ -210,5 +210,5 @@ export const updateRetentionSettingsHandler: RouterRouteHandler = async (req, ca
};
export const executeRetentionHandler: RouterRouteHandler = async (_req, callWithRequest) => {
- return await callWithRequest('slm.executeRetention');
+ return await callWithRequest('sr.executeRetention');
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts
index f6ac946ab07d..3d67494da4aa 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts
@@ -15,6 +15,7 @@ import {
RepositoryType,
RepositoryVerification,
SlmPolicyEs,
+ RepositoryCleanup,
} from '../../../common/types';
import { Plugins } from '../../shim';
@@ -34,6 +35,7 @@ export function registerRepositoriesRoutes(router: Router, plugins: Plugins) {
router.get('repositories', getAllHandler);
router.get('repositories/{name}', getOneHandler);
router.get('repositories/{name}/verify', getVerificationHandler);
+ router.post('repositories/{name}/cleanup', getCleanupHandler);
router.put('repositories', createHandler);
router.put('repositories/{name}', updateHandler);
router.delete('repositories/{names}', deleteHandler);
@@ -74,7 +76,7 @@ export const getAllHandler: RouterRouteHandler = async (
try {
const policiesByName: {
[key: string]: SlmPolicyEs;
- } = await callWithRequest('slm.policies', {
+ } = await callWithRequest('sr.policies', {
human: true,
});
const managedRepositoryPolicy = Object.entries(policiesByName)
@@ -172,6 +174,31 @@ export const getVerificationHandler: RouterRouteHandler = async (
};
};
+export const getCleanupHandler: RouterRouteHandler = async (
+ req,
+ callWithRequest
+): Promise<{
+ cleanup: RepositoryCleanup | {};
+}> => {
+ const { name } = req.params;
+
+ const cleanupResults = await callWithRequest('sr.cleanupRepository', {
+ name,
+ }).catch(e => ({
+ cleaned: false,
+ error: e.response ? JSON.parse(e.response) : e,
+ }));
+
+ return {
+ cleanup: cleanupResults.error
+ ? cleanupResults
+ : {
+ cleaned: true,
+ response: cleanupResults,
+ },
+ };
+};
+
export const getTypesHandler: RouterRouteHandler = async () => {
// In ECE/ESS, do not enable the default types
const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES];
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts
index 042a2dfeaf6b..0d34d6a6b1b3 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts
@@ -38,7 +38,7 @@ export const getAllHandler: RouterRouteHandler = async (
// Attempt to retrieve policies
// This could fail if user doesn't have access to read SLM policies
try {
- const policiesByName = await callWithRequest('slm.policies');
+ const policiesByName = await callWithRequest('sr.policies');
policies = Object.keys(policiesByName);
} catch (e) {
// Silently swallow error as policy names aren't required in UI
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/shim.ts b/x-pack/legacy/plugins/snapshot_restore/server/shim.ts
index 84c9ddf8e0be..d64f35c64f11 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/shim.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/shim.ts
@@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { createRouter, Router } from '../../../server/lib/create_router';
import { registerLicenseChecker } from '../../../server/lib/register_license_checker';
-import { elasticsearchJsPlugin } from './client/elasticsearch_slm';
+import { elasticsearchJsPlugin } from './client/elasticsearch_sr';
import { CloudSetup } from '../../../../plugins/cloud/server';
export interface Core {
http: {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index cf11e387a879..3b0c18831830 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -10965,7 +10965,6 @@
"xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel": "サーバー側エコシステム",
"xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel": "ストレージクラス",
"xpack.snapshotRestore.repositoryDetails.typeTitle": "レポジトリタイプ",
- "xpack.snapshotRestore.repositoryDetails.verificationDetails": "認証情報レポジトリ「{name}」",
"xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle": "詳細",
"xpack.snapshotRestore.repositoryDetails.verificationTitle": "認証ステータス",
"xpack.snapshotRestore.repositoryDetails.verifyButtonLabel": "レポジトリを検証",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 3a03bb383f2c..3cc476937d4e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -11054,7 +11054,6 @@
"xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel": "服务器端加密",
"xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel": "存储类",
"xpack.snapshotRestore.repositoryDetails.typeTitle": "存储库类型",
- "xpack.snapshotRestore.repositoryDetails.verificationDetails": "验证详情存储库“{name}”",
"xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle": "详情",
"xpack.snapshotRestore.repositoryDetails.verificationTitle": "验证状态",
"xpack.snapshotRestore.repositoryDetails.verifyButtonLabel": "验证存储库",
diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts
index 99d3ea7834e6..608c7f321a08 100644
--- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts
+++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'snapshotRestore']);
const log = getService('log');
+ const es = getService('legacyEs');
describe('Home page', function() {
this.tags('smoke');
@@ -26,5 +27,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const repositoriesButton = await pageObjects.snapshotRestore.registerRepositoryButton();
expect(await repositoriesButton.isDisplayed()).to.be(true);
});
+
+ describe('Repositories Tab', async () => {
+ before(async () => {
+ await es.snapshot.createRepository({
+ repository: 'my-repository',
+ body: {
+ type: 'fs',
+ settings: {
+ location: '/tmp/es-backups/',
+ compress: true,
+ },
+ },
+ verify: true,
+ });
+ await pageObjects.snapshotRestore.navToRepositories();
+ });
+
+ it('cleanup repository', async () => {
+ await pageObjects.snapshotRestore.viewRepositoryDetails('my-repository');
+ await pageObjects.common.sleep(25000);
+ const cleanupResponse = await pageObjects.snapshotRestore.performRepositoryCleanup();
+ await pageObjects.common.sleep(25000);
+ expect(cleanupResponse).to.contain('results');
+ expect(cleanupResponse).to.contain('deleted_bytes');
+ expect(cleanupResponse).to.contain('deleted_blobs');
+ });
+ after(async () => {
+ await es.snapshot.deleteRepository({
+ repository: 'my-repository',
+ });
+ });
+ });
});
};
diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js
index 17235c61c7d8..19e9a5966712 100644
--- a/x-pack/test/functional/config.js
+++ b/x-pack/test/functional/config.js
@@ -69,7 +69,7 @@ export default async function({ readConfigFile }) {
esTestCluster: {
license: 'trial',
from: 'snapshot',
- serverArgs: [],
+ serverArgs: ['path.repo=/tmp/'],
},
kbnTestServer: {
diff --git a/x-pack/test/functional/page_objects/snapshot_restore_page.ts b/x-pack/test/functional/page_objects/snapshot_restore_page.ts
index 25bdfc707572..1c8ba9f63311 100644
--- a/x-pack/test/functional/page_objects/snapshot_restore_page.ts
+++ b/x-pack/test/functional/page_objects/snapshot_restore_page.ts
@@ -3,11 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
import { FtrProviderContext } from '../ftr_provider_context';
export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
return {
async appTitleText() {
@@ -16,5 +16,50 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext)
async registerRepositoryButton() {
return await testSubjects.find('registerRepositoryButton');
},
+ async navToRepositories() {
+ await testSubjects.click('repositories_tab');
+ await retry.waitForWithTimeout(
+ 'Wait for register repository button to be on page',
+ 10000,
+ async () => {
+ return await testSubjects.isDisplayed('registerRepositoryButton');
+ }
+ );
+ },
+ async getRepoList() {
+ const table = await testSubjects.find('repositoryTable');
+ const rows = await table.findAllByCssSelector('[data-test-subj="row"]');
+ return await Promise.all(
+ rows.map(async row => {
+ return {
+ repoName: await (
+ await row.findByCssSelector('[data-test-subj="Name_cell"]')
+ ).getVisibleText(),
+ repoLink: await (
+ await row.findByCssSelector('[data-test-subj="Name_cell"]')
+ ).findByCssSelector('a'),
+ repoType: await (
+ await row.findByCssSelector('[data-test-subj="Type_cell"]')
+ ).getVisibleText(),
+ repoEdit: await row.findByCssSelector('[data-test-subj="editRepositoryButton"]'),
+ repoDelete: await row.findByCssSelector('[data-test-subj="deleteRepositoryButton"]'),
+ };
+ })
+ );
+ },
+ async viewRepositoryDetails(name: string) {
+ const repos = await this.getRepoList();
+ if (repos.length === 1) {
+ const repoToView = repos.filter(r => (r.repoName = name))[0];
+ await repoToView.repoLink.click();
+ }
+ await retry.waitForWithTimeout(`Repo title should be ${name}`, 10000, async () => {
+ return (await testSubjects.getVisibleText('title')) === name;
+ });
+ },
+ async performRepositoryCleanup() {
+ await testSubjects.click('cleanupRepositoryButton');
+ return await testSubjects.getVisibleText('cleanupCodeBlock');
+ },
};
}