Skip to content

Commit

Permalink
[Logs UI] Actions menu in log entry categorization page (elastic#69567)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alejandro Fernández authored Jul 7, 2020
1 parent 0bae5d6 commit 21fc56e
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
timeRangeRT,
routeTimingMetadataRT,
} from '../../shared';
import { logEntryContextRT } from '../../log_entries';

export const LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH =
'/api/infra/log_analysis/results/log_entry_category_examples';
Expand Down Expand Up @@ -42,9 +43,12 @@ export type GetLogEntryCategoryExamplesRequestPayload = rt.TypeOf<
*/

const logEntryCategoryExampleRT = rt.type({
id: rt.string,
dataset: rt.string,
message: rt.string,
timestamp: rt.number,
tiebreaker: rt.number,
context: logEntryContextRT,
});

export type LogEntryCategoryExample = rt.TypeOf<typeof logEntryCategoryExampleRT>;
Expand Down
13 changes: 8 additions & 5 deletions x-pack/plugins/infra/common/http_api/log_entries/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,17 @@ export const logMessageColumnRT = rt.type({

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

export const logEntryContextRT = rt.union([
rt.type({}),
rt.type({ 'container.id': rt.string }),
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
]);

export const logEntryRT = rt.type({
id: rt.string,
cursor: logEntriesCursorRT,
columns: rt.array(logColumnRT),
context: rt.union([
rt.type({}),
rt.type({ 'container.id': rt.string }),
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
]),
context: logEntryContextRT,
});

export type LogMessageConstantPart = rt.TypeOf<typeof logMessageConstantPartRT>;
Expand All @@ -92,6 +94,7 @@ export type LogTimestampColumn = rt.TypeOf<typeof logTimestampColumnRT>;
export type LogFieldColumn = rt.TypeOf<typeof logFieldColumnRT>;
export type LogMessageColumn = rt.TypeOf<typeof logMessageColumnRT>;
export type LogColumn = rt.TypeOf<typeof logColumnRT>;
export type LogEntryContext = rt.TypeOf<typeof logEntryContextRT>;
export type LogEntry = rt.TypeOf<typeof logEntryRT>;

export const logEntriesResponseRT = rt.type({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
StringTimeRange,
useLogEntryCategoriesResultsUrlState,
} from './use_log_entry_categories_results_url_state';
import { PageViewLogInContext } from '../stream/page_view_log_in_context';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';

const JOB_STATUS_POLLING_INTERVAL = 30000;

Expand Down Expand Up @@ -178,54 +180,61 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
);

return (
<ResultsContentPage>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem />
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
qualityWarnings={categoryQualityWarnings}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<TopCategoriesSection
availableDatasets={logEntryCategoryDatasets}
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
isLoadingTopCategories={isLoadingTopLogEntryCategories}
jobId={jobIds['log-entry-categories-count']}
onChangeDatasetSelection={setCategoryQueryDatasets}
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
selectedDatasets={categoryQueryDatasets}
sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange}
topCategories={topLogEntryCategories}
<ViewLogInContext.Provider
sourceId={sourceId}
startTimestamp={categoryQueryTimeRange.timeRange.startTime}
endTimestamp={categoryQueryTimeRange.timeRange.endTime}
>
<ResultsContentPage>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem />
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
qualityWarnings={categoryQualityWarnings}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ResultsContentPage>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<TopCategoriesSection
availableDatasets={logEntryCategoryDatasets}
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
isLoadingTopCategories={isLoadingTopLogEntryCategories}
jobId={jobIds['log-entry-categories-count']}
onChangeDatasetSelection={setCategoryQueryDatasets}
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
selectedDatasets={categoryQueryDatasets}
sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange}
topCategories={topLogEntryCategories}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ResultsContentPage>
<PageViewLogInContext />
</ViewLogInContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ export const CategoryDetailsRow: React.FunctionComponent<{
{logEntryCategoryExamples.map((example, exampleIndex) => (
<CategoryExampleMessage
key={exampleIndex}
id={example.id}
dataset={example.dataset}
message={example.message}
timeRange={timeRange}
timestamp={example.timestamp}
tiebreaker={example.tiebreaker}
context={example.context}
/>
))}
</LogEntryExampleMessages>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo } from 'react';
import React, { useMemo, useState, useCallback, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { encode } from 'rison-node';
import moment from 'moment';

import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis';
import { LogEntry, LogEntryContext } from '../../../../../../common/http_api';
import { TimeRange } from '../../../../../../common/http_api/shared';
import {
getFriendlyNameForPartitionId,
partitionField,
} from '../../../../../../common/log_analysis';
import { ViewLogInContext } from '../../../../../containers/logs/view_log_in_context';
import {
LogEntryColumn,
LogEntryFieldColumn,
Expand All @@ -15,24 +24,63 @@ import {
LogEntryTimestampColumn,
} from '../../../../../components/logging/log_text_stream';
import { LogColumnConfiguration } from '../../../../../utils/source_configuration';
import { LogEntryContextMenu } from '../../../../../components/logging/log_text_stream/log_entry_context_menu';
import { useLinkProps } from '../../../../../hooks/use_link_props';

export const exampleMessageScale = 'medium' as const;
export const exampleTimestampFormat = 'dateTime' as const;

export const CategoryExampleMessage: React.FunctionComponent<{
id: string;
dataset: string;
message: string;
timeRange: TimeRange;
timestamp: number;
}> = ({ dataset, message, timestamp }) => {
tiebreaker: number;
context: LogEntryContext;
}> = ({ id, dataset, message, timestamp, timeRange, tiebreaker, context }) => {
const [, { setContextEntry }] = useContext(ViewLogInContext.Context);
// the dataset must be encoded for the field column and the empty value must
// be turned into a user-friendly value
const encodedDatasetFieldValue = useMemo(
() => JSON.stringify(getFriendlyNameForPartitionId(dataset)),
[dataset]
);

const [isHovered, setIsHovered] = useState<boolean>(false);
const setHovered = useCallback(() => setIsHovered(true), []);
const setNotHovered = useCallback(() => setIsHovered(false), []);

const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const openMenu = useCallback(() => setIsMenuOpen(true), []);
const closeMenu = useCallback(() => setIsMenuOpen(false), []);

const viewInStreamLinkProps = useLinkProps({
app: 'logs',
pathname: 'stream',
search: {
logPosition: encode({
end: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
position: { tiebreaker, time: timestamp },
start: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
streamLive: false,
}),
flyoutOptions: encode({
surroundingLogsId: id,
}),
logFilter: encode({
expression: `${partitionField}: ${dataset}`,
kind: 'kuery',
}),
},
});

return (
<LogEntryRowWrapper scale={exampleMessageScale}>
<LogEntryRowWrapper
scale={exampleMessageScale}
onMouseEnter={setHovered}
onMouseLeave={setNotHovered}
>
<LogEntryColumn {...columnWidths[timestampColumnId]}>
<LogEntryTimestampColumn format={exampleTimestampFormat} time={timestamp} />
</LogEntryColumn>
Expand Down Expand Up @@ -60,6 +108,39 @@ export const CategoryExampleMessage: React.FunctionComponent<{
wrapMode="none"
/>
</LogEntryColumn>
<LogEntryColumn {...columnWidths[iconColumnId]}>
{isHovered || isMenuOpen ? (
<LogEntryContextMenu
isOpen={isMenuOpen}
onOpen={openMenu}
onClose={closeMenu}
items={[
{
label: i18n.translate('xpack.infra.logs.categoryExample.viewInStreamText', {
defaultMessage: 'View in stream',
}),
onClick: viewInStreamLinkProps.onClick!,
href: viewInStreamLinkProps.href,
},
{
label: i18n.translate('xpack.infra.logs.categoryExample.viewInContextText', {
defaultMessage: 'View in context',
}),
onClick: () => {
const logEntry: LogEntry = {
id,
context,
cursor: { time: timestamp, tiebreaker },
columns: [],
};

setContextEntry(logEntry);
},
},
]}
/>
) : null}
</LogEntryColumn>
</LogEntryRowWrapper>
);
};
Expand All @@ -68,6 +149,7 @@ const noHighlights: never[] = [];
const timestampColumnId = 'category-example-timestamp-column' as const;
const messageColumnId = 'category-examples-message-column' as const;
const datasetColumnId = 'category-examples-dataset-column' as const;
const iconColumnId = 'category-examples-icon-column' as const;

const columnWidths = {
[timestampColumnId]: {
Expand All @@ -85,7 +167,12 @@ const columnWidths = {
growWeight: 0,
shrinkWeight: 0,
// w_dataset + w_max_anomaly + w_expand - w_padding = 200 px + 160 px + 40 px + 40 px - 8 px
baseWidth: '432px',
baseWidth: '400px',
},
[iconColumnId]: {
growWeight: 0,
shrinkWeight: 0,
baseWidth: '32px',
},
};

Expand Down
Loading

0 comments on commit 21fc56e

Please sign in to comment.