Skip to content

Commit

Permalink
[Logs UI] [Alerting] Alerts management page enhancements (#64654) (#6…
Browse files Browse the repository at this point in the history
…5070)

* Ensure adding / editing log alerts works from the alerts management page

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
Kerry350 and elasticmachine authored May 4, 2020
1 parent 65b1937 commit 521ee01
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const AlertFlyout = (props: Props) => {
{triggersActionsUI && (
<AlertsContextProvider
value={{
metadata: {},
metadata: {
isInternal: true,
},
toastNotifications: services.notifications?.toasts,
http: services.http,
docLinks: services.docLinks,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,45 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback, useMemo, useEffect, useState } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui';
import { useMount } from 'react-use';
import { FormattedMessage } from '@kbn/i18n/react';
import {
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types';
import { useSource } from '../../../../containers/source';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../../triggers_actions_ui/public/application/context/alerts_context';
import {
LogDocumentCountAlertParams,
Comparator,
TimeUnit,
} from '../../../../../common/alerting/logs/types';
import { DocumentCount } from './document_count';
import { Criteria } from './criteria';
import { useSourceId } from '../../../../containers/source_id';
import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source';

export interface ExpressionCriteria {
field?: string;
comparator?: Comparator;
value?: string | number;
}

interface LogsContextMeta {
isInternal?: boolean;
}

interface Props {
errors: IErrorObject;
alertParams: Partial<LogDocumentCountAlertParams>;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
alertsContext: AlertsContextValue<LogsContextMeta>;
}

const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' };
Expand All @@ -48,32 +58,92 @@ const DEFAULT_EXPRESSION = {
};

export const ExpressionEditor: React.FC<Props> = props => {
const isInternal = props.alertsContext.metadata?.isInternal;
const [sourceId] = useSourceId();

return (
<>
{isInternal ? (
<SourceStatusWrapper {...props}>
<Editor {...props} />
</SourceStatusWrapper>
) : (
<LogSourceProvider sourceId={sourceId} fetch={props.alertsContext.http.fetch}>
<SourceStatusWrapper {...props}>
<Editor {...props} />
</SourceStatusWrapper>
</LogSourceProvider>
)}
</>
);
};

export const SourceStatusWrapper: React.FC<Props> = props => {
const {
initialize,
isLoadingSourceStatus,
isUninitialized,
hasFailedLoadingSourceStatus,
loadSourceStatus,
} = useLogSourceContext();
const { children } = props;

useMount(() => {
initialize();
});

return (
<>
{isLoadingSourceStatus || isUninitialized ? (
<div>
<EuiSpacer size="m" />
<EuiLoadingSpinner size="l" />
<EuiSpacer size="m" />
</div>
) : hasFailedLoadingSourceStatus ? (
<EuiCallOut
title={i18n.translate('xpack.infra.logs.alertFlyout.sourceStatusError', {
defaultMessage: 'Sorry, there was a problem loading field information',
})}
color="danger"
iconType="alert"
>
<EuiButton onClick={loadSourceStatus} iconType="refresh">
{i18n.translate('xpack.infra.logs.alertFlyout.sourceStatusErrorTryAgain', {
defaultMessage: 'Try again',
})}
</EuiButton>
</EuiCallOut>
) : (
children
)}
</>
);
};

export const Editor: React.FC<Props> = props => {
const { setAlertParams, alertParams, errors } = props;
const { createDerivedIndexPattern } = useSource({ sourceId: 'default' });
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');
const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false);
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('logs'), [
createDerivedIndexPattern,
]);
const { sourceStatus } = useLogSourceContext();

useMount(() => {
for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) {
setAlertParams(key, value);
setHasSetDefaults(true);
}
});

const supportedFields = useMemo(() => {
if (derivedIndexPattern?.fields) {
return derivedIndexPattern.fields.filter(field => {
if (sourceStatus?.logIndexFields) {
return sourceStatus.logIndexFields.filter(field => {
return (field.type === 'string' || field.type === 'number') && field.searchable;
});
} else {
return [];
}
}, [derivedIndexPattern]);

// Set the default expression (disables exhaustive-deps as we only want to run this once on mount)
useEffect(() => {
for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) {
setAlertParams(key, value);
setHasSetDefaults(true);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}, [sourceStatus]);

const updateCount = useCallback(
countParams => {
Expand Down Expand Up @@ -126,8 +196,6 @@ export const ExpressionEditor: React.FC<Props> = props => {
[alertParams, setAlertParams]
);

// Wait until field info has loaded
if (supportedFields.length === 0) return null;
// Wait until the alert param defaults have been set
if (!hasSetDefaults) return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HttpSetup } from 'src/core/public';
import {
getLogSourceConfigurationPath,
getLogSourceConfigurationSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import { npStart } from '../../../../legacy_singletons';

export const callFetchLogSourceConfigurationAPI = async (sourceId: string) => {
const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), {
export const callFetchLogSourceConfigurationAPI = async (
sourceId: string,
fetch: HttpSetup['fetch']
) => {
const response = await fetch(getLogSourceConfigurationPath(sourceId), {
method: 'GET',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HttpSetup } from 'src/core/public';
import {
getLogSourceStatusPath,
getLogSourceStatusSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import { npStart } from '../../../../legacy_singletons';

export const callFetchLogSourceStatusAPI = async (sourceId: string) => {
const response = await npStart.http.fetch(getLogSourceStatusPath(sourceId), {
export const callFetchLogSourceStatusAPI = async (sourceId: string, fetch: HttpSetup['fetch']) => {
const response = await fetch(getLogSourceStatusPath(sourceId), {
method: 'GET',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HttpSetup } from 'src/core/public';
import {
getLogSourceConfigurationPath,
patchLogSourceConfigurationSuccessResponsePayloadRT,
patchLogSourceConfigurationRequestBodyRT,
LogSourceConfigurationPropertiesPatch,
} from '../../../../../common/http_api/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import { npStart } from '../../../../legacy_singletons';

export const callPatchLogSourceConfigurationAPI = async (
sourceId: string,
patchedProperties: LogSourceConfigurationPropertiesPatch
patchedProperties: LogSourceConfigurationPropertiesPatch,
fetch: HttpSetup['fetch']
) => {
const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), {
const response = await fetch(getLogSourceConfigurationPath(sourceId), {
method: 'PATCH',
body: JSON.stringify(
patchLogSourceConfigurationRequestBodyRT.encode({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import createContainer from 'constate';
import { useState, useMemo, useCallback } from 'react';
import { HttpSetup } from 'src/core/public';
import {
LogSourceConfiguration,
LogSourceStatus,
Expand All @@ -24,7 +25,13 @@ export {
LogSourceStatus,
};

export const useLogSource = ({ sourceId }: { sourceId: string }) => {
export const useLogSource = ({
sourceId,
fetch,
}: {
sourceId: string;
fetch: HttpSetup['fetch'];
}) => {
const [sourceConfiguration, setSourceConfiguration] = useState<
LogSourceConfiguration | undefined
>(undefined);
Expand All @@ -35,40 +42,40 @@ export const useLogSource = ({ sourceId }: { sourceId: string }) => {
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return await callFetchLogSourceConfigurationAPI(sourceId);
return await callFetchLogSourceConfigurationAPI(sourceId, fetch);
},
onResolve: ({ data }) => {
setSourceConfiguration(data);
},
},
[sourceId]
[sourceId, fetch]
);

const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties);
return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch);
},
onResolve: ({ data }) => {
setSourceConfiguration(data);
loadSourceStatus();
},
},
[sourceId]
[sourceId, fetch]
);

const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return await callFetchLogSourceStatusAPI(sourceId);
return await callFetchLogSourceStatusAPI(sourceId, fetch);
},
onResolve: ({ data }) => {
setSourceStatus(data);
},
},
[sourceId]
[sourceId, fetch]
);

const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [
Expand Down Expand Up @@ -114,6 +121,10 @@ export const useLogSource = ({ sourceId }: { sourceId: string }) => {
[loadSourceConfigurationRequest.state]
);

const hasFailedLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'rejected', [
loadSourceStatusRequest.state,
]);

const loadSourceFailureMessage = useMemo(
() =>
loadSourceConfigurationRequest.state === 'rejected'
Expand All @@ -137,6 +148,7 @@ export const useLogSource = ({ sourceId }: { sourceId: string }) => {
return {
derivedIndexPattern,
hasFailedLoadingSource,
hasFailedLoadingSourceStatus,
initialize,
isLoading,
isLoadingSourceConfiguration,
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/infra/public/pages/logs/page_providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
*/

import React from 'react';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis';
import { LogSourceProvider } from '../../containers/logs/log_source';
// import { SourceProvider } from '../../containers/source';
import { useSourceId } from '../../containers/source_id';

export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const [sourceId] = useSourceId();

const { services } = useKibana();
return (
<LogSourceProvider sourceId={sourceId}>
<LogSourceProvider sourceId={sourceId} fetch={services.http.fetch}>
<LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider>
</LogSourceProvider>
);
Expand Down

0 comments on commit 521ee01

Please sign in to comment.