Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Logs UI] [Alerting] Alerts management page enhancements #64654

Merged
merged 7 commits into from
May 4, 2020
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why this path might be restricted? Maybe it's a bug and it shouldn't

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I need to follow this up with the Alerting team. There's a few types we're (logs and metrics) using that aren't exported from public or server, and so aren't really supposed to be used. Once they're bumped up to being part of the public or server contract, I'll remove the lint overrides. I'll handle in a followup 👍

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