[Logs UI] Refactor log entry data fetching to hooks (#51526)

* Get initialinitial log fetch working with v2 store

* Replicate shouldLoadAroundPosition logic within hooks

* Reload entries on filter change

* Add scroll to load additional entries functionality

* Cleanup types types and remove state/remote folder

* Typescript cleanup

* Remove extraneous console.log

* Fix typecheck

* Add action to load new entries manually

* Typecheck fix

* Move v2 store stuff into logs containers

* Typecheck fix

* More typecheck fix

* Remove filterQuery from log highlights redux bridge

* Rename LogEntriesDependencies to LogEntriesFetchParams

* Fix endless reloading bug

* Fix duplicate entry rendering

* Make sourceId into a dynamic parameter

* Fix bug in pagesAfterEnd not being reported causing endless reload

* Fix bugs with live streaming
This commit is contained in:
Zacqary Adam Xeper 2019-12-09 17:24:58 -06:00 committed by GitHub
parent d429a9a1e8
commit 21f9ab255a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 618 additions and 939 deletions

View file

@ -21,6 +21,7 @@ import { InfraFrontendLibs } from '../lib/lib';
import { PageRouter } from '../routes';
import { createStore } from '../store';
import { ApolloClientContext } from '../utils/apollo_context';
import { ReduxStateContextProvider } from '../utils/redux_context';
import { HistoryContext } from '../utils/history_context';
import {
useUiSetting$,
@ -46,15 +47,17 @@ export async function startApp(libs: InfraFrontendLibs) {
<UICapabilitiesProvider>
<EuiErrorBoundary>
<ReduxStoreProvider store={store}>
<ApolloProvider client={libs.apolloClient}>
<ApolloClientContext.Provider value={libs.apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<HistoryContext.Provider value={history}>
<PageRouter history={history} />
</HistoryContext.Provider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</ApolloProvider>
<ReduxStateContextProvider>
<ApolloProvider client={libs.apolloClient}>
<ApolloClientContext.Provider value={libs.apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<HistoryContext.Provider value={history}>
<PageRouter history={history} />
</HistoryContext.Provider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</ApolloProvider>
</ReduxStateContextProvider>
</ReduxStoreProvider>
</EuiErrorBoundary>
</UICapabilitiesProvider>

View file

@ -18,7 +18,7 @@ interface LogTextStreamLoadingItemViewProps {
hasMore: boolean;
isLoading: boolean;
isStreaming: boolean;
lastStreamingUpdate: number | null;
lastStreamingUpdate: Date | null;
onLoadMore?: () => void;
}

View file

@ -39,7 +39,7 @@ interface ScrollableLogTextStreamViewProps {
hasMoreBeforeStart: boolean;
hasMoreAfterEnd: boolean;
isStreaming: boolean;
lastLoadedTime: number | null;
lastLoadedTime: Date | null;
target: TimeKey | null;
jumpToTarget: (target: TimeKey) => any;
reportVisibleInterval: (params: {
@ -143,7 +143,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
const hasItems = items.length > 0;
return (
<ScrollableLogTextStreamViewWrapper>
{isReloading && !hasItems ? (
{isReloading && (!isStreaming || !hasItems) ? (
<InfraLoadingPanel
width="100%"
height="100%"

View file

@ -163,8 +163,10 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
// Flag the scrollTop change that's about to happen as programmatic, as
// opposed to being in direct response to user input
this.nextScrollEventFromCenterTarget = true;
scrollRef.current.scrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2;
return true;
const currentScrollTop = scrollRef.current.scrollTop;
const newScrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2;
scrollRef.current.scrollTop = newScrollTop;
return currentScrollTop !== newScrollTop;
}
return false;
};

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ApolloClient } from 'apollo-client';
import { TimeKey } from '../../../../common/time';
import { logEntriesQuery } from '../../../graphql/log_entries.gql_query';
import { useApolloClient } from '../../../utils/apollo_context';
import { LogEntriesResponse } from '.';
const LOAD_CHUNK_SIZE = 200;
type LogEntriesGetter = (
client: ApolloClient<{}>,
countBefore: number,
countAfter: number
) => (params: {
sourceId: string;
timeKey: TimeKey | null;
filterQuery: string | null;
}) => Promise<LogEntriesResponse>;
const getLogEntries: LogEntriesGetter = (client, countBefore, countAfter) => async ({
sourceId,
timeKey,
filterQuery,
}) => {
if (!timeKey) throw new Error('TimeKey is null');
const result = await client.query({
query: logEntriesQuery,
variables: {
sourceId,
timeKey: { time: timeKey.time, tiebreaker: timeKey.tiebreaker },
countBefore,
countAfter,
filterQuery,
},
fetchPolicy: 'no-cache',
});
// Workaround for Typescript. Since we're removing the GraphQL API in another PR or two
// 7.6 goes out I don't think it's worth the effort to actually make this
// typecheck pass
const { source } = result.data as any;
const { logEntriesAround } = source;
return {
entries: logEntriesAround.entries,
entriesStart: logEntriesAround.start,
entriesEnd: logEntriesAround.end,
hasMoreAfterEnd: logEntriesAround.hasMoreAfter,
hasMoreBeforeStart: logEntriesAround.hasMoreBefore,
lastLoadedTime: new Date(),
};
};
export const useGraphQLQueries = () => {
const client = useApolloClient();
if (!client) throw new Error('Unable to get Apollo Client from context');
return {
getLogEntriesAround: getLogEntries(client, LOAD_CHUNK_SIZE, LOAD_CHUNK_SIZE),
getLogEntriesBefore: getLogEntries(client, LOAD_CHUNK_SIZE, 0),
getLogEntriesAfter: getLogEntries(client, 0, LOAD_CHUNK_SIZE),
};
};

View file

@ -0,0 +1,268 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useState, useReducer, useCallback } from 'react';
import createContainer from 'constate';
import { pick, throttle } from 'lodash';
import { useGraphQLQueries } from './gql_queries';
import { TimeKey, timeKeyIsBetween } from '../../../../common/time';
import { InfraLogEntry } from './types';
const DESIRED_BUFFER_PAGES = 2;
enum Action {
FetchingNewEntries,
FetchingMoreEntries,
ReceiveNewEntries,
ReceiveEntriesBefore,
ReceiveEntriesAfter,
ErrorOnNewEntries,
ErrorOnMoreEntries,
}
type ReceiveActions =
| Action.ReceiveNewEntries
| Action.ReceiveEntriesBefore
| Action.ReceiveEntriesAfter;
interface ReceiveEntriesAction {
type: ReceiveActions;
payload: LogEntriesResponse;
}
interface FetchOrErrorAction {
type: Exclude<Action, ReceiveActions>;
}
type ActionObj = ReceiveEntriesAction | FetchOrErrorAction;
type Dispatch = (action: ActionObj) => void;
interface LogEntriesProps {
filterQuery: string | null;
timeKey: TimeKey | null;
pagesBeforeStart: number | null;
pagesAfterEnd: number | null;
sourceId: string;
isAutoReloading: boolean;
}
type FetchEntriesParams = Omit<LogEntriesProps, 'isAutoReloading'>;
type FetchMoreEntriesParams = Pick<LogEntriesProps, 'pagesBeforeStart' | 'pagesAfterEnd'>;
export interface LogEntriesResponse {
entries: InfraLogEntry[];
entriesStart: TimeKey | null;
entriesEnd: TimeKey | null;
hasMoreAfterEnd: boolean;
hasMoreBeforeStart: boolean;
lastLoadedTime: Date | null;
}
export type LogEntriesStateParams = {
isReloading: boolean;
isLoadingMore: boolean;
} & LogEntriesResponse;
export interface LogEntriesCallbacks {
fetchNewerEntries: () => Promise<void>;
}
export const logEntriesInitialCallbacks = {
fetchNewerEntries: async () => {},
};
export const logEntriesInitialState: LogEntriesStateParams = {
entries: [],
entriesStart: null,
entriesEnd: null,
hasMoreAfterEnd: false,
hasMoreBeforeStart: false,
isReloading: true,
isLoadingMore: false,
lastLoadedTime: null,
};
const cleanDuplicateItems = (entriesA: InfraLogEntry[], entriesB: InfraLogEntry[]) => {
const gids = new Set(entriesB.map(item => item.gid));
return entriesA.filter(item => !gids.has(item.gid));
};
const shouldFetchNewEntries = ({
prevParams,
timeKey,
filterQuery,
entriesStart,
entriesEnd,
}: FetchEntriesParams & LogEntriesStateParams & { prevParams: FetchEntriesParams }) => {
if (!timeKey) return false;
const shouldLoadWithNewFilter = filterQuery !== prevParams.filterQuery;
const shouldLoadAroundNewPosition =
!entriesStart || !entriesEnd || !timeKeyIsBetween(entriesStart, entriesEnd, timeKey);
return shouldLoadWithNewFilter || shouldLoadAroundNewPosition;
};
enum ShouldFetchMoreEntries {
Before,
After,
}
const shouldFetchMoreEntries = (
{ pagesAfterEnd, pagesBeforeStart }: FetchMoreEntriesParams,
{ hasMoreBeforeStart, hasMoreAfterEnd }: LogEntriesStateParams
) => {
if (pagesBeforeStart === null || pagesAfterEnd === null) return false;
if (pagesBeforeStart < DESIRED_BUFFER_PAGES && hasMoreBeforeStart)
return ShouldFetchMoreEntries.Before;
if (pagesAfterEnd < DESIRED_BUFFER_PAGES && hasMoreAfterEnd) return ShouldFetchMoreEntries.After;
return false;
};
const useFetchEntriesEffect = (
state: LogEntriesStateParams,
dispatch: Dispatch,
props: LogEntriesProps
) => {
const { getLogEntriesAround, getLogEntriesBefore, getLogEntriesAfter } = useGraphQLQueries();
const [prevParams, cachePrevParams] = useState(props);
const [startedStreaming, setStartedStreaming] = useState(false);
const runFetchNewEntriesRequest = async () => {
dispatch({ type: Action.FetchingNewEntries });
try {
const payload = await getLogEntriesAround(props);
dispatch({ type: Action.ReceiveNewEntries, payload });
} catch (e) {
dispatch({ type: Action.ErrorOnNewEntries });
}
};
const runFetchMoreEntriesRequest = async (direction: ShouldFetchMoreEntries) => {
dispatch({ type: Action.FetchingMoreEntries });
const getEntriesBefore = direction === ShouldFetchMoreEntries.Before;
const timeKey = getEntriesBefore
? state.entries[0].key
: state.entries[state.entries.length - 1].key;
const getMoreLogEntries = getEntriesBefore ? getLogEntriesBefore : getLogEntriesAfter;
try {
const payload = await getMoreLogEntries({ ...props, timeKey });
dispatch({
type: getEntriesBefore ? Action.ReceiveEntriesBefore : Action.ReceiveEntriesAfter,
payload,
});
} catch (e) {
dispatch({ type: Action.ErrorOnMoreEntries });
}
};
const fetchNewEntriesEffectDependencies = Object.values(
pick(props, ['sourceId', 'filterQuery', 'timeKey'])
);
const fetchNewEntriesEffect = () => {
if (props.isAutoReloading) return;
if (shouldFetchNewEntries({ ...props, ...state, prevParams })) {
runFetchNewEntriesRequest();
}
cachePrevParams(props);
};
const fetchMoreEntriesEffectDependencies = [
...Object.values(pick(props, ['pagesAfterEnd', 'pagesBeforeStart'])),
Object.values(pick(state, ['hasMoreBeforeStart', 'hasMoreAfterEnd'])),
];
const fetchMoreEntriesEffect = () => {
if (state.isLoadingMore || props.isAutoReloading) return;
const direction = shouldFetchMoreEntries(props, state);
switch (direction) {
case ShouldFetchMoreEntries.Before:
case ShouldFetchMoreEntries.After:
runFetchMoreEntriesRequest(direction);
break;
default:
break;
}
};
const fetchNewerEntries = useCallback(
throttle(() => runFetchMoreEntriesRequest(ShouldFetchMoreEntries.After), 500),
[props]
);
const streamEntriesEffectDependencies = [props.isAutoReloading, state.isLoadingMore];
const streamEntriesEffect = () => {
(async () => {
if (props.isAutoReloading && !state.isLoadingMore) {
if (startedStreaming) {
await new Promise(res => setTimeout(res, 5000));
} else {
setStartedStreaming(true);
}
fetchNewerEntries();
} else if (!props.isAutoReloading) {
setStartedStreaming(false);
}
})();
};
useEffect(fetchNewEntriesEffect, fetchNewEntriesEffectDependencies);
useEffect(fetchMoreEntriesEffect, fetchMoreEntriesEffectDependencies);
useEffect(streamEntriesEffect, streamEntriesEffectDependencies);
return { fetchNewerEntries };
};
export const useLogEntriesState: (
props: LogEntriesProps
) => [LogEntriesStateParams, LogEntriesCallbacks] = props => {
const [state, dispatch] = useReducer(logEntriesStateReducer, logEntriesInitialState);
const { fetchNewerEntries } = useFetchEntriesEffect(state, dispatch, props);
const callbacks = { fetchNewerEntries };
return [state, callbacks];
};
const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: ActionObj) => {
switch (action.type) {
case Action.ReceiveNewEntries:
return { ...prevState, ...action.payload, isReloading: false };
case Action.ReceiveEntriesBefore: {
const prevEntries = cleanDuplicateItems(prevState.entries, action.payload.entries);
const newEntries = [...action.payload.entries, ...prevEntries];
const { hasMoreBeforeStart, entriesStart, lastLoadedTime } = action.payload;
const update = {
entries: newEntries,
isLoadingMore: false,
hasMoreBeforeStart,
entriesStart,
lastLoadedTime,
};
return { ...prevState, ...update };
}
case Action.ReceiveEntriesAfter: {
const prevEntries = cleanDuplicateItems(prevState.entries, action.payload.entries);
const newEntries = [...prevEntries, ...action.payload.entries];
const { hasMoreAfterEnd, entriesEnd, lastLoadedTime } = action.payload;
const update = {
entries: newEntries,
isLoadingMore: false,
hasMoreAfterEnd,
entriesEnd,
lastLoadedTime,
};
return { ...prevState, ...update };
}
case Action.FetchingNewEntries:
return { ...prevState, isReloading: true };
case Action.FetchingMoreEntries:
return { ...prevState, isLoadingMore: true };
case Action.ErrorOnNewEntries:
return { ...prevState, isReloading: false };
case Action.ErrorOnMoreEntries:
return { ...prevState, isLoadingMore: false };
default:
throw new Error();
}
};
export const LogEntriesState = createContainer(useLogEntriesState);

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/** A segment of the log entry message that was derived from a field */
export interface InfraLogMessageFieldSegment {
/** The field the segment was derived from */
field: string;
/** The segment's message */
value: string;
/** A list of highlighted substrings of the value */
highlights: string[];
}
/** A segment of the log entry message that was derived from a string literal */
export interface InfraLogMessageConstantSegment {
/** The segment's message */
constant: string;
}
export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment;
/** A special built-in column that contains the log entry's timestamp */
export interface InfraLogEntryTimestampColumn {
/** The id of the corresponding column configuration */
columnId: string;
/** The timestamp */
timestamp: number;
}
/** A special built-in column that contains the log entry's constructed message */
export interface InfraLogEntryMessageColumn {
/** The id of the corresponding column configuration */
columnId: string;
/** A list of the formatted log entry segments */
message: InfraLogMessageSegment[];
}
/** A column that contains the value of a field of the log entry */
export interface InfraLogEntryFieldColumn {
/** The id of the corresponding column configuration */
columnId: string;
/** The field name of the column */
field: string;
/** The value of the field in the log entry */
value: string;
/** A list of highlighted substrings of the value */
highlights: string[];
}
/** A column of a log entry */
export type InfraLogEntryColumn =
| InfraLogEntryTimestampColumn
| InfraLogEntryMessageColumn
| InfraLogEntryFieldColumn;
/** A representation of the log entry's position in the event stream */
export interface InfraTimeKey {
/** The timestamp of the event that the log entry corresponds to */
time: number;
/** The tiebreaker that disambiguates events with the same timestamp */
tiebreaker: number;
}
/** A log entry */
export interface InfraLogEntry {
/** A unique representation of the log entry's position in the event stream */
key: InfraTimeKey;
/** The log entry's id */
gid: string;
/** The source id */
source: string;
/** The columns used for rendering the log entry */
columns: InfraLogEntryColumn[];
}

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useContext } from 'react';
import createContainer from 'constate';
import { ReduxStateContext } from '../../../utils/redux_context';
import { logFilterSelectors as logFilterReduxSelectors } from '../../../store/local/selectors';
export const useLogFilterState = () => {
const { local: state } = useContext(ReduxStateContext);
const filterQuery = logFilterReduxSelectors.selectLogFilterQueryAsJson(state);
return { filterQuery };
};
export interface LogFilterStateParams {
filterQuery: string | null;
}
export const logFilterInitialState = {
filterQuery: null,
};
export const LogFilterState = createContainer(useLogFilterState);

View file

@ -6,30 +6,30 @@
import createContainer from 'constate';
import { useState, useContext } from 'react';
import { useLogEntryHighlights } from './log_entry_highlights';
import { useLogSummaryHighlights } from './log_summary_highlights';
import { useNextAndPrevious } from './next_and_previous';
import { useReduxBridgeSetters } from './redux_bridge_setters';
import { useLogSummaryBufferInterval } from '../log_summary';
import { LogViewConfiguration } from '../log_view_configuration';
import { TimeKey } from '../../../../common/time';
export const useLogHighlightsState = ({
sourceId,
sourceVersion,
entriesStart,
entriesEnd,
filterQuery,
}: {
sourceId: string;
sourceVersion: string | undefined;
entriesStart: TimeKey | null;
entriesEnd: TimeKey | null;
filterQuery: string | null;
}) => {
const [highlightTerms, setHighlightTerms] = useState<string[]>([]);
const {
startKey,
endKey,
filterQuery,
visibleMidpoint,
setStartKey,
setEndKey,
setFilterQuery,
setVisibleMidpoint,
jumpToTarget,
@ -50,7 +50,14 @@ export const useLogHighlightsState = ({
logEntryHighlights,
logEntryHighlightsById,
loadLogEntryHighlightsRequest,
} = useLogEntryHighlights(sourceId, sourceVersion, startKey, endKey, filterQuery, highlightTerms);
} = useLogEntryHighlights(
sourceId,
sourceVersion,
entriesStart,
entriesEnd,
filterQuery,
highlightTerms
);
const { logSummaryHighlights, loadLogSummaryHighlightsRequest } = useLogSummaryHighlights(
sourceId,
@ -78,8 +85,6 @@ export const useLogHighlightsState = ({
return {
highlightTerms,
setHighlightTerms,
setStartKey,
setEndKey,
setFilterQuery,
logEntryHighlights,
logEntryHighlightsById,

View file

@ -8,19 +8,13 @@ import { useState } from 'react';
import { TimeKey } from '../../../../common/time';
export const useReduxBridgeSetters = () => {
const [startKey, setStartKey] = useState<TimeKey | null>(null);
const [endKey, setEndKey] = useState<TimeKey | null>(null);
const [filterQuery, setFilterQuery] = useState<string | null>(null);
const [visibleMidpoint, setVisibleMidpoint] = useState<TimeKey | null>(null);
const [jumpToTarget, setJumpToTarget] = useState<(target: TimeKey) => void>(() => undefined);
return {
startKey,
endKey,
filterQuery,
visibleMidpoint,
setStartKey,
setEndKey,
setFilterQuery,
setVisibleMidpoint,
jumpToTarget,

View file

@ -8,23 +8,11 @@ import React, { useEffect, useContext } from 'react';
import { TimeKey } from '../../../../common/time';
import { withLogFilter } from '../with_log_filter';
import { withStreamItems } from '../with_stream_items';
import { withLogPosition } from '../with_log_position';
import { LogHighlightsState } from './log_highlights';
// Bridges Redux container state with Hooks state. Once state is moved fully from
// Redux to Hooks this can be removed.
export const LogHighlightsStreamItemsBridge = withStreamItems(
({ entriesStart, entriesEnd }: { entriesStart: TimeKey | null; entriesEnd: TimeKey | null }) => {
const { setStartKey, setEndKey } = useContext(LogHighlightsState.Context);
useEffect(() => {
setStartKey(entriesStart);
setEndKey(entriesEnd);
}, [entriesStart, entriesEnd]);
return null;
}
);
export const LogHighlightsPositionBridge = withLogPosition(
({
@ -61,7 +49,6 @@ export const LogHighlightsFilterQueryBridge = withLogFilter(
export const LogHighlightsBridge = ({ indexPattern }: { indexPattern: any }) => (
<>
<LogHighlightsStreamItemsBridge />
<LogHighlightsPositionBridge />
<LogHighlightsFilterQueryBridge indexPattern={indexPattern} />
</>

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useContext } from 'react';
import createContainer from 'constate';
import { ReduxStateContext } from '../../../utils/redux_context';
import { logPositionSelectors as logPositionReduxSelectors } from '../../../store/local/selectors';
import { TimeKey } from '../../../../common/time';
export const useLogPositionState = () => {
const { local: state } = useContext(ReduxStateContext);
const timeKey = logPositionReduxSelectors.selectVisibleMidpointOrTarget(state);
const pages = logPositionReduxSelectors.selectPagesBeforeAndAfter(state);
const isAutoReloading = logPositionReduxSelectors.selectIsAutoReloading(state);
return { timeKey, isAutoReloading, ...pages };
};
export interface LogPositionStateParams {
timeKey: TimeKey | null;
pagesAfterEnd: number | null;
pagesBeforeStart: number | null;
isAutoReloading: boolean;
}
export const logPositionInitialState = {
timeKey: null,
pagesAfterEnd: null,
pagesBeforeStart: null,
isAutoReloading: false,
};
export const LogPositionState = createContainer(useLogPositionState);

View file

@ -4,85 +4,47 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect, useContext, useMemo } from 'react';
import { connect } from 'react-redux';
import { useContext, useMemo } from 'react';
import { StreamItem, LogEntryStreamItem } from '../../components/logging/log_text_stream/item';
import { logEntriesActions, logEntriesSelectors, logPositionSelectors, State } from '../../store';
import { LogEntry, LogEntryHighlight } from '../../utils/log_entry';
import { PropsOfContainer, RendererFunction } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { RendererFunction } from '../../utils/typed_react';
// deep inporting to avoid a circular import problem
import { LogHighlightsState } from './log_highlights/log_highlights';
import { LogPositionState } from './log_position';
import { LogEntriesState, LogEntriesStateParams, LogEntriesCallbacks } from './log_entries';
import { UniqueTimeKey } from '../../../common/time';
export const withStreamItems = connect(
(state: State) => ({
isAutoReloading: logPositionSelectors.selectIsAutoReloading(state),
isReloading: logEntriesSelectors.selectIsReloadingEntries(state),
isLoadingMore: logEntriesSelectors.selectIsLoadingMoreEntries(state),
wasAutoReloadJustAborted: logPositionSelectors.selectAutoReloadJustAborted(state),
hasMoreBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart(state),
hasMoreAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd(state),
lastLoadedTime: logEntriesSelectors.selectEntriesLastLoadedTime(state),
entries: logEntriesSelectors.selectEntries(state),
entriesStart: logEntriesSelectors.selectEntriesStart(state),
entriesEnd: logEntriesSelectors.selectEntriesEnd(state),
}),
bindPlainActionCreators({
loadNewerEntries: logEntriesActions.loadNewerEntries,
reloadEntries: logEntriesActions.reloadEntries,
setSourceId: logEntriesActions.setSourceId,
})
);
type WithStreamItemsProps = PropsOfContainer<typeof withStreamItems>;
export const WithStreamItems = withStreamItems(
({
children,
initializeOnMount,
...props
}: WithStreamItemsProps & {
children: RendererFunction<
WithStreamItemsProps & {
export const WithStreamItems: React.FunctionComponent<{
children: RendererFunction<
LogEntriesStateParams &
LogEntriesCallbacks & {
currentHighlightKey: UniqueTimeKey | null;
items: StreamItem[];
}
>;
initializeOnMount: boolean;
}) => {
const { currentHighlightKey, logEntryHighlightsById } = useContext(LogHighlightsState.Context);
const items = useMemo(
() =>
props.isReloading && !props.isAutoReloading && !props.wasAutoReloadJustAborted
? []
: props.entries.map(logEntry =>
createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || [])
),
>;
}> = ({ children }) => {
const [logEntries, logEntriesCallbacks] = useContext(LogEntriesState.Context);
const { isAutoReloading } = useContext(LogPositionState.Context);
const { currentHighlightKey, logEntryHighlightsById } = useContext(LogHighlightsState.Context);
[
props.isReloading,
props.isAutoReloading,
props.wasAutoReloadJustAborted,
props.entries,
logEntryHighlightsById,
]
);
const items = useMemo(
() =>
logEntries.isReloading && !isAutoReloading
? []
: logEntries.entries.map(logEntry =>
createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || [])
),
useEffect(() => {
if (initializeOnMount && !props.isReloading && !props.isLoadingMore) {
props.reloadEntries();
}
}, []);
[logEntries.entries, logEntryHighlightsById]
);
return children({
...props,
currentHighlightKey,
items,
});
}
);
return children({
...logEntries,
...logEntriesCallbacks,
items,
currentHighlightKey,
});
};
const createLogEntryStreamItem = (
logEntry: LogEntry,
@ -92,23 +54,3 @@ const createLogEntryStreamItem = (
logEntry,
highlights,
});
/**
* This component serves as connection between the state and side-effects
* managed by redux and the state and effects managed by hooks. In particular,
* it forwards changes of the source id to redux via the action creator
* `setSourceId`.
*
* It will be mounted beneath the hierachy level where the redux store and the
* source state are initialized. Once the log entry state and loading
* side-effects have been migrated from redux to hooks it can be removed.
*/
export const ReduxSourceIdBridge = withStreamItems(
({ setSourceId, sourceId }: { setSourceId: (sourceId: string) => void; sourceId: string }) => {
useEffect(() => {
setSourceId(sourceId);
}, [setSourceId, sourceId]);
return null;
}
);

View file

@ -6,7 +6,7 @@
import gql from 'graphql-tag';
import { sharedFragments } from '../../../../../common/graphql/shared';
import { sharedFragments } from '../../common/graphql/shared';
export const logEntriesQuery = gql`
query LogEntries(

View file

@ -24,7 +24,7 @@ import { WithLogMinimapUrlState } from '../../../containers/logs/with_log_minima
import { WithLogPositionUrlState } from '../../../containers/logs/with_log_position';
import { WithLogPosition } from '../../../containers/logs/with_log_position';
import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview';
import { ReduxSourceIdBridge, WithStreamItems } from '../../../containers/logs/with_stream_items';
import { WithStreamItems } from '../../../containers/logs/with_stream_items';
import { Source } from '../../../containers/source';
import { LogsToolbar } from './page_toolbar';
@ -44,10 +44,8 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
} = useContext(LogFlyoutState.Context);
const { logSummaryHighlights } = useContext(LogHighlightsState.Context);
const derivedIndexPattern = createDerivedIndexPattern('logs');
return (
<>
<ReduxSourceIdBridge sourceId={sourceId} />
<LogHighlightsBridge indexPattern={derivedIndexPattern} />
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
@ -87,7 +85,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
scrollUnlockLiveStreaming,
isScrollLocked,
}) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
<WithStreamItems>
{({
currentHighlightKey,
hasMoreAfterEnd,
@ -96,7 +94,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
fetchNewerEntries,
}) => (
<ScrollableLogTextStreamView
columnConfigurations={(source && source.configuration.logColumns) || []}
@ -108,7 +106,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
loadNewerItems={fetchNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
@ -140,7 +138,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
visibleMidpointTime,
visibleTimeInterval,
}) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
<WithStreamItems>
{({ isReloading }) => (
<LogMinimap
height={height}

View file

@ -9,17 +9,54 @@ import React, { useContext } from 'react';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights';
import { LogPositionState } from '../../../containers/logs/log_position';
import { LogFilterState } from '../../../containers/logs/log_filter';
import { LogEntriesState } from '../../../containers/logs/log_entries';
import { Source } from '../../../containers/source';
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const { sourceId, version } = useContext(Source.Context);
const LogEntriesStateProvider: React.FC = ({ children }) => {
const { sourceId } = useContext(Source.Context);
const { timeKey, pagesBeforeStart, pagesAfterEnd, isAutoReloading } = useContext(
LogPositionState.Context
);
const { filterQuery } = useContext(LogFilterState.Context);
const entriesProps = {
timeKey,
pagesBeforeStart,
pagesAfterEnd,
filterQuery,
sourceId,
isAutoReloading,
};
return <LogEntriesState.Provider {...entriesProps}>{children}</LogEntriesState.Provider>;
};
const LogHighlightsStateProvider: React.FC = ({ children }) => {
const { sourceId, version } = useContext(Source.Context);
const [{ entriesStart, entriesEnd }] = useContext(LogEntriesState.Context);
const { filterQuery } = useContext(LogFilterState.Context);
const highlightsProps = {
sourceId,
sourceVersion: version,
entriesStart,
entriesEnd,
filterQuery,
};
return <LogHighlightsState.Provider {...highlightsProps}>{children}</LogHighlightsState.Provider>;
};
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
return (
<LogViewConfiguration.Provider>
<LogFlyout.Provider>
<LogHighlightsState.Provider sourceId={sourceId} sourceVersion={version}>
{children}
</LogHighlightsState.Provider>
<LogPositionState.Provider>
<LogFilterState.Provider>
<LogEntriesStateProvider>
<LogHighlightsStateProvider>{children}</LogHighlightsStateProvider>
</LogEntriesStateProvider>
</LogFilterState.Provider>
</LogPositionState.Provider>
</LogFlyout.Provider>
</LogViewConfiguration.Provider>
);

View file

@ -11,4 +11,3 @@ export {
waffleTimeActions,
waffleOptionsActions,
} from './local';
export { logEntriesActions } from './remote';

View file

@ -7,7 +7,5 @@
import { combineEpics } from 'redux-observable';
import { createLocalEpic } from './local';
import { createRemoteEpic } from './remote';
export const createRootEpic = <State>() =>
combineEpics(createLocalEpic<State>(), createRemoteEpic<State>());
export const createRootEpic = <State>() => combineEpics(createLocalEpic<State>());

View file

@ -6,8 +6,6 @@
import { combineEpics } from 'redux-observable';
import { createLogPositionEpic } from './log_position';
import { createWaffleTimeEpic } from './waffle_time';
export const createLocalEpic = <State>() =>
combineEpics(createLogPositionEpic<State>(), createWaffleTimeEpic<State>());
export const createLocalEpic = <State>() => combineEpics(createWaffleTimeEpic<State>());

View file

@ -8,5 +8,4 @@ import * as logPositionActions from './actions';
import * as logPositionSelectors from './selectors';
export { logPositionActions, logPositionSelectors };
export * from './epic';
export * from './reducer';

View file

@ -17,8 +17,6 @@ import {
unlockAutoReloadScroll,
} from './actions';
import { loadEntriesActionCreators } from '../../remote/log_entries/operations/load';
interface ManualTargetPositionUpdatePolicy {
policy: 'manual';
}
@ -38,9 +36,10 @@ export interface LogPositionState {
startKey: TimeKey | null;
middleKey: TimeKey | null;
endKey: TimeKey | null;
pagesAfterEnd: number;
pagesBeforeStart: number;
};
controlsShouldDisplayTargetPosition: boolean;
autoReloadJustAborted: boolean;
autoReloadScrollLock: boolean;
}
@ -53,9 +52,10 @@ export const initialLogPositionState: LogPositionState = {
endKey: null,
middleKey: null,
startKey: null,
pagesBeforeStart: Infinity,
pagesAfterEnd: Infinity,
},
controlsShouldDisplayTargetPosition: false,
autoReloadJustAborted: false,
autoReloadScrollLock: false,
};
@ -76,11 +76,16 @@ const targetPositionUpdatePolicyReducer = reducerWithInitialState(
const visiblePositionReducer = reducerWithInitialState(
initialLogPositionState.visiblePositions
).case(reportVisiblePositions, (state, { startKey, middleKey, endKey }) => ({
endKey,
middleKey,
startKey,
}));
).case(
reportVisiblePositions,
(state, { startKey, middleKey, endKey, pagesBeforeStart, pagesAfterEnd }) => ({
endKey,
middleKey,
startKey,
pagesBeforeStart,
pagesAfterEnd,
})
);
// Determines whether to use the target position or the visible midpoint when
// displaying a timestamp or time range in the toolbar and log minimap. When the
@ -98,17 +103,6 @@ const controlsShouldDisplayTargetPositionReducer = reducerWithInitialState(
return state;
});
// If auto reload is aborted before a pending request finishes, this flag will
// prevent the UI from displaying the Loading Entries screen
const autoReloadJustAbortedReducer = reducerWithInitialState(
initialLogPositionState.autoReloadJustAborted
)
.case(stopAutoReload, () => true)
.case(startAutoReload, () => false)
.case(loadEntriesActionCreators.resolveDone, () => false)
.case(loadEntriesActionCreators.resolveFailed, () => false)
.case(loadEntriesActionCreators.resolve, () => false);
const autoReloadScrollLockReducer = reducerWithInitialState(
initialLogPositionState.autoReloadScrollLock
)
@ -122,6 +116,5 @@ export const logPositionReducer = combineReducers<LogPositionState>({
updatePolicy: targetPositionUpdatePolicyReducer,
visiblePositions: visiblePositionReducer,
controlsShouldDisplayTargetPosition: controlsShouldDisplayTargetPositionReducer,
autoReloadJustAborted: autoReloadJustAbortedReducer,
autoReloadScrollLock: autoReloadScrollLockReducer,
});

View file

@ -15,8 +15,6 @@ export const selectIsAutoReloading = (state: LogPositionState) =>
export const selectAutoReloadScrollLock = (state: LogPositionState) => state.autoReloadScrollLock;
export const selectAutoReloadJustAborted = (state: LogPositionState) => state.autoReloadJustAborted;
export const selectFirstVisiblePosition = (state: LogPositionState) =>
state.visiblePositions.startKey ? state.visiblePositions.startKey : null;
@ -26,6 +24,13 @@ export const selectMiddleVisiblePosition = (state: LogPositionState) =>
export const selectLastVisiblePosition = (state: LogPositionState) =>
state.visiblePositions.endKey ? state.visiblePositions.endKey : null;
export const selectPagesBeforeAndAfter = (state: LogPositionState) =>
state.visiblePositions
? {
pagesBeforeStart: state.visiblePositions.pagesBeforeStart,
pagesAfterEnd: state.visiblePositions.pagesAfterEnd,
}
: { pagesBeforeStart: null, pagesAfterEnd: null };
export const selectControlsShouldDisplayTargetPosition = (state: LogPositionState) =>
state.controlsShouldDisplayTargetPosition;

View file

@ -7,19 +7,15 @@
import { combineReducers } from 'redux';
import { initialLocalState, localReducer, LocalState } from './local';
import { initialRemoteState, remoteReducer, RemoteState } from './remote';
export interface State {
local: LocalState;
remote: RemoteState;
}
export const initialState: State = {
local: initialLocalState,
remote: initialRemoteState,
};
export const reducer = combineReducers<State>({
local: localReducer,
remote: remoteReducer,
});

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { logEntriesActions } from './log_entries';

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createLogEntriesEpic } from './log_entries';
export const createRemoteEpic = <State>() => createLogEntriesEpic<State>();

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './actions';
export * from './epic';
export * from './reducer';
export * from './selectors';

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import actionCreatorFactory from 'typescript-fsa';
import { loadEntriesActionCreators } from './operations/load';
import { loadMoreEntriesActionCreators } from './operations/load_more';
const actionCreator = actionCreatorFactory('x-pack/infra/remote/log_entries');
export const setSourceId = actionCreator<string>('SET_SOURCE_ID');
export const loadEntries = loadEntriesActionCreators.resolve;
export const loadMoreEntries = loadMoreEntriesActionCreators.resolve;
export const loadNewerEntries = actionCreator('LOAD_NEWER_LOG_ENTRIES');
export const reloadEntries = actionCreator('RELOAD_LOG_ENTRIES');

View file

@ -1,198 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Action } from 'redux';
import { combineEpics, Epic, EpicWithState } from 'redux-observable';
import { merge } from 'rxjs';
import { exhaustMap, filter, map, withLatestFrom } from 'rxjs/operators';
import { logFilterActions, logPositionActions } from '../..';
import { pickTimeKey, TimeKey, timeKeyIsBetween } from '../../../../common/time';
import {
loadEntries,
loadMoreEntries,
loadNewerEntries,
reloadEntries,
setSourceId,
} from './actions';
import { loadEntriesEpic } from './operations/load';
import { loadMoreEntriesEpic } from './operations/load_more';
const LOAD_CHUNK_SIZE = 200;
const DESIRED_BUFFER_PAGES = 2;
interface ManageEntriesDependencies<State> {
selectLogEntriesStart: (state: State) => TimeKey | null;
selectLogEntriesEnd: (state: State) => TimeKey | null;
selectHasMoreLogEntriesBeforeStart: (state: State) => boolean;
selectHasMoreLogEntriesAfterEnd: (state: State) => boolean;
selectIsAutoReloadingLogEntries: (state: State) => boolean;
selectIsLoadingLogEntries: (state: State) => boolean;
selectLogFilterQueryAsJson: (state: State) => string | null;
selectVisibleLogMidpointOrTarget: (state: State) => TimeKey | null;
}
export const createLogEntriesEpic = <State>() =>
combineEpics(
createEntriesEffectsEpic<State>(),
loadEntriesEpic as EpicWithState<typeof loadEntriesEpic, State>,
loadMoreEntriesEpic as EpicWithState<typeof loadEntriesEpic, State>
);
export const createEntriesEffectsEpic = <State>(): Epic<
Action,
Action,
State,
ManageEntriesDependencies<State>
> => (
action$,
state$,
{
selectLogEntriesStart,
selectLogEntriesEnd,
selectHasMoreLogEntriesBeforeStart,
selectHasMoreLogEntriesAfterEnd,
selectIsAutoReloadingLogEntries,
selectIsLoadingLogEntries,
selectLogFilterQueryAsJson,
selectVisibleLogMidpointOrTarget,
}
) => {
const filterQuery$ = state$.pipe(map(selectLogFilterQueryAsJson));
const visibleMidpointOrTarget$ = state$.pipe(
map(selectVisibleLogMidpointOrTarget),
filter(isNotNull),
map(pickTimeKey)
);
const sourceId$ = action$.pipe(
filter(setSourceId.match),
map(({ payload }) => payload)
);
const shouldLoadAroundNewPosition$ = action$.pipe(
filter(logPositionActions.jumpToTargetPosition.match),
withLatestFrom(state$),
filter(([{ payload }, state]) => {
const entriesStart = selectLogEntriesStart(state);
const entriesEnd = selectLogEntriesEnd(state);
return entriesStart && entriesEnd
? !timeKeyIsBetween(entriesStart, entriesEnd, payload)
: true;
}),
map(([{ payload }]) => pickTimeKey(payload))
);
const shouldLoadWithNewFilter$ = action$.pipe(
filter(logFilterActions.applyLogFilterQuery.match),
withLatestFrom(filterQuery$, (filterQuery, filterQueryString) => filterQueryString)
);
const shouldReload$ = merge(action$.pipe(filter(reloadEntries.match)), sourceId$);
const shouldLoadMoreBefore$ = action$.pipe(
filter(logPositionActions.reportVisiblePositions.match),
filter(({ payload: { pagesBeforeStart } }) => pagesBeforeStart < DESIRED_BUFFER_PAGES),
withLatestFrom(state$),
filter(
([action, state]) =>
!selectIsAutoReloadingLogEntries(state) &&
!selectIsLoadingLogEntries(state) &&
selectHasMoreLogEntriesBeforeStart(state)
),
map(([action, state]) => selectLogEntriesStart(state)),
filter(isNotNull),
map(pickTimeKey)
);
const shouldLoadMoreAfter$ = merge(
action$.pipe(
filter(logPositionActions.reportVisiblePositions.match),
filter(({ payload: { pagesAfterEnd } }) => pagesAfterEnd < DESIRED_BUFFER_PAGES),
withLatestFrom(state$, (action, state) => state),
filter(
state =>
!selectIsAutoReloadingLogEntries(state) &&
!selectIsLoadingLogEntries(state) &&
selectHasMoreLogEntriesAfterEnd(state)
)
),
action$.pipe(
filter(loadNewerEntries.match),
withLatestFrom(state$, (action, state) => state)
)
).pipe(
map(state => selectLogEntriesEnd(state)),
filter(isNotNull),
map(pickTimeKey)
);
return merge(
shouldLoadAroundNewPosition$.pipe(
withLatestFrom(filterQuery$, sourceId$),
exhaustMap(([timeKey, filterQuery, sourceId]) => [
loadEntries({
sourceId,
timeKey,
countBefore: LOAD_CHUNK_SIZE,
countAfter: LOAD_CHUNK_SIZE,
filterQuery,
}),
])
),
shouldLoadWithNewFilter$.pipe(
withLatestFrom(visibleMidpointOrTarget$, sourceId$),
exhaustMap(([filterQuery, timeKey, sourceId]) => [
loadEntries({
sourceId,
timeKey,
countBefore: LOAD_CHUNK_SIZE,
countAfter: LOAD_CHUNK_SIZE,
filterQuery,
}),
])
),
shouldReload$.pipe(
withLatestFrom(visibleMidpointOrTarget$, filterQuery$, sourceId$),
exhaustMap(([_, timeKey, filterQuery, sourceId]) => [
loadEntries({
sourceId,
timeKey,
countBefore: LOAD_CHUNK_SIZE,
countAfter: LOAD_CHUNK_SIZE,
filterQuery,
}),
])
),
shouldLoadMoreAfter$.pipe(
withLatestFrom(filterQuery$, sourceId$),
exhaustMap(([timeKey, filterQuery, sourceId]) => [
loadMoreEntries({
sourceId,
timeKey,
countBefore: 0,
countAfter: LOAD_CHUNK_SIZE,
filterQuery,
}),
])
),
shouldLoadMoreBefore$.pipe(
withLatestFrom(filterQuery$, sourceId$),
exhaustMap(([timeKey, filterQuery, sourceId]) => [
loadMoreEntries({
sourceId,
timeKey,
countBefore: LOAD_CHUNK_SIZE,
countAfter: 0,
filterQuery,
}),
])
)
);
};
const isNotNull = <T>(value: T | null): value is T => value !== null;

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as logEntriesActions from './actions';
import * as logEntriesSelectors from './selectors';
export { logEntriesActions, logEntriesSelectors };
export * from './epic';
export * from './reducer';
export { initialLogEntriesState, LogEntriesState } from './state';

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LogEntries as LogEntriesQuery } from '../../../../graphql/types';
import {
createGraphqlOperationActionCreators,
createGraphqlOperationReducer,
createGraphqlQueryEpic,
} from '../../../../utils/remote_state/remote_graphql_state';
import { initialLogEntriesState } from '../state';
import { logEntriesQuery } from './log_entries.gql_query';
const operationKey = 'load';
export const loadEntriesActionCreators = createGraphqlOperationActionCreators<
LogEntriesQuery.Query,
LogEntriesQuery.Variables
>('log_entries', operationKey);
export const loadEntriesReducer = createGraphqlOperationReducer(
operationKey,
initialLogEntriesState,
loadEntriesActionCreators,
(state, action) => action.payload.result.data.source.logEntriesAround,
() => ({
entries: [],
hasMoreAfter: false,
hasMoreBefore: false,
})
);
export const loadEntriesEpic = createGraphqlQueryEpic(logEntriesQuery, loadEntriesActionCreators);

View file

@ -1,72 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LogEntries as LogEntriesQuery } from '../../../../graphql/types';
import {
getLogEntryIndexAfterTime,
getLogEntryIndexBeforeTime,
getLogEntryKey,
} from '../../../../utils/log_entry';
import {
createGraphqlOperationActionCreators,
createGraphqlOperationReducer,
createGraphqlQueryEpic,
} from '../../../../utils/remote_state/remote_graphql_state';
import { initialLogEntriesState } from '../state';
import { logEntriesQuery } from './log_entries.gql_query';
const operationKey = 'load_more';
export const loadMoreEntriesActionCreators = createGraphqlOperationActionCreators<
LogEntriesQuery.Query,
LogEntriesQuery.Variables
>('log_entries', operationKey);
export const loadMoreEntriesReducer = createGraphqlOperationReducer(
operationKey,
initialLogEntriesState,
loadMoreEntriesActionCreators,
(state, action) => {
const logEntriesAround = action.payload.result.data.source.logEntriesAround;
const newEntries = logEntriesAround.entries;
const oldEntries = state && state.entries ? state.entries : [];
const oldStart = state && state.start ? state.start : null;
const oldEnd = state && state.end ? state.end : null;
if (newEntries.length <= 0) {
return state;
}
if ((action.payload.params.countBefore || 0) > 0) {
const lastLogEntry = newEntries[newEntries.length - 1];
const prependAtIndex = getLogEntryIndexAfterTime(oldEntries, getLogEntryKey(lastLogEntry));
return {
start: logEntriesAround.start,
end: oldEnd,
hasMoreBefore: logEntriesAround.hasMoreBefore,
hasMoreAfter: state ? state.hasMoreAfter : logEntriesAround.hasMoreAfter,
entries: [...newEntries, ...oldEntries.slice(prependAtIndex)],
};
} else if ((action.payload.params.countAfter || 0) > 0) {
const firstLogEntry = newEntries[0];
const appendAtIndex = getLogEntryIndexBeforeTime(oldEntries, getLogEntryKey(firstLogEntry));
return {
start: oldStart,
end: logEntriesAround.end,
hasMoreBefore: state ? state.hasMoreBefore : logEntriesAround.hasMoreBefore,
hasMoreAfter: logEntriesAround.hasMoreAfter,
entries: [...oldEntries.slice(0, appendAtIndex), ...newEntries],
};
} else {
return state;
}
}
);
export const loadMoreEntriesEpic = createGraphqlQueryEpic(
logEntriesQuery,
loadMoreEntriesActionCreators
);

View file

@ -1,17 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import reduceReducers from 'reduce-reducers';
import { Reducer } from 'redux';
import { loadEntriesReducer } from './operations/load';
import { loadMoreEntriesReducer } from './operations/load_more';
import { LogEntriesState } from './state';
export const logEntriesReducer = reduceReducers(
loadEntriesReducer,
loadMoreEntriesReducer
) as Reducer<LogEntriesState>;

View file

@ -1,71 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { createSelector } from 'reselect';
import { createGraphqlStateSelectors } from '../../../utils/remote_state/remote_graphql_state';
import { LogEntriesRemoteState } from './state';
const entriesGraphlStateSelectors = createGraphqlStateSelectors<LogEntriesRemoteState>();
export const selectEntries = createSelector(entriesGraphlStateSelectors.selectData, data =>
data ? data.entries : []
);
export const selectIsLoadingEntries = entriesGraphlStateSelectors.selectIsLoading;
export const selectIsReloadingEntries = createSelector(
entriesGraphlStateSelectors.selectIsLoading,
entriesGraphlStateSelectors.selectLoadingProgressOperationInfo,
(isLoading, operationInfo) =>
isLoading && operationInfo ? operationInfo.operationKey === 'load' : false
);
export const selectIsLoadingMoreEntries = createSelector(
entriesGraphlStateSelectors.selectIsLoading,
entriesGraphlStateSelectors.selectLoadingProgressOperationInfo,
(isLoading, operationInfo) =>
isLoading && operationInfo ? operationInfo.operationKey === 'load_more' : false
);
export const selectEntriesStart = createSelector(entriesGraphlStateSelectors.selectData, data =>
data && data.start ? data.start : null
);
export const selectEntriesEnd = createSelector(entriesGraphlStateSelectors.selectData, data =>
data && data.end ? data.end : null
);
export const selectHasMoreBeforeStart = createSelector(
entriesGraphlStateSelectors.selectData,
data => (data ? data.hasMoreBefore : true)
);
export const selectHasMoreAfterEnd = createSelector(entriesGraphlStateSelectors.selectData, data =>
data ? data.hasMoreAfter : true
);
export const selectEntriesLastLoadedTime = entriesGraphlStateSelectors.selectLoadingResultTime;
export const selectEntriesStartLoadingState = entriesGraphlStateSelectors.selectLoadingState;
export const selectEntriesEndLoadingState = entriesGraphlStateSelectors.selectLoadingState;
export const selectFirstEntry = createSelector(selectEntries, entries =>
entries.length > 0 ? entries[0] : null
);
export const selectLastEntry = createSelector(selectEntries, entries =>
entries.length > 0 ? entries[entries.length - 1] : null
);
export const selectLoadedEntriesTimeInterval = createSelector(
entriesGraphlStateSelectors.selectData,
data => ({
end: data && data.end ? data.end.time : null,
start: data && data.start ? data.start.time : null,
})
);

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LogEntries as LogEntriesQuery } from '../../../graphql/types';
import {
createGraphqlInitialState,
GraphqlState,
} from '../../../utils/remote_state/remote_graphql_state';
export type LogEntriesRemoteState = LogEntriesQuery.LogEntriesAround;
export type LogEntriesState = GraphqlState<LogEntriesRemoteState>;
export const initialLogEntriesState = createGraphqlInitialState<LogEntriesRemoteState>();

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { combineReducers } from 'redux';
import { initialLogEntriesState, logEntriesReducer, LogEntriesState } from './log_entries';
export interface RemoteState {
logEntries: LogEntriesState;
}
export const initialRemoteState = {
logEntries: initialLogEntriesState,
};
export const remoteReducer = combineReducers<RemoteState>({
logEntries: logEntriesReducer,
});

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { globalizeSelectors } from '../../utils/typed_redux';
import { logEntriesSelectors as innerLogEntriesSelectors } from './log_entries';
import { RemoteState } from './reducer';
export const logEntriesSelectors = globalizeSelectors(
(state: RemoteState) => state.logEntries,
innerLogEntriesSelectors
);

View file

@ -4,9 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createSelector } from 'reselect';
import { getLogEntryAtTime } from '../utils/log_entry';
import { globalizeSelectors } from '../utils/typed_redux';
import {
logFilterSelectors as localLogFilterSelectors,
@ -16,8 +13,6 @@ import {
waffleTimeSelectors as localWaffleTimeSelectors,
} from './local';
import { State } from './reducer';
import { logEntriesSelectors as remoteLogEntriesSelectors } from './remote';
/**
* local selectors
*/
@ -29,36 +24,3 @@ export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPosi
export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors);
export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors);
export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors);
/**
* remote selectors
*/
const selectRemote = (state: State) => state.remote;
export const logEntriesSelectors = globalizeSelectors(selectRemote, remoteLogEntriesSelectors);
/**
* shared selectors
*/
export const sharedSelectors = {
selectFirstVisibleLogEntry: createSelector(
logEntriesSelectors.selectEntries,
logPositionSelectors.selectFirstVisiblePosition,
(entries, firstVisiblePosition) =>
firstVisiblePosition ? getLogEntryAtTime(entries, firstVisiblePosition) : null
),
selectMiddleVisibleLogEntry: createSelector(
logEntriesSelectors.selectEntries,
logPositionSelectors.selectMiddleVisiblePosition,
(entries, middleVisiblePosition) =>
middleVisiblePosition ? getLogEntryAtTime(entries, middleVisiblePosition) : null
),
selectLastVisibleLogEntry: createSelector(
logEntriesSelectors.selectEntries,
logPositionSelectors.selectLastVisiblePosition,
(entries, lastVisiblePosition) =>
lastVisiblePosition ? getLogEntryAtTime(entries, lastVisiblePosition) : null
),
};

View file

@ -12,7 +12,6 @@ import { map } from 'rxjs/operators';
import {
createRootEpic,
initialState,
logEntriesSelectors,
logFilterSelectors,
logPositionSelectors,
reducer,
@ -38,11 +37,6 @@ export function createStore({ apolloClient, observableApi }: StoreDependencies)
const middlewareDependencies = {
postToApi$: observableApi.pipe(map(({ post }) => post)),
apolloClient$: apolloClient,
selectIsLoadingLogEntries: logEntriesSelectors.selectIsLoadingEntries,
selectLogEntriesEnd: logEntriesSelectors.selectEntriesEnd,
selectLogEntriesStart: logEntriesSelectors.selectEntriesStart,
selectHasMoreLogEntriesAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd,
selectHasMoreLogEntriesBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart,
selectIsAutoReloadingLogEntries: logPositionSelectors.selectIsAutoReloading,
selectIsAutoReloadingScrollLocked: logPositionSelectors.selectAutoReloadScrollLock,
selectLogFilterQueryAsJson: logFilterSelectors.selectLogFilterQueryAsJson,

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import React, { createContext } from 'react';
import { State, initialState } from '../store';
export const ReduxStateContext = createContext(initialState);
const withRedux = connect((state: State) => state);
export const ReduxStateContextProvider = withRedux(({ children, ...state }) => {
return <ReduxStateContext.Provider value={state as State}>{children}</ReduxStateContext.Provider>;
});

View file

@ -1,214 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ApolloError, ApolloQueryResult } from 'apollo-client';
import { DocumentNode } from 'graphql';
import { Action as ReduxAction } from 'redux';
import { Epic } from 'redux-observable';
import { from, Observable } from 'rxjs';
import { catchError, filter, map, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
import { Action, ActionCreator, actionCreatorFactory, Failure, Success } from 'typescript-fsa';
import { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
import { createSelector } from 'reselect';
import { InfraApolloClient } from '../../lib/lib';
import {
isFailureLoadingResult,
isIdleLoadingProgress,
isRunningLoadingProgress,
isSuccessLoadingResult,
isUninitializedLoadingResult,
LoadingPolicy,
LoadingProgress,
LoadingResult,
} from '../loading_state';
export interface GraphqlState<State> {
current: LoadingProgress<OperationInfo<any>>;
last: LoadingResult<OperationInfo<any>>;
data: State | undefined;
}
interface OperationInfo<Variables> {
operationKey: string;
variables: Variables;
}
type ResolveDonePayload<Variables, Data> = Success<Variables, ApolloQueryResult<Data>>;
type ResolveFailedPayload<Variables, Error> = Failure<Variables, Error>;
interface OperationActionCreators<Data, Variables, Error = ApolloError> {
resolve: ActionCreator<Variables>;
resolveStarted: ActionCreator<Variables>;
resolveDone: ActionCreator<ResolveDonePayload<Variables, Data>>;
resolveFailed: ActionCreator<ResolveFailedPayload<Variables, Error>>;
}
export const createGraphqlInitialState = <State>(initialData?: State): GraphqlState<State> => ({
current: {
progress: 'idle',
},
last: {
result: 'uninitialized',
},
data: initialData,
});
export const createGraphqlOperationActionCreators = <Data, Variables, Error = ApolloError>(
stateKey: string,
operationKey: string
): OperationActionCreators<Data, Variables, Error> => {
const actionCreator = actionCreatorFactory(`x-pack/infra/remote/${stateKey}/${operationKey}`);
const resolve = actionCreator<Variables>('RESOLVE');
const resolveEffect = actionCreator.async<Variables, ApolloQueryResult<Data>>('RESOLVE');
return {
resolve,
resolveStarted: resolveEffect.started,
resolveDone: resolveEffect.done,
resolveFailed: resolveEffect.failed,
};
};
export const createGraphqlOperationReducer = <State, Data, Variables, Error = ApolloError>(
operationKey: string,
initialState: GraphqlState<State>,
actionCreators: OperationActionCreators<Data, Variables, Error>,
reduceSuccess: (
state: State | undefined,
action: Action<ResolveDonePayload<Variables, Data>>
) => State | undefined = state => state,
reduceFailure: (
state: State | undefined,
action: Action<ResolveFailedPayload<Variables, Error>>
) => State | undefined = state => state
) =>
reducerWithInitialState(initialState)
.caseWithAction(actionCreators.resolveStarted, (state, action) => ({
...state,
current: {
progress: 'running',
time: Date.now(),
parameters: {
operationKey,
variables: action.payload,
},
},
}))
.caseWithAction(actionCreators.resolveDone, (state, action) => ({
...state,
current: {
progress: 'idle',
},
last: {
result: 'success',
parameters: {
operationKey,
variables: action.payload.params,
},
time: Date.now(),
isExhausted: false,
},
data: reduceSuccess(state.data, action),
}))
.caseWithAction(actionCreators.resolveFailed, (state, action) => ({
...state,
current: {
progress: 'idle',
},
last: {
result: 'failure',
reason: `${action.payload}`,
time: Date.now(),
parameters: {
operationKey,
variables: action.payload.params,
},
},
data: reduceFailure(state.data, action),
}))
.build();
export const createGraphqlQueryEpic = <Data, Variables, Error = ApolloError>(
graphqlQuery: DocumentNode,
actionCreators: OperationActionCreators<Data, Variables, Error>
): Epic<
ReduxAction,
ReduxAction,
any,
{
apolloClient$: Observable<InfraApolloClient>;
}
> => (action$, state$, { apolloClient$ }) =>
action$.pipe(
filter(actionCreators.resolve.match),
withLatestFrom(apolloClient$),
switchMap(([{ payload: variables }, apolloClient]) =>
from(
apolloClient.query<Data>({
query: graphqlQuery,
variables,
fetchPolicy: 'no-cache',
})
).pipe(
map(result => actionCreators.resolveDone({ params: variables, result })),
catchError(error => [actionCreators.resolveFailed({ params: variables, error })]),
startWith(actionCreators.resolveStarted(variables))
)
)
);
export const createGraphqlStateSelectors = <State>(
selectState: (parentState: any) => GraphqlState<State> = parentState => parentState
) => {
const selectData = createSelector(selectState, state => state.data);
const selectLoadingProgress = createSelector(selectState, state => state.current);
const selectLoadingProgressOperationInfo = createSelector(selectLoadingProgress, progress =>
isRunningLoadingProgress(progress) ? progress.parameters : null
);
const selectIsLoading = createSelector(selectLoadingProgress, isRunningLoadingProgress);
const selectIsIdle = createSelector(selectLoadingProgress, isIdleLoadingProgress);
const selectLoadingResult = createSelector(selectState, state => state.last);
const selectLoadingResultOperationInfo = createSelector(selectLoadingResult, result =>
!isUninitializedLoadingResult(result) ? result.parameters : null
);
const selectLoadingResultTime = createSelector(selectLoadingResult, result =>
!isUninitializedLoadingResult(result) ? result.time : null
);
const selectIsUninitialized = createSelector(selectLoadingResult, isUninitializedLoadingResult);
const selectIsSuccess = createSelector(selectLoadingResult, isSuccessLoadingResult);
const selectIsFailure = createSelector(selectLoadingResult, isFailureLoadingResult);
const selectLoadingState = createSelector(
selectLoadingProgress,
selectLoadingResult,
(loadingProgress, loadingResult) => ({
current: loadingProgress,
last: loadingResult,
policy: {
policy: 'manual',
} as LoadingPolicy,
})
);
return {
selectData,
selectIsFailure,
selectIsIdle,
selectIsLoading,
selectIsSuccess,
selectIsUninitialized,
selectLoadingProgress,
selectLoadingProgressOperationInfo,
selectLoadingResult,
selectLoadingResultOperationInfo,
selectLoadingResultTime,
selectLoadingState,
};
};