[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:
parent
d429a9a1e8
commit
21f9ab255a
|
@ -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>
|
||||
|
|
|
@ -18,7 +18,7 @@ interface LogTextStreamLoadingItemViewProps {
|
|||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
lastStreamingUpdate: number | null;
|
||||
lastStreamingUpdate: Date | null;
|
||||
onLoadMore?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -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%"
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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);
|
|
@ -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[];
|
||||
}
|
|
@ -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);
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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(
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -11,4 +11,3 @@ export {
|
|||
waffleTimeActions,
|
||||
waffleOptionsActions,
|
||||
} from './local';
|
||||
export { logEntriesActions } from './remote';
|
||||
|
|
|
@ -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>());
|
||||
|
|
|
@ -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>());
|
||||
|
|
|
@ -8,5 +8,4 @@ import * as logPositionActions from './actions';
|
|||
import * as logPositionSelectors from './selectors';
|
||||
|
||||
export { logPositionActions, logPositionSelectors };
|
||||
export * from './epic';
|
||||
export * from './reducer';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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';
|
|
@ -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>();
|
|
@ -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';
|
|
@ -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');
|
|
@ -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;
|
|
@ -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';
|
|
@ -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);
|
|
@ -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
|
||||
);
|
|
@ -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>;
|
|
@ -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,
|
||||
})
|
||||
);
|
|
@ -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>();
|
|
@ -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,
|
||||
});
|
|
@ -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
|
||||
);
|
|
@ -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
|
||||
),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
16
x-pack/legacy/plugins/infra/public/utils/redux_context.tsx
Normal file
16
x-pack/legacy/plugins/infra/public/utils/redux_context.tsx
Normal 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>;
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue