Goals:
- Understand and document all state for the log stream.
- Split or group the state handling in units that accommodate the current code.
- Establish a state management convention and use it everywhere in the log stream.
- Potentially, reuse the same state management convention in other places of the infra app.
Considerations:
Log stream state
A container is a constate instance that holds state for a particular slice of the log stream app. The information that can be hold in state can be of one of the following types:
- setting: A user-chosen value persisted between sessions.
- data: some value to present in the UI. Might be user input, network response, etc.
- UI: particular state about the interface. i.e.: an element is visible, a scroll is in a particular position.
- network: some async state is happening that may or may not be represented in the UI.
isLoading is network. isSpinnerVisible is UI.
- other kitchen sink category. Could be internal state that needs to be exposed, could be a memoized value, or state composed from other piece of state
All containers live in plugins/infra/public/containers/logs.
This document will only show information-holding state, not their setters. Unless specified, you (the reader) can assume there is a setter for each entry. (i.e. textScale has a setTextScale function exposed as well).
LogSource
|
export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource); |
Contains state about the current log source configuration
Takes:
sourceId The ID of the source
fetch fetcher function
Exposes:
sourceId (data) The ID of the source
derivedIndexPattern (data) - ???
isLoading (network)
hasFailedLoadingSource (network)
isLoadingSourceConfiguration (network)
sourceConfiguration (data)
hasFailedLoadingSourceStatus (network)
sourceStatus (data)
isLoadingSourceStatus (network)
isUninitialized (other)
loadSourceFailureMessage (data)
logIndicesExist (other)
LogViewConfiguration
|
export const LogViewConfiguration = createContainer(useLogViewConfiguration); |
Provides state for the view settings.
Takes:
Exposes:
textScale (setting) Size of the text: small, medium, large
textWrap (setting) should log lines wrap or not
LogFlyout
|
export const LogFlyout = createContainer(useLogFlyout); |
Provides state for the view details flyout
Takes:
Exposes:
flyoutVisible (UI): is the flyout visible
isLoading (network): the flyoutItem is being fetched or not.
flyoutId (data): ID of the log entry to show in the flyout.
surroundingLogsId (data): same ID. When set, the log stream highlights the relevant log line.
flyoutItem (data): fields for the log entry with the id flyoutId.
LogPositionState
|
export const LogPositionState = createContainer(useLogPositionState); |
Provides information about the visible range of log entries and their position.
Takes:
Exposes:
isInitialized (other): the hook has been initialized or not. If it's not, its state cannot be fully used yet.
targetPosition (data): Which log line should be centered in the UI
isStreaming (UI): Live stream is active or not.
firstVisiblePosition (UI) which log line is the first visible in the UI.
pagesBeforeStart (UI) how many scroll pages until the user reaches the top.
pagesAfterEnd (UI) how many scroll pages until the user reaches the bottom.
visibleMiddlePoint (UI) which log line is actually centered.
visibleMiddlePointTime (other), timestamp of the visibleMiddlePoint
visibleTimeInterval (UI) range between the time of the firstVisiblePosition and the last visible position (not exposed as state).
startDateExpression (data) beginning of date range (i.e now-1d)
endDateExpression (data) end of date range (i.e. now)
startTimestamp (other) resolved value for startDateExpression
endTimestamp (other) resolved value for endDateExpression
timestampsLastUpdate (other) when was the last time that the *DateExpressions were evaluated
ViewLogInContext
|
export const ViewLogInContext = createContainer(useViewLogInContext); |
Holds state for the "View in context" modal
Takes:
sourceId from LogSource
startTimestamp from LogPositionState
endTimestamp from LogPositionState
Exposes:
contextEntry (data) the log entry whose context we need to fetch.
entries (data) log entries around contextEntry
isLoading (network) are the entries being fetched
LogFilterState
|
export const LogFilterState = createContainer(useLogFilterState); |
Contains the user query in the search box.
Takes:
indexPattern from LogSource.derivedIndexPattern
Exposes:
filterQueryDraft (data) kuery expression the user has typed. Not yet commited.
filterQueryAsKuery (data) kuery expression the user has typed. Commited
filterQuery (other) serialized filterQueryAsKuery
isFilterQueryDraftValid (other) is the draft a valid kuery.
LogEntriesState
|
export const LogEntriesState = createContainer(useLogEntriesState); |
The log entries themselves
Takes:
startTimestamp from LogPositionState
endTimestamp from LogPositionState
timestampsLastUpdate from LogPositionState
filterQuery from LogFilterState
timeKey from LogPositionState.targetPosition
pagesBeforeStart from LogPositionState
pagesAfterEnd from LogPositionState
sourceId from LogSource
isStreaming from LogPositionState
jumpToTargetPosition, callback from LogPositionState
Exposes:
entries (data) the log entries
topCursor (other) first loaded entry. Used for pagination
bottomCursor (other) last loaded entry. Used for pagination
centerCursor (other) middle point of all loaded entries
isReloading (network|_UI_) has the entries array being reset
isLoadingMore (network|_UI_) is fetching a new page.
lastLoadedTime (other) when did the last fetch happen
hasMoreBeforeStart (data) are there more entries before topCursor
hasMoreAfterEnd (data) are there more entries after bottomCursor
LogHighlightsState
|
export const LogHighlightsState = createContainer(useLogHighlightsState); |
Holds state relevant to the highlight functionality.
Takes:
sourceId from LogSource
sourceVersion from LogSource.sourceConfiguration.version
entriesStart from LogEntriesState.topCursor
entriesEnd from LogEntriesState.bottomCursor
centerCursor from LogEntriesState.centerCursor
size from LogEntriesState.entries.length
filterQuery from LogFilter
Exposes:
highlightTerms (data) list of terms to be highlighted
logEntryHighlights (data) list of log entries with highlighted terms
logEntryHighlightsById (other) index of logEntryHighlights
logSummaryHighlights (data) list of date ranges with highlights on them, for the minimap
currentHighlightKey (UI) cursor of the log entry with the active highlight
hasPreviousHighlight (UI) are there any entries with highlights before the currentHighlightKey
hasNextHighlight (UI) are there any entries with highlights after the currentHighlightKey
Recommendations
reactive vs explicit data fetching
LogEntriesState triggers different data fetching based on changes on its props. These props include UI state, like scroll positions.
My advice is to change this. Instead of triggering a fetch as a reaction to input, it should expose callbacks that another component can use to trigger the fetch. This trigger can be made as a reaction to an input (i.e. a change in scroll position).
Evaluate if UI state should be held in containers at all. Since the state is only relevant to the UI, it should be moved to the UI components themselves.
For example, LogPositionState holds state about the user date range and the scroll position of the Log UI. The second category can be extracted to a view component instead of held in a container. The view component then can be responsible of passing it to LogEntriesState.
Unify how network state is represented. The possible states for any given request is always the same. We can use useTrackedPromise or a similar hook everywhere.
Revisit naming. Some concepts in the app have evolved or we understand better their functions. The names should reflect this.
LogFlyout => LogDetails: The fact that it uses a flyout is a UI decision and it should not be reflected in the container name.
position, TimeKey, cursor all represent the same idea. Pick one name
- ...
Establish a convention for filenames and exports. A match between the main export and the file name is preferred. This makes it easy to find where the code is
log_flyout.tsx exports LogFlyout. This is good
log_entries/index.tsx exports LogEntriesState. We should either move the export to log_entries_state.tsx or rename the export to LogEntries.
Establish a convention to pass information between containers. Some containers get the information with useContext, whereas others pass the information through arguments.
Establish a convention for the internal container structure.
This has two goals:
- When a programmer is working in the log streaming UI, they don't need to hold in their heads the intricacies of each particular container and can focus on the business logic.
- When a new container needs to be created, the pattern to follow can be copied from an existing one.
The most common pattern seems to be the following:
/* container_name.tsx */
import createContainer from 'constate';
import "...";
interface ContainerNameProps {
// Parameters to pass to the container
}
interface ContainerNameState {
// public data
}
interface ContainerNameCallbacks {
// public API of the container
}
function useContainerName(props: ContainerNameProps): ContainerNameState & ContainerNameCallbacks {
// ...
return { ... };
}
export const ContainerName = createContainer(useContainerName);
Goals:
Considerations:
Log stream state
A
containeris a constate instance that holds state for a particular slice of the log stream app. The information that can be hold in state can be of one of the following types:isLoadingis network.isSpinnerVisibleis UI.All containers live in
plugins/infra/public/containers/logs.This document will only show information-holding state, not their setters. Unless specified, you (the reader) can assume there is a setter for each entry. (i.e.
textScalehas asetTextScalefunction exposed as well).LogSourcekibana/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts
Line 169 in 5bcf2c8
Contains state about the current log source configuration
Takes:
sourceIdThe ID of the sourcefetchfetcher functionExposes:
sourceId(data) The ID of the sourcederivedIndexPattern(data) - ???isLoading(network)hasFailedLoadingSource(network)isLoadingSourceConfiguration(network)sourceConfiguration(data)hasFailedLoadingSourceStatus(network)sourceStatus(data)isLoadingSourceStatus(network)isUninitialized(other)loadSourceFailureMessage(data)logIndicesExist(other)LogViewConfigurationkibana/x-pack/plugins/infra/public/containers/logs/log_view_configuration.tsx
Line 28 in fe4c164
Provides state for the view settings.
Takes:
Exposes:
textScale(setting) Size of the text: small, medium, largetextWrap(setting) should log lines wrap or notLogFlyoutkibana/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx
Line 76 in 2fba7ed
Provides state for the view details flyout
Takes:
Exposes:
flyoutVisible(UI): is the flyout visibleisLoading(network): theflyoutItemis being fetched or not.flyoutId(data): ID of the log entry to show in the flyout.surroundingLogsId(data): same ID. When set, the log stream highlights the relevant log line.flyoutItem(data): fields for the log entry with the idflyoutId.LogPositionStatekibana/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts
Line 208 in fe4c164
Provides information about the visible range of log entries and their position.
Takes:
Exposes:
isInitialized(other): the hook has been initialized or not. If it's not, its state cannot be fully used yet.targetPosition(data): Which log line should be centered in the UIisStreaming(UI): Live stream is active or not.firstVisiblePosition(UI) which log line is the first visible in the UI.pagesBeforeStart(UI) how many scroll pages until the user reaches the top.pagesAfterEnd(UI) how many scroll pages until the user reaches the bottom.visibleMiddlePoint(UI) which log line is actually centered.visibleMiddlePointTime(other), timestamp of thevisibleMiddlePointvisibleTimeInterval(UI) range between the time of thefirstVisiblePositionand the last visible position (not exposed as state).startDateExpression(data) beginning of date range (i.enow-1d)endDateExpression(data) end of date range (i.e.now)startTimestamp(other) resolved value forstartDateExpressionendTimestamp(other) resolved value forendDateExpressiontimestampsLastUpdate(other) when was the last time that the*DateExpressions were evaluatedViewLogInContextkibana/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts
Line 83 in 53b33bb
Holds state for the "View in context" modal
Takes:
sourceIdfromLogSourcestartTimestampfromLogPositionStateendTimestampfromLogPositionStateExposes:
contextEntry(data) the log entry whose context we need to fetch.entries(data) log entries aroundcontextEntryisLoading(network) are the entries being fetchedLogFilterStatekibana/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts
Line 101 in 0a6c748
Contains the user query in the search box.
Takes:
indexPatternfromLogSource.derivedIndexPatternExposes:
filterQueryDraft(data) kuery expression the user has typed. Not yet commited.filterQueryAsKuery(data) kuery expression the user has typed. CommitedfilterQuery(other) serializedfilterQueryAsKueryisFilterQueryDraftValid(other) is the draft a valid kuery.LogEntriesStatekibana/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts
Line 433 in fe4c164
The log entries themselves
Takes:
startTimestampfromLogPositionStateendTimestampfromLogPositionStatetimestampsLastUpdatefromLogPositionStatefilterQueryfromLogFilterStatetimeKeyfromLogPositionState.targetPositionpagesBeforeStartfromLogPositionStatepagesAfterEndfromLogPositionStatesourceIdfromLogSourceisStreamingfromLogPositionStatejumpToTargetPosition, callback fromLogPositionStateExposes:
entries(data) the log entriestopCursor(other) first loaded entry. Used for paginationbottomCursor(other) last loaded entry. Used for paginationcenterCursor(other) middle point of all loaded entriesisReloading(network|_UI_) has theentriesarray being resetisLoadingMore(network|_UI_) is fetching a new page.lastLoadedTime(other) when did the last fetch happenhasMoreBeforeStart(data) are there more entries beforetopCursorhasMoreAfterEnd(data) are there more entries afterbottomCursorLogHighlightsState
export const LogHighlightsState = createContainer(useLogHighlightsState);
kibana/x-pack/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx
Line 94 in fe4c164
Holds state relevant to the highlight functionality.
Takes:
sourceIdfromLogSourcesourceVersionfromLogSource.sourceConfiguration.versionentriesStartfromLogEntriesState.topCursorentriesEndfromLogEntriesState.bottomCursorcenterCursorfromLogEntriesState.centerCursorsizefromLogEntriesState.entries.lengthfilterQueryfromLogFilterExposes:
highlightTerms(data) list of terms to be highlightedlogEntryHighlights(data) list of log entries with highlighted termslogEntryHighlightsById(other) index oflogEntryHighlightslogSummaryHighlights(data) list of date ranges with highlights on them, for the minimapcurrentHighlightKey(UI) cursor of the log entry with the active highlighthasPreviousHighlight(UI) are there any entries with highlights before thecurrentHighlightKeyhasNextHighlight(UI) are there any entries with highlights after thecurrentHighlightKeyRecommendations
reactive vs explicit data fetching
LogEntriesStatetriggers different data fetching based on changes on itsprops. These props include UI state, like scroll positions.My advice is to change this. Instead of triggering a fetch as a reaction to input, it should expose callbacks that another component can use to trigger the fetch. This trigger can be made as a reaction to an input (i.e. a change in scroll position).
Evaluate if UI state should be held in containers at all. Since the state is only relevant to the UI, it should be moved to the UI components themselves.
For example,
LogPositionStateholds state about the user date range and the scroll position of the Log UI. The second category can be extracted to a view component instead of held in a container. The view component then can be responsible of passing it toLogEntriesState.Unify how network state is represented. The possible states for any given request is always the same. We can use
useTrackedPromiseor a similar hook everywhere.Revisit naming. Some concepts in the app have evolved or we understand better their functions. The names should reflect this.
LogFlyout=>LogDetails: The fact that it uses a flyout is a UI decision and it should not be reflected in the container name.position,TimeKey,cursorall represent the same idea. Pick one nameEstablish a convention for filenames and exports. A match between the main export and the file name is preferred. This makes it easy to find where the code is
log_flyout.tsxexportsLogFlyout. This is goodlog_entries/index.tsxexportsLogEntriesState. We should either move the export tolog_entries_state.tsxor rename the export toLogEntries.Establish a convention to pass information between containers. Some containers get the information with
useContext, whereas others pass the information through arguments.Establish a convention for the internal container structure.
This has two goals:
The most common pattern seems to be the following: