44 * you may not use this file except in compliance with the Elastic License.
55 */
66
7- import { useState , useMemo } from 'react' ;
7+ import { useMemo , useEffect } from 'react' ;
8+ import useSetState from 'react-use/lib/useSetState' ;
9+ import usePrevious from 'react-use/lib/usePrevious' ;
810import { esKuery } from '../../../../../../../src/plugins/data/public' ;
911import { fetchLogEntries } from '../log_entries/api/fetch_log_entries' ;
1012import { useTrackedPromise } from '../../../utils/use_tracked_promise' ;
@@ -21,19 +23,62 @@ interface LogStreamProps {
2123
2224interface LogStreamState {
2325 entries : LogEntry [ ] ;
26+ topCursor : LogEntriesCursor | null ;
27+ bottomCursor : LogEntriesCursor | null ;
28+ hasMoreBefore : boolean ;
29+ hasMoreAfter : boolean ;
30+ }
31+
32+ type LoadingState = 'uninitialized' | 'loading' | 'success' | 'error' ;
33+
34+ interface LogStreamReturn extends LogStreamState {
2435 fetchEntries : ( ) => void ;
25- loadingState : 'uninitialized' | 'loading' | 'success' | 'error' ;
36+ fetchPreviousEntries : ( ) => void ;
37+ fetchNextEntries : ( ) => void ;
38+ loadingState : LoadingState ;
39+ pageLoadingState : LoadingState ;
2640}
2741
42+ const INITIAL_STATE : LogStreamState = {
43+ entries : [ ] ,
44+ topCursor : null ,
45+ bottomCursor : null ,
46+ // Assume there are pages available until the API proves us wrong
47+ hasMoreBefore : true ,
48+ hasMoreAfter : true ,
49+ } ;
50+
51+ const EMPTY_DATA = {
52+ entries : [ ] ,
53+ topCursor : null ,
54+ bottomCursor : null ,
55+ } ;
56+
2857export function useLogStream ( {
2958 sourceId,
3059 startTimestamp,
3160 endTimestamp,
3261 query,
3362 center,
34- } : LogStreamProps ) : LogStreamState {
63+ } : LogStreamProps ) : LogStreamReturn {
3564 const { services } = useKibanaContextForPlugin ( ) ;
36- const [ entries , setEntries ] = useState < LogStreamState [ 'entries' ] > ( [ ] ) ;
65+ const [ state , setState ] = useSetState < LogStreamState > ( INITIAL_STATE ) ;
66+
67+ // Ensure the pagination keeps working when the timerange gets extended
68+ const prevStartTimestamp = usePrevious ( startTimestamp ) ;
69+ const prevEndTimestamp = usePrevious ( endTimestamp ) ;
70+
71+ useEffect ( ( ) => {
72+ if ( prevStartTimestamp && prevStartTimestamp > startTimestamp ) {
73+ setState ( { hasMoreBefore : true } ) ;
74+ }
75+ } , [ prevStartTimestamp , startTimestamp , setState ] ) ;
76+
77+ useEffect ( ( ) => {
78+ if ( prevEndTimestamp && prevEndTimestamp < endTimestamp ) {
79+ setState ( { hasMoreAfter : true } ) ;
80+ }
81+ } , [ prevEndTimestamp , endTimestamp , setState ] ) ;
3782
3883 const parsedQuery = useMemo ( ( ) => {
3984 return query
@@ -46,7 +91,7 @@ export function useLogStream({
4691 {
4792 cancelPreviousOn : 'creation' ,
4893 createPromise : ( ) => {
49- setEntries ( [ ] ) ;
94+ setState ( INITIAL_STATE ) ;
5095 const fetchPosition = center ? { center } : { before : 'last' } ;
5196
5297 return fetchLogEntries (
@@ -61,26 +106,130 @@ export function useLogStream({
61106 ) ;
62107 } ,
63108 onResolve : ( { data } ) => {
64- setEntries ( data . entries ) ;
109+ setState ( ( prevState ) => ( {
110+ ...data ,
111+ hasMoreBefore : data . hasMoreBefore ?? prevState . hasMoreBefore ,
112+ hasMoreAfter : data . hasMoreAfter ?? prevState . hasMoreAfter ,
113+ } ) ) ;
65114 } ,
66115 } ,
67116 [ sourceId , startTimestamp , endTimestamp , query ]
68117 ) ;
69118
70- const loadingState = useMemo ( ( ) => convertPromiseStateToLoadingState ( entriesPromise . state ) , [
71- entriesPromise . state ,
72- ] ) ;
119+ const [ previousEntriesPromise , fetchPreviousEntries ] = useTrackedPromise (
120+ {
121+ cancelPreviousOn : 'creation' ,
122+ createPromise : ( ) => {
123+ if ( state . topCursor === null ) {
124+ throw new Error (
125+ 'useLogState: Cannot fetch previous entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.'
126+ ) ;
127+ }
128+
129+ if ( ! state . hasMoreBefore ) {
130+ return Promise . resolve ( { data : EMPTY_DATA } ) ;
131+ }
132+
133+ return fetchLogEntries (
134+ {
135+ sourceId,
136+ startTimestamp,
137+ endTimestamp,
138+ query : parsedQuery ,
139+ before : state . topCursor ,
140+ } ,
141+ services . http . fetch
142+ ) ;
143+ } ,
144+ onResolve : ( { data } ) => {
145+ if ( ! data . entries . length ) {
146+ return ;
147+ }
148+ setState ( ( prevState ) => ( {
149+ entries : [ ...data . entries , ...prevState . entries ] ,
150+ hasMoreBefore : data . hasMoreBefore ?? prevState . hasMoreBefore ,
151+ topCursor : data . topCursor ?? prevState . topCursor ,
152+ } ) ) ;
153+ } ,
154+ } ,
155+ [ sourceId , startTimestamp , endTimestamp , query , state . topCursor ]
156+ ) ;
157+
158+ const [ nextEntriesPromise , fetchNextEntries ] = useTrackedPromise (
159+ {
160+ cancelPreviousOn : 'creation' ,
161+ createPromise : ( ) => {
162+ if ( state . bottomCursor === null ) {
163+ throw new Error (
164+ 'useLogState: Cannot fetch next entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.'
165+ ) ;
166+ }
167+
168+ if ( ! state . hasMoreAfter ) {
169+ return Promise . resolve ( { data : EMPTY_DATA } ) ;
170+ }
171+
172+ return fetchLogEntries (
173+ {
174+ sourceId,
175+ startTimestamp,
176+ endTimestamp,
177+ query : parsedQuery ,
178+ after : state . bottomCursor ,
179+ } ,
180+ services . http . fetch
181+ ) ;
182+ } ,
183+ onResolve : ( { data } ) => {
184+ if ( ! data . entries . length ) {
185+ return ;
186+ }
187+ setState ( ( prevState ) => ( {
188+ entries : [ ...prevState . entries , ...data . entries ] ,
189+ hasMoreAfter : data . hasMoreAfter ?? prevState . hasMoreAfter ,
190+ bottomCursor : data . bottomCursor ?? prevState . bottomCursor ,
191+ } ) ) ;
192+ } ,
193+ } ,
194+ [ sourceId , startTimestamp , endTimestamp , query , state . bottomCursor ]
195+ ) ;
196+
197+ const loadingState = useMemo < LoadingState > (
198+ ( ) => convertPromiseStateToLoadingState ( entriesPromise . state ) ,
199+ [ entriesPromise . state ]
200+ ) ;
201+
202+ const pageLoadingState = useMemo < LoadingState > ( ( ) => {
203+ const states = [ previousEntriesPromise . state , nextEntriesPromise . state ] ;
204+
205+ if ( states . includes ( 'pending' ) ) {
206+ return 'loading' ;
207+ }
208+
209+ if ( states . includes ( 'rejected' ) ) {
210+ return 'error' ;
211+ }
212+
213+ if ( states . includes ( 'resolved' ) ) {
214+ return 'success' ;
215+ }
216+
217+ return 'uninitialized' ;
218+ } , [ previousEntriesPromise . state , nextEntriesPromise . state ] ) ;
73219
74220 return {
75- entries ,
221+ ... state ,
76222 fetchEntries,
223+ fetchPreviousEntries,
224+ fetchNextEntries,
77225 loadingState,
226+ pageLoadingState,
78227 } ;
79228}
80229
81230function convertPromiseStateToLoadingState (
82231 state : 'uninitialized' | 'pending' | 'resolved' | 'rejected'
83- ) : LogStreamState [ 'loadingState' ] {
232+ ) : LoadingState {
84233 switch ( state ) {
85234 case 'uninitialized' :
86235 return 'uninitialized' ;
0 commit comments