Skip to content

Commit

Permalink
[7.x] [Logs UI] Add pagination to the log stream shared component (#8…
Browse files Browse the repository at this point in the history
…1193) (#83283)

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
Alejandro Fernández Gómez and kibanamachine authored Nov 13, 2020
1 parent 3cc8a88 commit 74dfc79
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 73 deletions.
16 changes: 11 additions & 5 deletions x-pack/plugins/infra/common/http_api/log_entries/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,17 @@ export type LogEntryContext = rt.TypeOf<typeof logEntryContextRT>;
export type LogEntry = rt.TypeOf<typeof logEntryRT>;

export const logEntriesResponseRT = rt.type({
data: rt.type({
entries: rt.array(logEntryRT),
topCursor: rt.union([logEntriesCursorRT, rt.null]),
bottomCursor: rt.union([logEntriesCursorRT, rt.null]),
}),
data: rt.intersection([
rt.type({
entries: rt.array(logEntryRT),
topCursor: rt.union([logEntriesCursorRT, rt.null]),
bottomCursor: rt.union([logEntriesCursorRT, rt.null]),
}),
rt.partial({
hasMoreBefore: rt.boolean,
hasMoreAfter: rt.boolean,
}),
]),
});

export type LogEntriesResponse = rt.TypeOf<typeof logEntriesResponseRT>;
47 changes: 40 additions & 7 deletions x-pack/plugins/infra/public/components/log_stream/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import { noop } from 'lodash';
import useMount from 'react-use/lib/useMount';
import { euiStyled } from '../../../../observability/public';
Expand All @@ -17,6 +17,8 @@ import { useLogStream } from '../../containers/logs/log_stream';

import { ScrollableLogTextStreamView } from '../logging/log_text_stream';

const PAGE_THRESHOLD = 2;

export interface LogStreamProps {
sourceId?: string;
startTimestamp: number;
Expand Down Expand Up @@ -58,7 +60,16 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
});

// Internal state
const { loadingState, entries, fetchEntries } = useLogStream({
const {
loadingState,
pageLoadingState,
entries,
hasMoreBefore,
hasMoreAfter,
fetchEntries,
fetchPreviousEntries,
fetchNextEntries,
} = useLogStream({
sourceId,
startTimestamp,
endTimestamp,
Expand All @@ -70,6 +81,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
const isReloading =
isLoadingSourceConfiguration || loadingState === 'uninitialized' || loadingState === 'loading';

const isLoadingMore = pageLoadingState === 'loading';

const columnConfigurations = useMemo(() => {
return sourceConfiguration ? sourceConfiguration.configuration.logColumns : [];
}, [sourceConfiguration]);
Expand All @@ -84,13 +97,33 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
[entries]
);

const parsedHeight = typeof height === 'number' ? `${height}px` : height;

// Component lifetime
useMount(() => {
loadSourceConfiguration();
fetchEntries();
});

const parsedHeight = typeof height === 'number' ? `${height}px` : height;
// Pagination handler
const handlePagination = useCallback(
({ fromScroll, pagesBeforeStart, pagesAfterEnd }) => {
if (!fromScroll) {
return;
}

if (isLoadingMore) {
return;
}

if (pagesBeforeStart < PAGE_THRESHOLD) {
fetchPreviousEntries();
} else if (pagesAfterEnd < PAGE_THRESHOLD) {
fetchNextEntries();
}
},
[isLoadingMore, fetchPreviousEntries, fetchNextEntries]
);

return (
<LogStreamContent height={parsedHeight}>
Expand All @@ -101,13 +134,13 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
scale="medium"
wrap={false}
isReloading={isReloading}
isLoadingMore={false}
hasMoreBeforeStart={false}
hasMoreAfterEnd={false}
isLoadingMore={isLoadingMore}
hasMoreBeforeStart={hasMoreBefore}
hasMoreAfterEnd={hasMoreAfter}
isStreaming={false}
lastLoadedTime={null}
jumpToTarget={noop}
reportVisibleInterval={noop}
reportVisibleInterval={handlePagination}
loadNewerItems={noop}
reloadItems={fetchEntries}
highlightedItem={highlight ?? null}
Expand Down
18 changes: 10 additions & 8 deletions x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,16 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action
case Action.ReceiveNewEntries:
return {
...prevState,
...action.payload,
entries: action.payload.entries,
topCursor: action.payload.topCursor,
bottomCursor: action.payload.bottomCursor,
centerCursor: getCenterCursor(action.payload.entries),
lastLoadedTime: new Date(),
isReloading: false,

// Be optimistic. If any of the before/after requests comes empty, set
// the corresponding flag to `false`
hasMoreBeforeStart: true,
hasMoreAfterEnd: true,
hasMoreBeforeStart: action.payload.hasMoreBefore ?? prevState.hasMoreBeforeStart,
hasMoreAfterEnd: action.payload.hasMoreAfter ?? prevState.hasMoreAfterEnd,
};

case Action.ReceiveEntriesBefore: {
const newEntries = action.payload.entries;
const prevEntries = cleanDuplicateItems(prevState.entries, newEntries);
Expand All @@ -385,7 +385,7 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action
const update = {
entries,
isLoadingMore: false,
hasMoreBeforeStart: newEntries.length > 0,
hasMoreBeforeStart: action.payload.hasMoreBefore ?? prevState.hasMoreBeforeStart,
// Keep the previous cursor if request comes empty, to easily extend the range.
topCursor: newEntries.length > 0 ? action.payload.topCursor : prevState.topCursor,
centerCursor: getCenterCursor(entries),
Expand All @@ -402,7 +402,7 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action
const update = {
entries,
isLoadingMore: false,
hasMoreAfterEnd: newEntries.length > 0,
hasMoreAfterEnd: action.payload.hasMoreAfter ?? prevState.hasMoreAfterEnd,
// Keep the previous cursor if request comes empty, to easily extend the range.
bottomCursor: newEntries.length > 0 ? action.payload.bottomCursor : prevState.bottomCursor,
centerCursor: getCenterCursor(entries),
Expand All @@ -419,6 +419,8 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action
topCursor: null,
bottomCursor: null,
centerCursor: null,
// Assume there are more pages on both ends unless proven wrong by the
// API with an explicit `false` response.
hasMoreBeforeStart: true,
hasMoreAfterEnd: true,
};
Expand Down
171 changes: 160 additions & 11 deletions x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { useState, useMemo } from 'react';
import { useMemo, useEffect } from 'react';
import useSetState from 'react-use/lib/useSetState';
import usePrevious from 'react-use/lib/usePrevious';
import { esKuery } from '../../../../../../../src/plugins/data/public';
import { fetchLogEntries } from '../log_entries/api/fetch_log_entries';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
Expand All @@ -21,19 +23,62 @@ interface LogStreamProps {

interface LogStreamState {
entries: LogEntry[];
topCursor: LogEntriesCursor | null;
bottomCursor: LogEntriesCursor | null;
hasMoreBefore: boolean;
hasMoreAfter: boolean;
}

type LoadingState = 'uninitialized' | 'loading' | 'success' | 'error';

interface LogStreamReturn extends LogStreamState {
fetchEntries: () => void;
loadingState: 'uninitialized' | 'loading' | 'success' | 'error';
fetchPreviousEntries: () => void;
fetchNextEntries: () => void;
loadingState: LoadingState;
pageLoadingState: LoadingState;
}

const INITIAL_STATE: LogStreamState = {
entries: [],
topCursor: null,
bottomCursor: null,
// Assume there are pages available until the API proves us wrong
hasMoreBefore: true,
hasMoreAfter: true,
};

const EMPTY_DATA = {
entries: [],
topCursor: null,
bottomCursor: null,
};

export function useLogStream({
sourceId,
startTimestamp,
endTimestamp,
query,
center,
}: LogStreamProps): LogStreamState {
}: LogStreamProps): LogStreamReturn {
const { services } = useKibanaContextForPlugin();
const [entries, setEntries] = useState<LogStreamState['entries']>([]);
const [state, setState] = useSetState<LogStreamState>(INITIAL_STATE);

// Ensure the pagination keeps working when the timerange gets extended
const prevStartTimestamp = usePrevious(startTimestamp);
const prevEndTimestamp = usePrevious(endTimestamp);

useEffect(() => {
if (prevStartTimestamp && prevStartTimestamp > startTimestamp) {
setState({ hasMoreBefore: true });
}
}, [prevStartTimestamp, startTimestamp, setState]);

useEffect(() => {
if (prevEndTimestamp && prevEndTimestamp < endTimestamp) {
setState({ hasMoreAfter: true });
}
}, [prevEndTimestamp, endTimestamp, setState]);

const parsedQuery = useMemo(() => {
return query
Expand All @@ -46,7 +91,7 @@ export function useLogStream({
{
cancelPreviousOn: 'creation',
createPromise: () => {
setEntries([]);
setState(INITIAL_STATE);
const fetchPosition = center ? { center } : { before: 'last' };

return fetchLogEntries(
Expand All @@ -61,26 +106,130 @@ export function useLogStream({
);
},
onResolve: ({ data }) => {
setEntries(data.entries);
setState((prevState) => ({
...data,
hasMoreBefore: data.hasMoreBefore ?? prevState.hasMoreBefore,
hasMoreAfter: data.hasMoreAfter ?? prevState.hasMoreAfter,
}));
},
},
[sourceId, startTimestamp, endTimestamp, query]
);

const loadingState = useMemo(() => convertPromiseStateToLoadingState(entriesPromise.state), [
entriesPromise.state,
]);
const [previousEntriesPromise, fetchPreviousEntries] = useTrackedPromise(
{
cancelPreviousOn: 'creation',
createPromise: () => {
if (state.topCursor === null) {
throw new Error(
'useLogState: Cannot fetch previous entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.'
);
}

if (!state.hasMoreBefore) {
return Promise.resolve({ data: EMPTY_DATA });
}

return fetchLogEntries(
{
sourceId,
startTimestamp,
endTimestamp,
query: parsedQuery,
before: state.topCursor,
},
services.http.fetch
);
},
onResolve: ({ data }) => {
if (!data.entries.length) {
return;
}
setState((prevState) => ({
entries: [...data.entries, ...prevState.entries],
hasMoreBefore: data.hasMoreBefore ?? prevState.hasMoreBefore,
topCursor: data.topCursor ?? prevState.topCursor,
}));
},
},
[sourceId, startTimestamp, endTimestamp, query, state.topCursor]
);

const [nextEntriesPromise, fetchNextEntries] = useTrackedPromise(
{
cancelPreviousOn: 'creation',
createPromise: () => {
if (state.bottomCursor === null) {
throw new Error(
'useLogState: Cannot fetch next entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.'
);
}

if (!state.hasMoreAfter) {
return Promise.resolve({ data: EMPTY_DATA });
}

return fetchLogEntries(
{
sourceId,
startTimestamp,
endTimestamp,
query: parsedQuery,
after: state.bottomCursor,
},
services.http.fetch
);
},
onResolve: ({ data }) => {
if (!data.entries.length) {
return;
}
setState((prevState) => ({
entries: [...prevState.entries, ...data.entries],
hasMoreAfter: data.hasMoreAfter ?? prevState.hasMoreAfter,
bottomCursor: data.bottomCursor ?? prevState.bottomCursor,
}));
},
},
[sourceId, startTimestamp, endTimestamp, query, state.bottomCursor]
);

const loadingState = useMemo<LoadingState>(
() => convertPromiseStateToLoadingState(entriesPromise.state),
[entriesPromise.state]
);

const pageLoadingState = useMemo<LoadingState>(() => {
const states = [previousEntriesPromise.state, nextEntriesPromise.state];

if (states.includes('pending')) {
return 'loading';
}

if (states.includes('rejected')) {
return 'error';
}

if (states.includes('resolved')) {
return 'success';
}

return 'uninitialized';
}, [previousEntriesPromise.state, nextEntriesPromise.state]);

return {
entries,
...state,
fetchEntries,
fetchPreviousEntries,
fetchNextEntries,
loadingState,
pageLoadingState,
};
}

function convertPromiseStateToLoadingState(
state: 'uninitialized' | 'pending' | 'resolved' | 'rejected'
): LogStreamState['loadingState'] {
): LoadingState {
switch (state) {
case 'uninitialized':
return 'uninitialized';
Expand Down
Loading

0 comments on commit 74dfc79

Please sign in to comment.