[Logs UI] Fix errors during navigation (#78319)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Gómez 2020-11-09 12:43:11 +01:00 committed by GitHub
parent 202dec7c24
commit 6110ef82a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 47 additions and 14 deletions

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState, useReducer, useCallback } from 'react';
import { useMountedState } from 'react-use';
import createContainer from 'constate';
import { pick, throttle } from 'lodash';
import { TimeKey, timeKeyIsBetween } from '../../../../common/time';
@ -146,15 +147,20 @@ const useFetchEntriesEffect = (
props: LogEntriesProps
) => {
const { services } = useKibanaContextForPlugin();
const isMounted = useMountedState();
const [prevParams, cachePrevParams] = useState<LogEntriesProps | undefined>();
const [startedStreaming, setStartedStreaming] = useState(false);
const dispatchIfMounted = useCallback((action) => (isMounted() ? dispatch(action) : undefined), [
dispatch,
isMounted,
]);
const runFetchNewEntriesRequest = async (overrides: Partial<LogEntriesProps> = {}) => {
if (!props.startTimestamp || !props.endTimestamp) {
return;
}
dispatch({ type: Action.FetchingNewEntries });
dispatchIfMounted({ type: Action.FetchingNewEntries });
try {
const commonFetchArgs: LogEntriesBaseRequest = {
@ -175,13 +181,15 @@ const useFetchEntriesEffect = (
};
const { data: payload } = await fetchLogEntries(fetchArgs, services.http.fetch);
dispatch({ type: Action.ReceiveNewEntries, payload });
dispatchIfMounted({ type: Action.ReceiveNewEntries, payload });
// Move position to the bottom if it's the first load.
// Do it in the next tick to allow the `dispatch` to fire
if (!props.timeKey && payload.bottomCursor) {
setTimeout(() => {
props.jumpToTargetPosition(payload.bottomCursor!);
if (isMounted()) {
props.jumpToTargetPosition(payload.bottomCursor!);
}
});
} else if (
props.timeKey &&
@ -192,7 +200,7 @@ const useFetchEntriesEffect = (
props.jumpToTargetPosition(payload.topCursor);
}
} catch (e) {
dispatch({ type: Action.ErrorOnNewEntries });
dispatchIfMounted({ type: Action.ErrorOnNewEntries });
}
};
@ -210,7 +218,7 @@ const useFetchEntriesEffect = (
return;
}
dispatch({ type: Action.FetchingMoreEntries });
dispatchIfMounted({ type: Action.FetchingMoreEntries });
try {
const commonFetchArgs: LogEntriesBaseRequest = {
@ -232,14 +240,14 @@ const useFetchEntriesEffect = (
const { data: payload } = await fetchLogEntries(fetchArgs, services.http.fetch);
dispatch({
dispatchIfMounted({
type: getEntriesBefore ? Action.ReceiveEntriesBefore : Action.ReceiveEntriesAfter,
payload,
});
return payload.bottomCursor;
} catch (e) {
dispatch({ type: Action.ErrorOnMoreEntries });
dispatchIfMounted({ type: Action.ErrorOnMoreEntries });
}
};
@ -322,7 +330,7 @@ const useFetchEntriesEffect = (
after: props.endTimestamp > prevParams.endTimestamp,
};
dispatch({ type: Action.ExpandRange, payload: shouldExpand });
dispatchIfMounted({ type: Action.ExpandRange, payload: shouldExpand });
};
const expandRangeEffectDependencies = [

View file

@ -6,13 +6,15 @@
/* eslint-disable max-classes-per-file */
import { DependencyList, useEffect, useMemo, useRef, useState } from 'react';
import { DependencyList, useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { useMountedState } from 'react-use';
interface UseTrackedPromiseArgs<Arguments extends any[], Result> {
createPromise: (...args: Arguments) => Promise<Result>;
onResolve?: (result: Result) => void;
onReject?: (value: unknown) => void;
cancelPreviousOn?: 'creation' | 'settlement' | 'resolution' | 'rejection' | 'never';
triggerOrThrow?: 'always' | 'whenMounted';
}
/**
@ -64,6 +66,16 @@ interface UseTrackedPromiseArgs<Arguments extends any[], Result> {
* The last argument is a normal React hook dependency list that indicates
* under which conditions a new reference to the configuration object should be
* used.
*
* The `onResolve`, `onReject` and possible uncatched errors are only triggered
* if the underlying component is mounted. To ensure they always trigger (i.e.
* if the promise is called in a `useLayoutEffect`) use the `triggerOrThrow`
* attribute:
*
* 'whenMounted': (default) they are called only if the component is mounted.
*
* 'always': they always call. The consumer is then responsible of ensuring no
* side effects happen if the underlying component is not mounted.
*/
export const useTrackedPromise = <Arguments extends any[], Result>(
{
@ -71,9 +83,20 @@ export const useTrackedPromise = <Arguments extends any[], Result>(
onResolve = noOp,
onReject = noOp,
cancelPreviousOn = 'never',
triggerOrThrow = 'whenMounted',
}: UseTrackedPromiseArgs<Arguments, Result>,
dependencies: DependencyList
) => {
const isComponentMounted = useMountedState();
const shouldTriggerOrThrow = useCallback(() => {
switch (triggerOrThrow) {
case 'always':
return true;
case 'whenMounted':
return isComponentMounted();
}
}, [isComponentMounted, triggerOrThrow]);
/**
* If a promise is currently pending, this holds a reference to it and its
* cancellation function.
@ -144,7 +167,7 @@ export const useTrackedPromise = <Arguments extends any[], Result>(
(pendingPromise) => pendingPromise.promise !== newPendingPromise.promise
);
if (onResolve) {
if (onResolve && shouldTriggerOrThrow()) {
onResolve(value);
}
@ -173,11 +196,13 @@ export const useTrackedPromise = <Arguments extends any[], Result>(
(pendingPromise) => pendingPromise.promise !== newPendingPromise.promise
);
if (onReject) {
onReject(value);
}
if (shouldTriggerOrThrow()) {
if (onReject) {
onReject(value);
}
throw value;
throw value;
}
}
),
};