Skip to content

Commit

Permalink
[Logs UI] Show highlighted log entries in the minimap (elastic#40745) (
Browse files Browse the repository at this point in the history
…elastic#43031)

* Commit to allow opening the PR

* Add start of the highlights menu

* Add terms input

* Add a container for highlights state

* Start to hook up highlights state effects, and Bridge with Redux

* Move highlighted logs to a separate graphql field

* Add highlights argument to unit tests

* Add initial draft of a log highlights hook

* Finish hooking up hook state ready for GraphQL query

* Take startKey and endKey from overall collection of log entries

* Add sourceVersion

* Fetch the data

* Use columns ids instead of column indices

* merge highlights into stream WIP

* Add highlighting for the message column type

* Use the serialized filter query

* UX / Performance improvements

* Show a highlight is active
* Focus input when menu is opened
* Icon for highlights menu
* Debounce onChange handler
* Loading state for highlights menu
* Neaten up Provider

* Tweaks

* Breathing room for icon
* Better debounce time for typing

* Menu changes

* Ensure width of popover stays consistent
* Add a button to clear highlights

* Add highlighting for field columns

* Use phrase query for highlighting

While a `phrase_prefix` query feels more natural, it doesn't work on
`keyword` type fields. Using just a `phrase` query keeps the
highlighting consistent between text and keyword type fields.

* Factor out commonly used visibility state

* Fix debounce to support changing onChange prop

* Move clear highlight button inline

* Translate labels

* Rely on popover ownfocus prop to focus the input

* Rename function to avoid i18n checker false positive

* Rough and ready WIP next / previous

* Add `countBefore` and `countAfter` highlight args

* Fix value used

* Add a check for entry

* Rearrange icons

* 0 falsey blurgh

* Organise into separate sub-hooks for clarity

* Add comment

* Use filter and highlight queries also beyond interval

* Refine behaviour of next / previous

* Stops wrapping
* Ensures that when scrolling next / previous takes off where it was left
* Ensures the first highlight is scrolled to when changing highlight term

* Add log highlights api tests

* Fix a few runtime and compiler errors and linter warnings

* Refactor prev/next jumping to avoid index errors

* Use nearest index for initial jump

* Include element type in translation id

* Disable clear button when nothing to clear

* Initial summary highlights state

* Amend component usage

* Comment out load call until query exists

* Add graphql api for summary highlights

* Add the client-side log summary highlight query

* Directly use log view configuration without a bridge

* Rename log entry highlight hook to maintain symmetry

* Re-enable highlight rendering in the minimap

* Keep the time cursor from intercepting clicks

* tweak minimap highlighting colors and interaction

* Clean up after merge

* Remove commented-out code

* Change cursor to hint at minimap highlighting interaction

* Restrict highlight query filter to relevant fields

* Add explanatory error message
  • Loading branch information
Kerry350 authored Aug 9, 2019
1 parent 4d0200d commit 4f24b7e
Show file tree
Hide file tree
Showing 27 changed files with 1,015 additions and 170 deletions.
87 changes: 87 additions & 0 deletions x-pack/legacy/plugins/infra/common/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface InfraSource {
logEntryHighlights: InfraLogEntryInterval[];
/** A consecutive span of summary buckets within an interval */
logSummaryBetween: InfraLogSummaryInterval;
/** Spans of summary highlight buckets within an interval */
logSummaryHighlightsBetween: InfraLogSummaryHighlightInterval[];

logItem: InfraLogItem;
/** A snapshot of nodes */
Expand Down Expand Up @@ -224,6 +226,30 @@ export interface InfraLogSummaryBucket {
/** The number of entries inside the bucket */
entriesCount: number;
}
/** A consecutive sequence of log summary highlight buckets */
export interface InfraLogSummaryHighlightInterval {
/** The millisecond timestamp corresponding to the start of the interval covered by the summary */
start?: number | null;
/** The millisecond timestamp corresponding to the end of the interval covered by the summary */
end?: number | null;
/** The query the log entries were filtered by */
filterQuery?: string | null;
/** The query the log entries were highlighted with */
highlightQuery?: string | null;
/** A list of the log entries */
buckets: InfraLogSummaryHighlightBucket[];
}
/** A log summary highlight bucket */
export interface InfraLogSummaryHighlightBucket {
/** The start timestamp of the bucket */
start: number;
/** The end timestamp of the bucket */
end: number;
/** The number of highlighted entries inside the bucket */
entriesCount: number;
/** The time key of a representative of the highlighted log entries in this bucket */
representativeKey: InfraTimeKey;
}

export interface InfraLogItem {
/** The ID of the document */
Expand Down Expand Up @@ -446,6 +472,18 @@ export interface LogSummaryBetweenInfraSourceArgs {
/** The query to filter the log entries by */
filterQuery?: string | null;
}
export interface LogSummaryHighlightsBetweenInfraSourceArgs {
/** The millisecond timestamp that corresponds to the start of the interval */
start: number;
/** The millisecond timestamp that corresponds to the end of the interval */
end: number;
/** The size of each bucket in milliseconds */
bucketSize: number;
/** The query to filter the log entries by */
filterQuery?: string | null;
/** The highlighting to apply to the log entries */
highlightQueries: string[];
}
export interface LogItemInfraSourceArgs {
id: string;
}
Expand Down Expand Up @@ -657,6 +695,55 @@ export namespace LogEntryHighlightsQuery {
export type Entries = InfraLogEntryHighlightFields.Fragment;
}

export namespace LogSummaryHighlightsQuery {
export type Variables = {
sourceId?: string | null;
start: number;
end: number;
bucketSize: number;
highlightQueries: string[];
filterQuery?: string | null;
};

export type Query = {
__typename?: 'Query';

source: Source;
};

export type Source = {
__typename?: 'InfraSource';

id: string;

logSummaryHighlightsBetween: LogSummaryHighlightsBetween[];
};

export type LogSummaryHighlightsBetween = {
__typename?: 'InfraLogSummaryHighlightInterval';

start?: number | null;

end?: number | null;

buckets: Buckets[];
};

export type Buckets = {
__typename?: 'InfraLogSummaryHighlightBucket';

start: number;

end: number;

entriesCount: number;

representativeKey: RepresentativeKey;
};

export type RepresentativeKey = InfraTimeKeyFields.Fragment;
}

export namespace LogSummary {
export type Variables = {
sourceId?: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import * as React from 'react';

import euiStyled from '../../../../../../common/eui_styled_components';
import { LogEntryTime } from '../../../../common/log_entry';
// import { SearchSummaryBucket } from '../../../../common/log_search_summary';
import { DensityChart } from './density_chart';
import { HighlightedInterval } from './highlighted_interval';
// import { SearchMarkers } from './search_markers';
import { SearchMarkers } from './search_markers';
import { TimeRuler } from './time_ruler';
import { SummaryBucket } from './types';
import { SummaryBucket, SummaryHighlightBucket } from './types';

interface LogMinimapProps {
className?: string;
Expand All @@ -26,7 +25,7 @@ interface LogMinimapProps {
jumpToTarget: (params: LogEntryTime) => any;
intervalSize: number;
summaryBuckets: SummaryBucket[];
// searchSummaryBuckets?: SearchSummaryBucket[];
summaryHighlightBuckets?: SummaryHighlightBucket[];
target: number | null;
width: number;
}
Expand Down Expand Up @@ -81,9 +80,9 @@ export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState
className,
height,
highlightedInterval,
// jumpToTarget,
jumpToTarget,
summaryBuckets,
// searchSummaryBuckets,
summaryHighlightBuckets,
width,
} = this.props;

Expand Down Expand Up @@ -119,17 +118,17 @@ export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState
width={width}
/>
) : null}
<TimeCursor x1={0} x2={width} y1={timeCursorY} y2={timeCursorY} />
{/* <g transform={`translate(${width * 0.5}, 0)`}>
<g transform={`translate(${width * 0.5}, 0)`}>
<SearchMarkers
buckets={searchSummaryBuckets || []}
buckets={summaryHighlightBuckets || []}
start={minTime}
end={maxTime}
width={width / 2}
height={height}
jumpToTarget={jumpToTarget}
/>
</g> */}
</g>
<TimeCursor x1={0} x2={width} y1={timeCursorY} y2={timeCursorY} />
</MinimapWrapper>
);
}
Expand All @@ -145,6 +144,7 @@ const MinimapBorder = euiStyled.line`
`;

const TimeCursor = euiStyled.line`
pointer-events: none;
stroke-width: 1px;
stroke: ${props =>
props.theme.darkMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import * as React from 'react';

import euiStyled, { keyframes } from '../../../../../../common/eui_styled_components';
import { LogEntryTime } from '../../../../common/log_entry';
import { SearchSummaryBucket } from '../../../../common/log_search_summary';
import { SearchMarkerTooltip } from './search_marker_tooltip';
import { SummaryHighlightBucket } from './types';

interface SearchMarkerProps {
bucket: SearchSummaryBucket;
bucket: SummaryHighlightBucket;
height: number;
width: number;
jumpToTarget: (target: LogEntryTime) => void;
Expand All @@ -31,7 +31,7 @@ export class SearchMarker extends React.PureComponent<SearchMarkerProps, SearchM
public handleClick: React.MouseEventHandler<SVGGElement> = evt => {
evt.stopPropagation();

this.props.jumpToTarget(this.props.bucket.representative.fields);
this.props.jumpToTarget(this.props.bucket.representativeKey);
};

public handleMouseEnter: React.MouseEventHandler<SVGGElement> = evt => {
Expand All @@ -51,7 +51,7 @@ export class SearchMarker extends React.PureComponent<SearchMarkerProps, SearchM
const { hoveredPosition } = this.state;

const bulge =
bucket.count > 1 ? (
bucket.entriesCount > 1 ? (
<SearchMarkerForegroundRect x="-2" y="-2" width="4" height={height + 2} rx="2" ry="2" />
) : (
<>
Expand All @@ -73,9 +73,9 @@ export class SearchMarker extends React.PureComponent<SearchMarkerProps, SearchM
<SearchMarkerTooltip markerPosition={hoveredPosition}>
<FormattedMessage
id="xpack.infra.logs.searchResultTooltip"
defaultMessage="{bucketCount, plural, one {# search result} other {# search results}}"
defaultMessage="{bucketCount, plural, one {# highlighted entry} other {# highlighted entries}}"
values={{
bucketCount: bucket.count,
bucketCount: bucket.entriesCount,
}}
/>
</SearchMarkerTooltip>
Expand Down Expand Up @@ -107,15 +107,16 @@ const SearchMarkerGroup = euiStyled.g`
`;

const SearchMarkerBackgroundRect = euiStyled.rect`
fill: ${props => props.theme.eui.euiColorSecondary};
fill: ${props => props.theme.eui.euiColorAccent};
opacity: 0;
transition: opacity ${props => props.theme.eui.euiAnimSpeedNormal} ease-in;
cursor: pointer;
${SearchMarkerGroup}:hover & {
opacity: 0.2;
opacity: 0.3;
}
`;

const SearchMarkerForegroundRect = euiStyled.rect`
fill: ${props => props.theme.eui.euiColorSecondary};
fill: ${props => props.theme.eui.euiColorAccent};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { scaleTime } from 'd3-scale';
import * as React from 'react';

import { LogEntryTime } from '../../../../common/log_entry';
import { SearchSummaryBucket } from '../../../../common/log_search_summary';
import { SearchMarker } from './search_marker';
import { SummaryHighlightBucket } from './types';

interface SearchMarkersProps {
buckets: SearchSummaryBucket[];
buckets: SummaryHighlightBucket[];
className?: string;
end: number;
start: number;
Expand All @@ -38,7 +38,10 @@ export class SearchMarkers extends React.PureComponent<SearchMarkersProps, {}> {
return (
<g className={classes}>
{buckets.map(bucket => (
<g key={bucket.representative.gid} transform={`translate(0, ${yScale(bucket.start)})`}>
<g
key={`${bucket.representativeKey.time}:${bucket.representativeKey.tiebreaker}`}
transform={`translate(0, ${yScale(bucket.start)})`}
>
<SearchMarker
bucket={bucket}
height={yScale(bucket.end) - yScale(bucket.start)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const TimeRulerTickLabel = euiStyled.text`
font-size: ${props => props.theme.eui.euiFontSizeXS};
line-height: ${props => props.theme.eui.euiLineHeight};
fill: ${props => props.theme.eui.textColors.subdued};
pointer-events: none;
`;

const TimeRulerGridLine = euiStyled.line`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { TimeKey } from '../../../../common/time';

export interface SummaryBucket {
start: number;
end: number;
entriesCount: number;
}

export interface SummaryHighlightBucket extends SummaryBucket {
representativeKey: TimeKey;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { LogEntryHighlightsQuery } from '../../../graphql/types';
import { DependencyError, useApolloClient } from '../../../utils/apollo_context';
import { LogEntryHighlightsMap } from '../../../utils/log_entry';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { logEntryHighlightsQuery } from './log_highlights.gql_query';
import { logEntryHighlightsQuery } from './log_entry_highlights.gql_query';

export type LogEntryHighlights = LogEntryHighlightsQuery.Query['source']['logEntryHighlights'];

export const useHighlightsFetcher = (
export const useLogEntryHighlights = (
sourceId: string,
sourceVersion: string | undefined,
startKey: TimeKey | null,
Expand All @@ -24,9 +24,7 @@ export const useHighlightsFetcher = (
highlightTerms: string[]
) => {
const apolloClient = useApolloClient();
const [logEntryHighlights, setLogEntryHighlights] = useState<LogEntryHighlights | undefined>(
undefined
);
const [logEntryHighlights, setLogEntryHighlights] = useState<LogEntryHighlights>([]);
const [loadLogEntryHighlightsRequest, loadLogEntryHighlights] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
Expand All @@ -35,7 +33,7 @@ export const useHighlightsFetcher = (
throw new DependencyError('Failed to load source: No apollo client available.');
}
if (!startKey || !endKey || !highlightTerms.length) {
throw new Error();
throw new Error('Skipping request: Insufficient parameters');
}

return await apolloClient.query<
Expand All @@ -51,9 +49,7 @@ export const useHighlightsFetcher = (
filterQuery,
highlights: [
{
query: JSON.stringify({
multi_match: { query: highlightTerms[0], type: 'phrase', lenient: true },
}),
query: highlightTerms[0],
countBefore: 1,
countAfter: 1,
},
Expand All @@ -69,7 +65,7 @@ export const useHighlightsFetcher = (
);

useEffect(() => {
setLogEntryHighlights(undefined);
setLogEntryHighlights([]);
}, [highlightTerms]);

useEffect(() => {
Expand All @@ -80,29 +76,24 @@ export const useHighlightsFetcher = (
) {
loadLogEntryHighlights();
} else {
setLogEntryHighlights(undefined);
setLogEntryHighlights([]);
}
}, [highlightTerms, startKey, endKey, filterQuery, sourceVersion]);

const logEntryHighlightsById = useMemo(
() =>
logEntryHighlights
? logEntryHighlights.reduce<LogEntryHighlightsMap>(
(accumulatedLogEntryHighlightsById, { entries }) => {
return entries.reduce<LogEntryHighlightsMap>(
(singleHighlightLogEntriesById, entry) => {
const highlightsForId = singleHighlightLogEntriesById[entry.gid] || [];
return {
...singleHighlightLogEntriesById,
[entry.gid]: [...highlightsForId, entry],
};
},
accumulatedLogEntryHighlightsById
);
},
{}
)
: {},
logEntryHighlights.reduce<LogEntryHighlightsMap>(
(accumulatedLogEntryHighlightsById, { entries }) => {
return entries.reduce<LogEntryHighlightsMap>((singleHighlightLogEntriesById, entry) => {
const highlightsForId = singleHighlightLogEntriesById[entry.gid] || [];
return {
...singleHighlightLogEntriesById,
[entry.gid]: [...highlightsForId, entry],
};
}, accumulatedLogEntryHighlightsById);
},
{}
),
[logEntryHighlights]
);

Expand Down
Loading

0 comments on commit 4f24b7e

Please sign in to comment.