Skip to content

Commit 3cbaf33

Browse files
author
Alejandro Fernández
authored
[7.x] [Logs UI] Actions menu in log entry categorization page (#69567) (#70968)
1 parent 7dad41b commit 3cbaf33

File tree

8 files changed

+215
-69
lines changed

8 files changed

+215
-69
lines changed

x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
timeRangeRT,
1313
routeTimingMetadataRT,
1414
} from '../../shared';
15+
import { logEntryContextRT } from '../../log_entries';
1516

1617
export const LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH =
1718
'/api/infra/log_analysis/results/log_entry_category_examples';
@@ -42,9 +43,12 @@ export type GetLogEntryCategoryExamplesRequestPayload = rt.TypeOf<
4243
*/
4344

4445
const logEntryCategoryExampleRT = rt.type({
46+
id: rt.string,
4547
dataset: rt.string,
4648
message: rt.string,
4749
timestamp: rt.number,
50+
tiebreaker: rt.number,
51+
context: logEntryContextRT,
4852
});
4953

5054
export type LogEntryCategoryExample = rt.TypeOf<typeof logEntryCategoryExampleRT>;

x-pack/plugins/infra/common/http_api/log_entries/entries.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,17 @@ export const logMessageColumnRT = rt.type({
7474

7575
export const logColumnRT = rt.union([logTimestampColumnRT, logFieldColumnRT, logMessageColumnRT]);
7676

77+
export const logEntryContextRT = rt.union([
78+
rt.type({}),
79+
rt.type({ 'container.id': rt.string }),
80+
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
81+
]);
82+
7783
export const logEntryRT = rt.type({
7884
id: rt.string,
7985
cursor: logEntriesCursorRT,
8086
columns: rt.array(logColumnRT),
81-
context: rt.union([
82-
rt.type({}),
83-
rt.type({ 'container.id': rt.string }),
84-
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
85-
]),
87+
context: logEntryContextRT,
8688
});
8789

8890
export type LogMessageConstantPart = rt.TypeOf<typeof logMessageConstantPartRT>;
@@ -92,6 +94,7 @@ export type LogTimestampColumn = rt.TypeOf<typeof logTimestampColumnRT>;
9294
export type LogFieldColumn = rt.TypeOf<typeof logFieldColumnRT>;
9395
export type LogMessageColumn = rt.TypeOf<typeof logMessageColumnRT>;
9496
export type LogColumn = rt.TypeOf<typeof logColumnRT>;
97+
export type LogEntryContext = rt.TypeOf<typeof logEntryContextRT>;
9598
export type LogEntry = rt.TypeOf<typeof logEntryRT>;
9699

97100
export const logEntriesResponseRT = rt.type({

x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
StringTimeRange,
2222
useLogEntryCategoriesResultsUrlState,
2323
} from './use_log_entry_categories_results_url_state';
24+
import { PageViewLogInContext } from '../stream/page_view_log_in_context';
25+
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
2426

2527
const JOB_STATUS_POLLING_INTERVAL = 30000;
2628

@@ -178,54 +180,61 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
178180
);
179181

180182
return (
181-
<ResultsContentPage>
182-
<EuiFlexGroup direction="column">
183-
<EuiFlexItem grow={false}>
184-
<EuiPanel paddingSize="m">
185-
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
186-
<EuiFlexItem />
187-
<EuiFlexItem grow={false}>
188-
<EuiSuperDatePicker
189-
start={selectedTimeRange.startTime}
190-
end={selectedTimeRange.endTime}
191-
onTimeChange={handleSelectedTimeRangeChange}
192-
isPaused={autoRefresh.isPaused}
193-
refreshInterval={autoRefresh.interval}
194-
onRefreshChange={handleAutoRefreshChange}
195-
/>
196-
</EuiFlexItem>
197-
</EuiFlexGroup>
198-
</EuiPanel>
199-
</EuiFlexItem>
200-
<EuiFlexItem grow={false}>
201-
<CategoryJobNoticesSection
202-
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
203-
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
204-
hasStoppedJobs={hasStoppedJobs}
205-
isFirstUse={isFirstUse}
206-
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
207-
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
208-
qualityWarnings={categoryQualityWarnings}
209-
/>
210-
</EuiFlexItem>
211-
<EuiFlexItem grow={false}>
212-
<EuiPanel paddingSize="m">
213-
<TopCategoriesSection
214-
availableDatasets={logEntryCategoryDatasets}
215-
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
216-
isLoadingTopCategories={isLoadingTopLogEntryCategories}
217-
jobId={jobIds['log-entry-categories-count']}
218-
onChangeDatasetSelection={setCategoryQueryDatasets}
219-
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
220-
selectedDatasets={categoryQueryDatasets}
221-
sourceId={sourceId}
222-
timeRange={categoryQueryTimeRange.timeRange}
223-
topCategories={topLogEntryCategories}
183+
<ViewLogInContext.Provider
184+
sourceId={sourceId}
185+
startTimestamp={categoryQueryTimeRange.timeRange.startTime}
186+
endTimestamp={categoryQueryTimeRange.timeRange.endTime}
187+
>
188+
<ResultsContentPage>
189+
<EuiFlexGroup direction="column">
190+
<EuiFlexItem grow={false}>
191+
<EuiPanel paddingSize="m">
192+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
193+
<EuiFlexItem />
194+
<EuiFlexItem grow={false}>
195+
<EuiSuperDatePicker
196+
start={selectedTimeRange.startTime}
197+
end={selectedTimeRange.endTime}
198+
onTimeChange={handleSelectedTimeRangeChange}
199+
isPaused={autoRefresh.isPaused}
200+
refreshInterval={autoRefresh.interval}
201+
onRefreshChange={handleAutoRefreshChange}
202+
/>
203+
</EuiFlexItem>
204+
</EuiFlexGroup>
205+
</EuiPanel>
206+
</EuiFlexItem>
207+
<EuiFlexItem grow={false}>
208+
<CategoryJobNoticesSection
209+
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
210+
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
211+
hasStoppedJobs={hasStoppedJobs}
212+
isFirstUse={isFirstUse}
213+
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
214+
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
215+
qualityWarnings={categoryQualityWarnings}
224216
/>
225-
</EuiPanel>
226-
</EuiFlexItem>
227-
</EuiFlexGroup>
228-
</ResultsContentPage>
217+
</EuiFlexItem>
218+
<EuiFlexItem grow={false}>
219+
<EuiPanel paddingSize="m">
220+
<TopCategoriesSection
221+
availableDatasets={logEntryCategoryDatasets}
222+
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
223+
isLoadingTopCategories={isLoadingTopLogEntryCategories}
224+
jobId={jobIds['log-entry-categories-count']}
225+
onChangeDatasetSelection={setCategoryQueryDatasets}
226+
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
227+
selectedDatasets={categoryQueryDatasets}
228+
sourceId={sourceId}
229+
timeRange={categoryQueryTimeRange.timeRange}
230+
topCategories={topLogEntryCategories}
231+
/>
232+
</EuiPanel>
233+
</EuiFlexItem>
234+
</EuiFlexGroup>
235+
</ResultsContentPage>
236+
<PageViewLogInContext />
237+
</ViewLogInContext.Provider>
229238
);
230239
};
231240

x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_details_row.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ export const CategoryDetailsRow: React.FunctionComponent<{
4545
{logEntryCategoryExamples.map((example, exampleIndex) => (
4646
<CategoryExampleMessage
4747
key={exampleIndex}
48+
id={example.id}
4849
dataset={example.dataset}
4950
message={example.message}
51+
timeRange={timeRange}
5052
timestamp={example.timestamp}
53+
tiebreaker={example.tiebreaker}
54+
context={example.context}
5155
/>
5256
))}
5357
</LogEntryExampleMessages>

x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { useMemo } from 'react';
7+
import React, { useMemo, useState, useCallback, useContext } from 'react';
8+
import { i18n } from '@kbn/i18n';
9+
import { encode } from 'rison-node';
10+
import moment from 'moment';
811

9-
import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis';
12+
import { LogEntry, LogEntryContext } from '../../../../../../common/http_api';
13+
import { TimeRange } from '../../../../../../common/http_api/shared';
14+
import {
15+
getFriendlyNameForPartitionId,
16+
partitionField,
17+
} from '../../../../../../common/log_analysis';
18+
import { ViewLogInContext } from '../../../../../containers/logs/view_log_in_context';
1019
import {
1120
LogEntryColumn,
1221
LogEntryFieldColumn,
@@ -15,24 +24,63 @@ import {
1524
LogEntryTimestampColumn,
1625
} from '../../../../../components/logging/log_text_stream';
1726
import { LogColumnConfiguration } from '../../../../../utils/source_configuration';
27+
import { LogEntryContextMenu } from '../../../../../components/logging/log_text_stream/log_entry_context_menu';
28+
import { useLinkProps } from '../../../../../hooks/use_link_props';
1829

1930
export const exampleMessageScale = 'medium' as const;
2031
export const exampleTimestampFormat = 'dateTime' as const;
2132

2233
export const CategoryExampleMessage: React.FunctionComponent<{
34+
id: string;
2335
dataset: string;
2436
message: string;
37+
timeRange: TimeRange;
2538
timestamp: number;
26-
}> = ({ dataset, message, timestamp }) => {
39+
tiebreaker: number;
40+
context: LogEntryContext;
41+
}> = ({ id, dataset, message, timestamp, timeRange, tiebreaker, context }) => {
42+
const [, { setContextEntry }] = useContext(ViewLogInContext.Context);
2743
// the dataset must be encoded for the field column and the empty value must
2844
// be turned into a user-friendly value
2945
const encodedDatasetFieldValue = useMemo(
3046
() => JSON.stringify(getFriendlyNameForPartitionId(dataset)),
3147
[dataset]
3248
);
3349

50+
const [isHovered, setIsHovered] = useState<boolean>(false);
51+
const setHovered = useCallback(() => setIsHovered(true), []);
52+
const setNotHovered = useCallback(() => setIsHovered(false), []);
53+
54+
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
55+
const openMenu = useCallback(() => setIsMenuOpen(true), []);
56+
const closeMenu = useCallback(() => setIsMenuOpen(false), []);
57+
58+
const viewInStreamLinkProps = useLinkProps({
59+
app: 'logs',
60+
pathname: 'stream',
61+
search: {
62+
logPosition: encode({
63+
end: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
64+
position: { tiebreaker, time: timestamp },
65+
start: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
66+
streamLive: false,
67+
}),
68+
flyoutOptions: encode({
69+
surroundingLogsId: id,
70+
}),
71+
logFilter: encode({
72+
expression: `${partitionField}: ${dataset}`,
73+
kind: 'kuery',
74+
}),
75+
},
76+
});
77+
3478
return (
35-
<LogEntryRowWrapper scale={exampleMessageScale}>
79+
<LogEntryRowWrapper
80+
scale={exampleMessageScale}
81+
onMouseEnter={setHovered}
82+
onMouseLeave={setNotHovered}
83+
>
3684
<LogEntryColumn {...columnWidths[timestampColumnId]}>
3785
<LogEntryTimestampColumn format={exampleTimestampFormat} time={timestamp} />
3886
</LogEntryColumn>
@@ -60,6 +108,39 @@ export const CategoryExampleMessage: React.FunctionComponent<{
60108
wrapMode="none"
61109
/>
62110
</LogEntryColumn>
111+
<LogEntryColumn {...columnWidths[iconColumnId]}>
112+
{isHovered || isMenuOpen ? (
113+
<LogEntryContextMenu
114+
isOpen={isMenuOpen}
115+
onOpen={openMenu}
116+
onClose={closeMenu}
117+
items={[
118+
{
119+
label: i18n.translate('xpack.infra.logs.categoryExample.viewInStreamText', {
120+
defaultMessage: 'View in stream',
121+
}),
122+
onClick: viewInStreamLinkProps.onClick!,
123+
href: viewInStreamLinkProps.href,
124+
},
125+
{
126+
label: i18n.translate('xpack.infra.logs.categoryExample.viewInContextText', {
127+
defaultMessage: 'View in context',
128+
}),
129+
onClick: () => {
130+
const logEntry: LogEntry = {
131+
id,
132+
context,
133+
cursor: { time: timestamp, tiebreaker },
134+
columns: [],
135+
};
136+
137+
setContextEntry(logEntry);
138+
},
139+
},
140+
]}
141+
/>
142+
) : null}
143+
</LogEntryColumn>
63144
</LogEntryRowWrapper>
64145
);
65146
};
@@ -68,6 +149,7 @@ const noHighlights: never[] = [];
68149
const timestampColumnId = 'category-example-timestamp-column' as const;
69150
const messageColumnId = 'category-examples-message-column' as const;
70151
const datasetColumnId = 'category-examples-dataset-column' as const;
152+
const iconColumnId = 'category-examples-icon-column' as const;
71153

72154
const columnWidths = {
73155
[timestampColumnId]: {
@@ -85,7 +167,12 @@ const columnWidths = {
85167
growWeight: 0,
86168
shrinkWeight: 0,
87169
// w_dataset + w_max_anomaly + w_expand - w_padding = 200 px + 160 px + 40 px + 40 px - 8 px
88-
baseWidth: '432px',
170+
baseWidth: '400px',
171+
},
172+
[iconColumnId]: {
173+
growWeight: 0,
174+
shrinkWeight: 0,
175+
baseWidth: '32px',
89176
},
90177
};
91178

0 commit comments

Comments
 (0)