mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Transform] Add alerting rules management to Transform UI (#115363)
* transform alert flyout * fetch alerting rules * show alerting rules indicators * filter continuous transforms * add alert rules to the expanded row * edit alert rule from the list * fix ts issues * fix types * update texts * refactor using context, wip create alert from the list * update unit test * fix ts issue * privilege check
This commit is contained in:
parent
b306f8e2c3
commit
2aaa515bbc
33 changed files with 539 additions and 74 deletions
|
@ -12,7 +12,7 @@ import type { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common';
|
|||
import type { Dictionary } from '../types/common';
|
||||
import type { PivotAggDict } from '../types/pivot_aggs';
|
||||
import type { PivotGroupByDict } from '../types/pivot_group_by';
|
||||
import type { TransformId, TransformPivotConfig } from '../types/transform';
|
||||
import type { TransformId, TransformConfigUnion } from '../types/transform';
|
||||
|
||||
import { transformStateSchema, runtimeMappingsSchema } from './common';
|
||||
|
||||
|
@ -33,7 +33,7 @@ export type GetTransformsRequestSchema = TypeOf<typeof getTransformsRequestSchem
|
|||
|
||||
export interface GetTransformsResponseSchema {
|
||||
count: number;
|
||||
transforms: TransformPivotConfig[];
|
||||
transforms: TransformConfigUnion[];
|
||||
}
|
||||
|
||||
// schemas shared by parts of the preview, create and update endpoint
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { AlertTypeParams } from '../../../alerting/common';
|
||||
import type { Alert, AlertTypeParams } from '../../../alerting/common';
|
||||
|
||||
export type TransformHealthRuleParams = {
|
||||
includeTransforms?: string[];
|
||||
|
@ -20,3 +20,5 @@ export type TransformHealthRuleParams = {
|
|||
export type TransformHealthRuleTestsConfig = TransformHealthRuleParams['testsConfig'];
|
||||
|
||||
export type TransformHealthTests = keyof Exclude<TransformHealthRuleTestsConfig, null | undefined>;
|
||||
|
||||
export type TransformHealthAlertRule = Omit<Alert<TransformHealthRuleParams>, 'apiKey'>;
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
|
||||
import type { LatestFunctionConfig, PutTransformsRequestSchema } from '../api_schemas/transforms';
|
||||
import { isPopulatedObject } from '../shared_imports';
|
||||
import { PivotGroupByDict } from './pivot_group_by';
|
||||
import { PivotAggDict } from './pivot_aggs';
|
||||
import type { PivotGroupByDict } from './pivot_group_by';
|
||||
import type { PivotAggDict } from './pivot_aggs';
|
||||
import type { TransformHealthAlertRule } from './alerting';
|
||||
|
||||
export type IndexName = string;
|
||||
export type IndexPattern = string;
|
||||
|
@ -22,6 +23,7 @@ export type TransformBaseConfig = PutTransformsRequestSchema & {
|
|||
id: TransformId;
|
||||
create_time?: number;
|
||||
version?: string;
|
||||
alerting_rules?: TransformHealthAlertRule[];
|
||||
};
|
||||
|
||||
export interface PivotConfigDefinition {
|
||||
|
@ -45,6 +47,11 @@ export type TransformLatestConfig = Omit<TransformBaseConfig, 'pivot'> & {
|
|||
|
||||
export type TransformConfigUnion = TransformPivotConfig | TransformLatestConfig;
|
||||
|
||||
export type ContinuousTransform = Omit<TransformConfigUnion, 'sync'> &
|
||||
Required<{
|
||||
sync: TransformConfigUnion['sync'];
|
||||
}>;
|
||||
|
||||
export function isPivotTransform(transform: unknown): transform is TransformPivotConfig {
|
||||
return isPopulatedObject(transform, ['pivot']);
|
||||
}
|
||||
|
@ -53,6 +60,10 @@ export function isLatestTransform(transform: unknown): transform is TransformLat
|
|||
return isPopulatedObject(transform, ['latest']);
|
||||
}
|
||||
|
||||
export function isContinuousTransform(transform: unknown): transform is ContinuousTransform {
|
||||
return isPopulatedObject(transform, ['sync']);
|
||||
}
|
||||
|
||||
export interface LatestFunctionConfigUI {
|
||||
unique_key: Array<EuiComboBoxOptionOption<string>> | undefined;
|
||||
sort: EuiComboBoxOptionOption<string> | undefined;
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { createContext, FC, useContext, useMemo } from 'react';
|
||||
import { memoize } from 'lodash';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { pluck } from 'rxjs/operators';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { useAppDependencies } from '../app/app_dependencies';
|
||||
import { TransformHealthAlertRule, TransformHealthRuleParams } from '../../common/types/alerting';
|
||||
import { TRANSFORM_RULE_TYPE } from '../../common';
|
||||
|
||||
interface TransformAlertFlyoutProps {
|
||||
initialAlert?: TransformHealthAlertRule | null;
|
||||
ruleParams?: TransformHealthRuleParams | null;
|
||||
onSave?: () => void;
|
||||
onCloseFlyout: () => void;
|
||||
}
|
||||
|
||||
export const TransformAlertFlyout: FC<TransformAlertFlyoutProps> = ({
|
||||
initialAlert,
|
||||
ruleParams,
|
||||
onCloseFlyout,
|
||||
onSave,
|
||||
}) => {
|
||||
const { triggersActionsUi } = useAppDependencies();
|
||||
|
||||
const AlertFlyout = useMemo(() => {
|
||||
if (!triggersActionsUi) return;
|
||||
|
||||
const commonProps = {
|
||||
onClose: () => {
|
||||
onCloseFlyout();
|
||||
},
|
||||
onSave: async () => {
|
||||
if (onSave) {
|
||||
onSave();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (initialAlert) {
|
||||
return triggersActionsUi.getEditAlertFlyout({
|
||||
...commonProps,
|
||||
initialAlert,
|
||||
});
|
||||
}
|
||||
|
||||
return triggersActionsUi.getAddAlertFlyout({
|
||||
...commonProps,
|
||||
consumer: 'stackAlerts',
|
||||
canChangeTrigger: false,
|
||||
alertTypeId: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH,
|
||||
metadata: {},
|
||||
initialValues: {
|
||||
params: ruleParams!,
|
||||
},
|
||||
});
|
||||
// deps on id to avoid re-rendering on auto-refresh
|
||||
}, [triggersActionsUi, initialAlert, ruleParams, onCloseFlyout, onSave]);
|
||||
|
||||
return <>{AlertFlyout}</>;
|
||||
};
|
||||
|
||||
interface AlertRulesManage {
|
||||
editAlertRule$: Observable<TransformHealthAlertRule | null>;
|
||||
createAlertRule$: Observable<TransformHealthRuleParams | null>;
|
||||
setEditAlertRule: (alertRule: TransformHealthAlertRule) => void;
|
||||
setCreateAlertRule: (transformId: string) => void;
|
||||
hideAlertFlyout: () => void;
|
||||
}
|
||||
|
||||
export const getAlertRuleManageContext = memoize(function (): AlertRulesManage {
|
||||
const ruleState$ = new BehaviorSubject<{
|
||||
editAlertRule: null | TransformHealthAlertRule;
|
||||
createAlertRule: null | TransformHealthRuleParams;
|
||||
}>({
|
||||
editAlertRule: null,
|
||||
createAlertRule: null,
|
||||
});
|
||||
return {
|
||||
editAlertRule$: ruleState$.pipe(pluck('editAlertRule')),
|
||||
createAlertRule$: ruleState$.pipe(pluck('createAlertRule')),
|
||||
setEditAlertRule: (initialRule) => {
|
||||
ruleState$.next({
|
||||
createAlertRule: null,
|
||||
editAlertRule: initialRule,
|
||||
});
|
||||
},
|
||||
setCreateAlertRule: (transformId: string) => {
|
||||
ruleState$.next({
|
||||
createAlertRule: { includeTransforms: [transformId] },
|
||||
editAlertRule: null,
|
||||
});
|
||||
},
|
||||
hideAlertFlyout: () => {
|
||||
ruleState$.next({
|
||||
createAlertRule: null,
|
||||
editAlertRule: null,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const AlertRulesManageContext = createContext<AlertRulesManage>(getAlertRuleManageContext());
|
||||
|
||||
export function useAlertRuleFlyout(): AlertRulesManage {
|
||||
return useContext(AlertRulesManageContext);
|
||||
}
|
||||
|
||||
export const TransformAlertFlyoutWrapper = () => {
|
||||
const { editAlertRule$, createAlertRule$, hideAlertFlyout } = useAlertRuleFlyout();
|
||||
const editAlertRule = useObservable(editAlertRule$);
|
||||
const createAlertRule = useObservable(createAlertRule$);
|
||||
|
||||
return editAlertRule || createAlertRule ? (
|
||||
<TransformAlertFlyout
|
||||
initialAlert={editAlertRule}
|
||||
ruleParams={createAlertRule!}
|
||||
onCloseFlyout={hideAlertFlyout}
|
||||
/>
|
||||
) : null;
|
||||
};
|
|
@ -19,6 +19,7 @@ import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
|
|||
import type { AppDependencies } from '../app_dependencies';
|
||||
import { MlSharedContext } from './shared_context';
|
||||
import type { GetMlSharedImportsReturnType } from '../../shared_imports';
|
||||
import type { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
@ -43,6 +44,7 @@ const appDependencies: AppDependencies = {
|
|||
savedObjectsPlugin: savedObjectsPluginMock.createStartContract(),
|
||||
share: { urlGenerators: { getUrlGenerator: jest.fn() } } as unknown as SharePluginStart,
|
||||
ml: {} as GetMlSharedImportsReturnType,
|
||||
triggersActionsUi: {} as jest.Mocked<TriggersAndActionsUIPublicPluginStart>,
|
||||
};
|
||||
|
||||
export const useAppDependencies = () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
|||
import type { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
import type { GetMlSharedImportsReturnType } from '../shared_imports';
|
||||
import type { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
|
||||
|
||||
export interface AppDependencies {
|
||||
application: CoreStart['application'];
|
||||
|
@ -34,6 +35,7 @@ export interface AppDependencies {
|
|||
share: SharePluginStart;
|
||||
ml: GetMlSharedImportsReturnType;
|
||||
spaces?: SpacesPluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
}
|
||||
|
||||
export const useAppDependencies = () => {
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiTableActionsColumnType } from '@elastic/eui';
|
||||
|
||||
import { TransformConfigUnion, TransformId } from '../../../common/types/transform';
|
||||
import { TransformStats } from '../../../common/types/transform_stats';
|
||||
import type { EuiTableActionsColumnType } from '@elastic/eui';
|
||||
import type { TransformConfigUnion, TransformId } from '../../../common/types/transform';
|
||||
import type { TransformStats } from '../../../common/types/transform_stats';
|
||||
import type { TransformHealthAlertRule } from '../../../common/types/alerting';
|
||||
|
||||
// Used to pass on attribute names to table columns
|
||||
export enum TRANSFORM_LIST_COLUMN {
|
||||
|
@ -21,6 +21,7 @@ export interface TransformListRow {
|
|||
config: TransformConfigUnion;
|
||||
mode?: string; // added property on client side to allow filtering by this field
|
||||
stats: TransformStats;
|
||||
alerting_rules?: TransformHealthAlertRule[];
|
||||
}
|
||||
|
||||
// The single Action type is not exported as is
|
||||
|
|
|
@ -87,6 +87,7 @@ export const useGetTransforms = (
|
|||
mode:
|
||||
typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH,
|
||||
stats,
|
||||
alerting_rules: config.alerting_rules,
|
||||
});
|
||||
return reducedtableRows;
|
||||
}, [] as TransformListRow[]);
|
||||
|
|
|
@ -20,12 +20,14 @@ interface Authorization {
|
|||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
const initialCapabalities: Capabilities = {
|
||||
const initialCapabilities: Capabilities = {
|
||||
canGetTransform: false,
|
||||
canDeleteTransform: false,
|
||||
canPreviewTransform: false,
|
||||
canCreateTransform: false,
|
||||
canStartStopTransform: false,
|
||||
canCreateTransformAlerts: false,
|
||||
canUseTransformAlerts: false,
|
||||
};
|
||||
|
||||
const initialValue: Authorization = {
|
||||
|
@ -35,7 +37,7 @@ const initialValue: Authorization = {
|
|||
hasAllPrivileges: false,
|
||||
missingPrivileges: {},
|
||||
},
|
||||
capabilities: initialCapabalities,
|
||||
capabilities: initialCapabilities,
|
||||
};
|
||||
|
||||
export const AuthorizationContext = createContext<Authorization>({ ...initialValue });
|
||||
|
@ -58,7 +60,7 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) =
|
|||
const value = {
|
||||
isLoading,
|
||||
privileges: isLoading ? { ...initialValue.privileges } : privilegesData,
|
||||
capabilities: { ...initialCapabalities },
|
||||
capabilities: { ...initialCapabilities },
|
||||
apiError: error ? (error as Error) : null,
|
||||
};
|
||||
|
||||
|
@ -85,6 +87,10 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) =
|
|||
hasPrivilege(['cluster', 'cluster:admin/transform/start_task']) &&
|
||||
hasPrivilege(['cluster', 'cluster:admin/transform/stop']);
|
||||
|
||||
value.capabilities.canCreateTransformAlerts = value.capabilities.canCreateTransform;
|
||||
|
||||
value.capabilities.canUseTransformAlerts = value.capabilities.canGetTransform;
|
||||
|
||||
return (
|
||||
<AuthorizationContext.Provider value={{ ...value }}>{children}</AuthorizationContext.Provider>
|
||||
);
|
||||
|
|
|
@ -16,6 +16,8 @@ export interface Capabilities {
|
|||
canPreviewTransform: boolean;
|
||||
canCreateTransform: boolean;
|
||||
canStartStopTransform: boolean;
|
||||
canCreateTransformAlerts: boolean;
|
||||
canUseTransformAlerts: boolean;
|
||||
}
|
||||
|
||||
export type Privilege = [string, string];
|
||||
|
@ -67,6 +69,14 @@ export function createCapabilityFailureMessage(
|
|||
defaultMessage: 'You do not have permission to create transforms.',
|
||||
});
|
||||
break;
|
||||
case 'canCreateTransformAlerts':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip',
|
||||
{
|
||||
defaultMessage: 'You do not have permission to create transform alert rules.',
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'canStartStopTransform':
|
||||
message = i18n.translate(
|
||||
'xpack.transform.capability.noPermission.startOrStopTransformTooltip',
|
||||
|
|
|
@ -29,7 +29,7 @@ export async function mountManagementSection(
|
|||
const startServices = await getStartServices();
|
||||
const [core, plugins] = startServices;
|
||||
const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core;
|
||||
const { data, share, spaces } = plugins;
|
||||
const { data, share, spaces, triggersActionsUi } = plugins;
|
||||
const { docTitle } = chrome;
|
||||
|
||||
// Initialize services
|
||||
|
@ -55,6 +55,7 @@ export async function mountManagementSection(
|
|||
share,
|
||||
spaces,
|
||||
ml: await getMlSharedImports(),
|
||||
triggersActionsUi,
|
||||
};
|
||||
|
||||
const unmountAppCallback = renderApp(element, appDependencies);
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants';
|
||||
import { TransformPivotConfig } from '../../../../common/types/transform';
|
||||
import { TransformConfigUnion } from '../../../../common/types/transform';
|
||||
|
||||
import { isHttpFetchError } from '../../common/request';
|
||||
import { useApi } from '../../hooks/use_api';
|
||||
|
@ -50,7 +50,7 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => {
|
|||
|
||||
const transformId = match.params.transformId;
|
||||
|
||||
const [transformConfig, setTransformConfig] = useState<TransformPivotConfig>();
|
||||
const [transformConfig, setTransformConfig] = useState<TransformConfigUnion>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const { error: searchItemsError, searchItems, setSavedObjectId } = useSearchItems(undefined);
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
import {
|
||||
|
@ -52,7 +53,8 @@ import {
|
|||
} from '../../../../../../common/api_schemas/transforms';
|
||||
import type { RuntimeField } from '../../../../../../../../../src/plugins/data/common';
|
||||
import { isPopulatedObject } from '../../../../../../common/shared_imports';
|
||||
import { isLatestTransform } from '../../../../../../common/types/transform';
|
||||
import { isContinuousTransform, isLatestTransform } from '../../../../../../common/types/transform';
|
||||
import { TransformAlertFlyout } from '../../../../../alerting/transform_alerting_flyout';
|
||||
|
||||
export interface StepDetailsExposedState {
|
||||
created: boolean;
|
||||
|
@ -86,6 +88,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [created, setCreated] = useState(defaults.created);
|
||||
const [started, setStarted] = useState(defaults.started);
|
||||
const [alertFlyoutVisible, setAlertFlyoutVisible] = useState(false);
|
||||
const [indexPatternId, setIndexPatternId] = useState(defaults.indexPatternId);
|
||||
const [progressPercentComplete, setProgressPercentComplete] = useState<undefined | number>(
|
||||
undefined
|
||||
|
@ -398,6 +401,31 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{isContinuousTransform(transformConfig) && created ? (
|
||||
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
|
||||
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={loading}
|
||||
onClick={setAlertFlyoutVisible.bind(null, true)}
|
||||
data-test-subj="transformWizardCreateAlertButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.transform.stepCreateForm.createAlertRuleButton"
|
||||
defaultMessage="Create alert rule"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.transform.stepCreateForm.createAlertRuleDescription', {
|
||||
defaultMessage:
|
||||
'Opens a wizard to create an alert rule for monitoring transform health.',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
<EuiFlexGroup alignItems="center" style={FLEX_GROUP_STYLE}>
|
||||
<EuiFlexItem grow={false} style={FLEX_ITEM_STYLE}>
|
||||
<EuiButton
|
||||
|
@ -414,7 +442,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.transform.stepCreateForm.createTransformDescription', {
|
||||
defaultMessage:
|
||||
'Create the transform without starting it. You will be able to start the transform later by returning to the transforms list.',
|
||||
'Creates the transform without starting it. You will be able to start the transform later by returning to the transforms list.',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -535,6 +563,12 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
|
|||
</Fragment>
|
||||
)}
|
||||
</EuiForm>
|
||||
{alertFlyoutVisible ? (
|
||||
<TransformAlertFlyout
|
||||
ruleParams={{ includeTransforms: [transformId] }}
|
||||
onCloseFlyout={setAlertFlyoutVisible.bind(null, false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform';
|
||||
import type { TransformConfigUnion, TransformId } from '../../../../../../common/types/transform';
|
||||
|
||||
export type EsIndexName = string;
|
||||
export type IndexPatternTitle = string;
|
||||
|
@ -55,7 +55,7 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState {
|
|||
|
||||
export function applyTransformConfigToDetailsState(
|
||||
state: StepDetailsExposedState,
|
||||
transformConfig?: TransformPivotConfig
|
||||
transformConfig?: TransformConfigUnion
|
||||
): StepDetailsExposedState {
|
||||
// apply the transform configuration to wizard DETAILS state
|
||||
if (transformConfig !== undefined) {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
isEsIndices,
|
||||
isPostTransformsPreviewResponseSchema,
|
||||
} from '../../../../../../common/api_schemas/type_guards';
|
||||
import { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform';
|
||||
import { TransformId } from '../../../../../../common/types/transform';
|
||||
import { isValidIndexName } from '../../../../../../common/utils/es_utils';
|
||||
|
||||
import { getErrorMessage } from '../../../../../../common/utils/errors';
|
||||
|
@ -158,7 +158,7 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
|
|||
),
|
||||
});
|
||||
} else {
|
||||
setTransformIds(resp.transforms.map((transform: TransformPivotConfig) => transform.id));
|
||||
setTransformIds(resp.transforms.map((transform) => transform.id));
|
||||
}
|
||||
|
||||
const indices = await api.getEsIndices();
|
||||
|
|
|
@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { EuiSteps, EuiStepStatus } from '@elastic/eui';
|
||||
|
||||
import { TransformPivotConfig } from '../../../../../../common/types/transform';
|
||||
import type { TransformConfigUnion } from '../../../../../../common/types/transform';
|
||||
|
||||
import { getCreateTransformRequestBody } from '../../../../common';
|
||||
import { SearchItems } from '../../../../hooks/use_search_items';
|
||||
|
@ -81,7 +81,7 @@ const StepDefine: FC<DefinePivotStepProps> = ({
|
|||
};
|
||||
|
||||
interface WizardProps {
|
||||
cloneConfig?: TransformPivotConfig;
|
||||
cloneConfig?: TransformConfigUnion;
|
||||
searchItems: SearchItems;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createCapabilityFailureMessage } from '../../../../lib/authorization';
|
||||
|
||||
interface CreateAlertRuleActionProps {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const crateAlertRuleActionNameText = i18n.translate(
|
||||
'xpack.transform.transformList.createAlertRuleNameText',
|
||||
{
|
||||
defaultMessage: 'Create alert rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const CreateAlertRuleActionName: FC<CreateAlertRuleActionProps> = ({ disabled }) => {
|
||||
if (disabled) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={createCapabilityFailureMessage('canCreateTransformAlerts')}
|
||||
>
|
||||
<>{crateAlertRuleActionNameText}</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{crateAlertRuleActionNameText}</>;
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useCreateAlertRuleAction } from './use_create_alert_rule_action';
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { AuthorizationContext } from '../../../../lib/authorization';
|
||||
import { TransformListAction, TransformListRow } from '../../../../common';
|
||||
import {
|
||||
crateAlertRuleActionNameText,
|
||||
CreateAlertRuleActionName,
|
||||
} from './create_alert_rule_action_name';
|
||||
import { useAlertRuleFlyout } from '../../../../../alerting/transform_alerting_flyout';
|
||||
import { isContinuousTransform } from '../../../../../../common/types/transform';
|
||||
|
||||
export type CreateAlertRuleAction = ReturnType<typeof useCreateAlertRuleAction>;
|
||||
export const useCreateAlertRuleAction = (forceDisable: boolean) => {
|
||||
const { canCreateTransformAlerts } = useContext(AuthorizationContext).capabilities;
|
||||
const { setCreateAlertRule } = useAlertRuleFlyout();
|
||||
|
||||
const clickHandler = useCallback(
|
||||
(item: TransformListRow) => {
|
||||
setCreateAlertRule(item.id);
|
||||
},
|
||||
[setCreateAlertRule]
|
||||
);
|
||||
|
||||
const action: TransformListAction = useMemo(
|
||||
() => ({
|
||||
name: (item: TransformListRow) => (
|
||||
<CreateAlertRuleActionName disabled={!canCreateTransformAlerts} />
|
||||
),
|
||||
available: (item: TransformListRow) => isContinuousTransform(item.config),
|
||||
enabled: () => canCreateTransformAlerts && !forceDisable,
|
||||
description: crateAlertRuleActionNameText,
|
||||
type: 'icon',
|
||||
icon: 'bell',
|
||||
onClick: clickHandler,
|
||||
'data-test-subj': 'transformActionCreateAlertRule',
|
||||
}),
|
||||
[canCreateTransformAlerts, forceDisable, clickHandler]
|
||||
);
|
||||
|
||||
return { action };
|
||||
};
|
|
@ -22,6 +22,7 @@ import { getMlSharedImports } from '../../../../../shared_imports';
|
|||
|
||||
// FLAKY https://github.com/elastic/kibana/issues/112922
|
||||
describe.skip('Transform: Transform List <ExpandedRow />', () => {
|
||||
const onAlertEdit = jest.fn();
|
||||
// Set timezone to US/Eastern for consistent test results.
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('US/Eastern');
|
||||
|
@ -38,7 +39,7 @@ describe.skip('Transform: Transform List <ExpandedRow />', () => {
|
|||
|
||||
render(
|
||||
<MlSharedContext.Provider value={mlShared}>
|
||||
<ExpandedRow item={item} />
|
||||
<ExpandedRow item={item} onAlertEdit={onAlertEdit} />
|
||||
</MlSharedContext.Provider>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,17 +7,18 @@
|
|||
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiTabbedContent } from '@elastic/eui';
|
||||
import { Optional } from '@kbn/utility-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { TransformListRow } from '../../../../common';
|
||||
import { useAppDependencies } from '../../../../app_dependencies';
|
||||
import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane';
|
||||
import { ExpandedRowDetailsPane, SectionConfig, SectionItem } from './expanded_row_details_pane';
|
||||
import { ExpandedRowJsonPane } from './expanded_row_json_pane';
|
||||
import { ExpandedRowMessagesPane } from './expanded_row_messages_pane';
|
||||
import { ExpandedRowPreviewPane } from './expanded_row_preview_pane';
|
||||
import { TransformHealthAlertRule } from '../../../../../../common/types/alerting';
|
||||
|
||||
function getItemDescription(value: any) {
|
||||
if (typeof value === 'object') {
|
||||
|
@ -44,18 +45,16 @@ export function stringHash(str: string): number {
|
|||
return hash < 0 ? hash * -2 : hash;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
title: string;
|
||||
description: any;
|
||||
}
|
||||
type Item = SectionItem;
|
||||
|
||||
interface Props {
|
||||
item: TransformListRow;
|
||||
onAlertEdit: (alertRule: TransformHealthAlertRule) => void;
|
||||
}
|
||||
|
||||
type StateValues = Optional<TransformListRow['stats'], 'stats' | 'checkpointing'>;
|
||||
|
||||
export const ExpandedRow: FC<Props> = ({ item }) => {
|
||||
export const ExpandedRow: FC<Props> = ({ item, onAlertEdit }) => {
|
||||
const {
|
||||
ml: { formatHumanReadableDateTimeSeconds },
|
||||
} = useAppDependencies();
|
||||
|
@ -166,12 +165,40 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const alertRuleItems: Item[] | undefined = item.alerting_rules?.map((rule) => {
|
||||
return {
|
||||
title: (
|
||||
<EuiButtonEmpty
|
||||
iconType={'documentEdit'}
|
||||
iconSide={'left'}
|
||||
onClick={() => {
|
||||
onAlertEdit(rule);
|
||||
}}
|
||||
flush="left"
|
||||
size={'xs'}
|
||||
iconSize={'s'}
|
||||
>
|
||||
{rule.name}
|
||||
</EuiButtonEmpty>
|
||||
),
|
||||
description: rule.executionStatus.status,
|
||||
};
|
||||
});
|
||||
|
||||
const checkpointing: SectionConfig = {
|
||||
title: 'Checkpointing',
|
||||
items: checkpointingItems,
|
||||
position: 'right',
|
||||
};
|
||||
|
||||
const alertingRules: SectionConfig = {
|
||||
title: i18n.translate('xpack.transform.transformList.transformDetails.alertRulesTitle', {
|
||||
defaultMessage: 'Alert rules',
|
||||
}),
|
||||
items: alertRuleItems!,
|
||||
position: 'right',
|
||||
};
|
||||
|
||||
const stats: SectionConfig = {
|
||||
title: 'Stats',
|
||||
items: Object.entries(item.stats.stats).map((s) => {
|
||||
|
@ -192,7 +219,16 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
|
|||
defaultMessage: 'Details',
|
||||
}
|
||||
),
|
||||
content: <ExpandedRowDetailsPane sections={[general, state, checkpointing]} />,
|
||||
content: (
|
||||
<ExpandedRowDetailsPane
|
||||
sections={[
|
||||
general,
|
||||
state,
|
||||
checkpointing,
|
||||
...(alertingRules.items ? [alertingRules] : []),
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: `transform-stats-tab-${tabId}`,
|
||||
|
|
|
@ -17,9 +17,10 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
export interface SectionItem {
|
||||
title: string;
|
||||
description: string;
|
||||
title: string | JSX.Element;
|
||||
description: string | number | JSX.Element;
|
||||
}
|
||||
|
||||
export interface SectionConfig {
|
||||
title: string;
|
||||
position: 'left' | 'right';
|
||||
|
|
|
@ -50,15 +50,18 @@ import { useColumns } from './use_columns';
|
|||
import { ExpandedRow } from './expanded_row';
|
||||
import { transformFilters, filterTransforms } from './transform_search_bar_filters';
|
||||
import { useTableSettings } from './use_table_settings';
|
||||
import { useAlertRuleFlyout } from '../../../../../alerting/transform_alerting_flyout';
|
||||
import { TransformHealthAlertRule } from '../../../../../../common/types/alerting';
|
||||
|
||||
function getItemIdToExpandedRowMap(
|
||||
itemIds: TransformId[],
|
||||
transforms: TransformListRow[]
|
||||
transforms: TransformListRow[],
|
||||
onAlertEdit: (alertRule: TransformHealthAlertRule) => void
|
||||
): ItemIdToExpandedRowMap {
|
||||
return itemIds.reduce((m: ItemIdToExpandedRowMap, transformId: TransformId) => {
|
||||
const item = transforms.find((transform) => transform.config.id === transformId);
|
||||
if (item !== undefined) {
|
||||
m[transformId] = <ExpandedRow item={item} />;
|
||||
m[transformId] = <ExpandedRow item={item} onAlertEdit={onAlertEdit} />;
|
||||
}
|
||||
return m;
|
||||
}, {} as ItemIdToExpandedRowMap);
|
||||
|
@ -79,6 +82,7 @@ export const TransformList: FC<TransformListProps> = ({
|
|||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { refresh } = useRefreshTransformList({ isLoading: setIsLoading });
|
||||
const { setEditAlertRule } = useAlertRuleFlyout();
|
||||
|
||||
const [filterActive, setFilterActive] = useState(false);
|
||||
|
||||
|
@ -171,7 +175,11 @@ export const TransformList: FC<TransformListProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, transforms);
|
||||
const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(
|
||||
expandedRowItemIds,
|
||||
transforms,
|
||||
setEditAlertRule
|
||||
);
|
||||
|
||||
const bulkActionMenuItems = [
|
||||
<div key="startAction" className="transform__BulkActionItem">
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('Transform: Transform List Actions', () => {
|
|||
// in the runtime result here anyway.
|
||||
expect(actions.map((a: any) => a['data-test-subj'])).toStrictEqual([
|
||||
'transformActionDiscover',
|
||||
'transformActionCreateAlertRule',
|
||||
'transformActionStart',
|
||||
'transformActionStop',
|
||||
'transformActionEdit',
|
||||
|
|
|
@ -18,6 +18,7 @@ import { EditTransformFlyout } from '../edit_transform_flyout';
|
|||
import { useEditAction } from '../action_edit';
|
||||
import { useStartAction, StartActionModal } from '../action_start';
|
||||
import { useStopAction } from '../action_stop';
|
||||
import { useCreateAlertRuleAction } from '../action_create_alert';
|
||||
|
||||
export const useActions = ({
|
||||
forceDisable,
|
||||
|
@ -35,6 +36,7 @@ export const useActions = ({
|
|||
const editAction = useEditAction(forceDisable, transformNodes);
|
||||
const startAction = useStartAction(forceDisable, transformNodes);
|
||||
const stopAction = useStopAction(forceDisable);
|
||||
const createAlertRuleAction = useCreateAlertRuleAction(forceDisable);
|
||||
|
||||
return {
|
||||
modals: (
|
||||
|
@ -52,6 +54,7 @@ export const useActions = ({
|
|||
),
|
||||
actions: [
|
||||
discoverAction.action,
|
||||
createAlertRuleAction.action,
|
||||
startAction.action,
|
||||
stopAction.action,
|
||||
editAction.action,
|
||||
|
|
|
@ -20,14 +20,15 @@ describe('Transform: Job List Columns', () => {
|
|||
|
||||
const columns: ReturnType<typeof useColumns>['columns'] = result.current.columns;
|
||||
|
||||
expect(columns).toHaveLength(8);
|
||||
expect(columns).toHaveLength(9);
|
||||
expect(columns[0].isExpander).toBeTruthy();
|
||||
expect(columns[1].name).toBe('ID');
|
||||
expect(columns[2].name).toBe('Description');
|
||||
expect(columns[3].name).toBe('Type');
|
||||
expect(columns[4].name).toBe('Status');
|
||||
expect(columns[5].name).toBe('Mode');
|
||||
expect(columns[6].name).toBe('Progress');
|
||||
expect(columns[7].name).toBe('Actions');
|
||||
expect(columns[2].id).toBe('alertRule');
|
||||
expect(columns[3].name).toBe('Description');
|
||||
expect(columns[4].name).toBe('Type');
|
||||
expect(columns[5].name).toBe('Status');
|
||||
expect(columns[6].name).toBe('Mode');
|
||||
expect(columns[7].name).toBe('Progress');
|
||||
expect(columns[8].name).toBe('Actions');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
EuiText,
|
||||
EuiToolTip,
|
||||
RIGHT_ALIGNMENT,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
|
@ -95,6 +96,7 @@ export const useColumns = (
|
|||
const columns: [
|
||||
EuiTableComputedColumnType<TransformListRow>,
|
||||
EuiTableFieldDataColumnType<TransformListRow>,
|
||||
EuiTableComputedColumnType<TransformListRow>,
|
||||
EuiTableFieldDataColumnType<TransformListRow>,
|
||||
EuiTableComputedColumnType<TransformListRow>,
|
||||
EuiTableComputedColumnType<TransformListRow>,
|
||||
|
@ -143,6 +145,38 @@ export const useColumns = (
|
|||
truncateText: true,
|
||||
scope: 'row',
|
||||
},
|
||||
{
|
||||
id: 'alertRule',
|
||||
name: (
|
||||
<EuiScreenReaderOnly>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.transform.transformList.alertingRules.screenReaderDescription"
|
||||
defaultMessage="This column displays an icon when there are alert rules associated with a transform"
|
||||
/>
|
||||
</p>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
width: '30px',
|
||||
render: (item) => {
|
||||
return Array.isArray(item.alerting_rules) ? (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.transformList.alertingRules.tooltipContent"
|
||||
defaultMessage="Transform has {rulesCount} associated alert {rulesCount, plural, one { rule} other { rules}}"
|
||||
values={{ rulesCount: item.alerting_rules.length }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="bell" />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<span />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: TRANSFORM_LIST_COLUMN.DESCRIPTION,
|
||||
'data-test-subj': 'transformListColumnDescription',
|
||||
|
|
|
@ -35,6 +35,11 @@ import { useRefreshInterval } from './components/transform_list/use_refresh_inte
|
|||
import { SearchSelection } from './components/search_selection';
|
||||
import { TransformList } from './components/transform_list';
|
||||
import { TransformStatsBar } from './components/transform_list/transforms_stats_bar';
|
||||
import {
|
||||
AlertRulesManageContext,
|
||||
getAlertRuleManageContext,
|
||||
TransformAlertFlyoutWrapper,
|
||||
} from '../../../alerting/transform_alerting_flyout';
|
||||
|
||||
export const TransformManagement: FC = () => {
|
||||
const { esTransform } = useDocumentationLinks();
|
||||
|
@ -149,12 +154,15 @@ export const TransformManagement: FC = () => {
|
|||
</EuiFlexGroup>
|
||||
)}
|
||||
{typeof errorMessage === 'undefined' && (
|
||||
<TransformList
|
||||
onCreateTransform={onOpenModal}
|
||||
transformNodes={transformNodes}
|
||||
transforms={transforms}
|
||||
transformsLoading={transformsLoading}
|
||||
/>
|
||||
<AlertRulesManageContext.Provider value={getAlertRuleManageContext()}>
|
||||
<TransformList
|
||||
onCreateTransform={onOpenModal}
|
||||
transformNodes={transformNodes}
|
||||
transforms={transforms}
|
||||
transformsLoading={transformsLoading}
|
||||
/>
|
||||
<TransformAlertFlyoutWrapper />
|
||||
</AlertRulesManageContext.Provider>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -16,7 +16,7 @@ import type { SharePluginStart } from 'src/plugins/share/public';
|
|||
import type { SpacesApi } from '../../spaces/public';
|
||||
import { registerFeature } from './register_feature';
|
||||
import type { PluginSetupContract as AlertingSetup } from '../../alerting/public';
|
||||
import type { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
|
||||
import type { TriggersAndActionsUIPublicPluginStart } from '../../triggers_actions_ui/public';
|
||||
import { getTransformHealthRuleType } from './alerting';
|
||||
|
||||
export interface PluginsDependencies {
|
||||
|
@ -27,7 +27,7 @@ export interface PluginsDependencies {
|
|||
share: SharePluginStart;
|
||||
spaces?: SpacesApi;
|
||||
alerting?: AlertingSetup;
|
||||
triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
}
|
||||
|
||||
export class TransformUiPlugin {
|
||||
|
|
|
@ -8,16 +8,21 @@
|
|||
import { ElasticsearchClient } from 'kibana/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Transform as EsTransform } from '@elastic/elasticsearch/api/types';
|
||||
import { keyBy } from 'lodash';
|
||||
import { TransformHealthRuleParams } from './schema';
|
||||
import {
|
||||
ALL_TRANSFORMS_SELECTION,
|
||||
TRANSFORM_HEALTH_CHECK_NAMES,
|
||||
TRANSFORM_RULE_TYPE,
|
||||
} from '../../../../common/constants';
|
||||
import { getResultTestConfig } from '../../../../common/utils/alerts';
|
||||
import {
|
||||
NotStartedTransformResponse,
|
||||
TransformHealthAlertContext,
|
||||
} from './register_transform_health_rule_type';
|
||||
import type { RulesClient } from '../../../../../alerting/server';
|
||||
import type { TransformHealthAlertRule } from '../../../../common/types/alerting';
|
||||
import { isContinuousTransform } from '../../../../common/types/transform';
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
|
@ -27,37 +32,48 @@ interface TestResult {
|
|||
// @ts-ignore FIXME update types in the elasticsearch client
|
||||
type Transform = EsTransform & { id: string; description?: string; sync: object };
|
||||
|
||||
export function transformHealthServiceProvider(esClient: ElasticsearchClient) {
|
||||
type TransformWithAlertingRules = Transform & { alerting_rules: TransformHealthAlertRule[] };
|
||||
|
||||
export function transformHealthServiceProvider(
|
||||
esClient: ElasticsearchClient,
|
||||
rulesClient?: RulesClient
|
||||
) {
|
||||
const transformsDict = new Map<string, Transform>();
|
||||
|
||||
/**
|
||||
* Resolves result transform selection.
|
||||
* @param includeTransforms
|
||||
* @param excludeTransforms
|
||||
* @param skipIDsCheck
|
||||
*/
|
||||
const getResultsTransformIds = async (
|
||||
includeTransforms: string[],
|
||||
excludeTransforms: string[] | null
|
||||
excludeTransforms: string[] | null,
|
||||
skipIDsCheck = false
|
||||
): Promise<string[]> => {
|
||||
const includeAll = includeTransforms.some((id) => id === ALL_TRANSFORMS_SELECTION);
|
||||
|
||||
// Fetch transforms to make sure assigned transforms exists.
|
||||
const transformsResponse = (
|
||||
await esClient.transform.getTransform({
|
||||
...(includeAll ? {} : { transform_id: includeTransforms.join(',') }),
|
||||
allow_no_match: true,
|
||||
size: 1000,
|
||||
})
|
||||
).body.transforms as Transform[];
|
||||
|
||||
let resultTransformIds: string[] = [];
|
||||
|
||||
transformsResponse.forEach((t) => {
|
||||
transformsDict.set(t.id, t);
|
||||
if (t.sync) {
|
||||
resultTransformIds.push(t.id);
|
||||
}
|
||||
});
|
||||
if (skipIDsCheck) {
|
||||
resultTransformIds = includeTransforms;
|
||||
} else {
|
||||
// Fetch transforms to make sure assigned transforms exists.
|
||||
const transformsResponse = (
|
||||
await esClient.transform.getTransform({
|
||||
...(includeAll ? {} : { transform_id: includeTransforms.join(',') }),
|
||||
allow_no_match: true,
|
||||
size: 1000,
|
||||
})
|
||||
).body.transforms as Transform[];
|
||||
|
||||
transformsResponse.forEach((t) => {
|
||||
transformsDict.set(t.id, t);
|
||||
if (t.sync) {
|
||||
resultTransformIds.push(t.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeTransforms && excludeTransforms.length > 0) {
|
||||
const excludeIdsSet = new Set(excludeTransforms);
|
||||
|
@ -129,6 +145,53 @@ export function transformHealthServiceProvider(esClient: ElasticsearchClient) {
|
|||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates transform list with associated alerting rules.
|
||||
*/
|
||||
async populateTransformsWithAssignedRules(
|
||||
transforms: Transform[]
|
||||
): Promise<TransformWithAlertingRules[]> {
|
||||
const newList = transforms.filter(isContinuousTransform) as TransformWithAlertingRules[];
|
||||
|
||||
if (!rulesClient) {
|
||||
throw new Error('Rules client is missing');
|
||||
}
|
||||
|
||||
const transformMap = keyBy(newList, 'id');
|
||||
|
||||
const transformAlertingRules = await rulesClient.find<TransformHealthRuleParams>({
|
||||
options: {
|
||||
perPage: 1000,
|
||||
filter: `alert.attributes.alertTypeId:${TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH}`,
|
||||
},
|
||||
});
|
||||
|
||||
for (const ruleInstance of transformAlertingRules.data) {
|
||||
// Retrieve result transform IDs
|
||||
const resultTransformIds: string[] = await getResultsTransformIds(
|
||||
ruleInstance.params.includeTransforms.includes(ALL_TRANSFORMS_SELECTION)
|
||||
? Object.keys(transformMap)
|
||||
: ruleInstance.params.includeTransforms,
|
||||
ruleInstance.params.excludeTransforms,
|
||||
true
|
||||
);
|
||||
|
||||
resultTransformIds.forEach((transformId) => {
|
||||
const transformRef = transformMap[transformId] as TransformWithAlertingRules;
|
||||
|
||||
if (transformRef) {
|
||||
if (Array.isArray(transformRef.alerting_rules)) {
|
||||
transformRef.alerting_rules.push(ruleInstance);
|
||||
} else {
|
||||
transformRef.alerting_rules = [ruleInstance];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newList;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ import { registerTransformNodesRoutes } from './transforms_nodes';
|
|||
import { IIndexPattern } from '../../../../../../src/plugins/data/common';
|
||||
import { isLatestTransform } from '../../../common/types/transform';
|
||||
import { isKeywordDuplicate } from '../../../common/utils/field_utils';
|
||||
import { transformHealthServiceProvider } from '../../lib/alerting/transform_health_rule_type/transform_health_service';
|
||||
|
||||
enum TRANSFORM_ACTIONS {
|
||||
STOP = 'stop',
|
||||
|
@ -90,6 +91,17 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
|
|||
size: 1000,
|
||||
...req.params,
|
||||
});
|
||||
|
||||
if (ctx.alerting) {
|
||||
const transformHealthService = transformHealthServiceProvider(
|
||||
ctx.core.elasticsearch.client.asCurrentUser,
|
||||
ctx.alerting.getRulesClient()
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
await transformHealthService.populateTransformsWithAssignedRules(body.transforms);
|
||||
}
|
||||
|
||||
return res.ok({ body });
|
||||
} catch (e) {
|
||||
return res.customError(wrapError(wrapEsError(e)));
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from 'kibana/server';
|
||||
|
||||
import { LicensingPluginSetup, LicenseType } from '../../../licensing/server';
|
||||
import type { AlertingApiRequestHandlerContext } from '../../../alerting/server';
|
||||
|
||||
export interface LicenseStatus {
|
||||
isValid: boolean;
|
||||
|
@ -28,6 +29,10 @@ interface SetupSettings {
|
|||
defaultErrorMessage: string;
|
||||
}
|
||||
|
||||
type TransformRequestHandlerContext = RequestHandlerContext & {
|
||||
alerting?: AlertingApiRequestHandlerContext;
|
||||
};
|
||||
|
||||
export class License {
|
||||
private licenseStatus: LicenseStatus = {
|
||||
isValid: false,
|
||||
|
@ -64,7 +69,9 @@ export class License {
|
|||
});
|
||||
}
|
||||
|
||||
guardApiRoute<Params, Query, Body>(handler: RequestHandler<Params, Query, Body>) {
|
||||
guardApiRoute<Params, Query, Body>(
|
||||
handler: RequestHandler<Params, Query, Body, TransformRequestHandlerContext>
|
||||
) {
|
||||
const license = this;
|
||||
|
||||
return function licenseCheck(
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
IndexedHostsAndAlertsResponse,
|
||||
indexHostsAndAlerts,
|
||||
} from '../../../plugins/security_solution/common/endpoint/index_data';
|
||||
import { TransformPivotConfig } from '../../../plugins/transform/common/types/transform';
|
||||
import { TransformConfigUnion } from '../../../plugins/transform/common/types/transform';
|
||||
import { GetTransformsResponseSchema } from '../../../plugins/transform/common/api_schemas/transforms';
|
||||
import { catchAndWrapError } from '../../../plugins/security_solution/server/endpoint/utils';
|
||||
import { installOrUpgradeEndpointFleetPackage } from '../../../plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint';
|
||||
|
@ -38,9 +38,9 @@ export class EndpointTestResources extends FtrService {
|
|||
*
|
||||
* @param [endpointPackageVersion] if set, it will be used to get the specific transform this this package version. Else just returns first one found
|
||||
*/
|
||||
async getTransform(endpointPackageVersion?: string): Promise<TransformPivotConfig> {
|
||||
async getTransform(endpointPackageVersion?: string): Promise<TransformConfigUnion> {
|
||||
const transformId = this.generateTransformId(endpointPackageVersion);
|
||||
let transform: TransformPivotConfig | undefined;
|
||||
let transform: TransformConfigUnion | undefined;
|
||||
|
||||
if (endpointPackageVersion) {
|
||||
await this.transform.api.waitForTransformToExist(transformId);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue