Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Logs UI] Add sorting capabilities to categories page #88051

Merged
merged 6 commits into from
Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ export type LogEntryCategoriesHistogramParameters = rt.TypeOf<
typeof logEntryCategoriesHistogramParametersRT
>;

const sortOptionsRT = rt.keyof({
maximumAnomalyScore: null,
logEntryCount: null,
});

const sortDirectionsRT = rt.keyof({
asc: null,
desc: null,
});

const categorySortRT = rt.type({
field: sortOptionsRT,
direction: sortDirectionsRT,
});

export type CategorySort = rt.TypeOf<typeof categorySortRT>;

export const getLogEntryCategoriesRequestPayloadRT = rt.type({
data: rt.intersection([
rt.type({
Expand All @@ -41,6 +58,8 @@ export const getLogEntryCategoriesRequestPayloadRT = rt.type({
timeRange: timeRangeRT,
// a list of histograms to create
histograms: rt.array(logEntryCategoriesHistogramParametersRT),
// the criteria to the categories by
sort: categorySortRT,
Kerry350 marked this conversation as resolved.
Show resolved Hide resolved
}),
rt.partial({
// the datasets to filter for (optional, unfiltered if not present)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
isLoadingTopLogEntryCategories,
logEntryCategoryDatasets,
topLogEntryCategories,
sortOptions,
changeSortOptions,
} = useLogEntryCategoriesResults({
categoriesCount: 25,
endTime: categoryQueryTimeRange.timeRange.endTime,
Expand Down Expand Up @@ -145,7 +147,12 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC

useEffect(() => {
getTopLogEntryCategories();
}, [getTopLogEntryCategories, categoryQueryDatasets, categoryQueryTimeRange.lastChangedTime]);
}, [
getTopLogEntryCategories,
categoryQueryDatasets,
categoryQueryTimeRange.lastChangedTime,
sortOptions,
]);

useEffect(() => {
getLogEntryCategoryDatasets();
Expand Down Expand Up @@ -219,6 +226,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange}
topCategories={topLogEntryCategories}
sortOptions={sortOptions}
changeSortOptions={changeSortOptions}
/>
</EuiPanel>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { RecreateJobButton } from '../../../../../components/logging/log_analysi
import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results';
import { DatasetsSelector } from '../../../../../components/logging/log_analysis_results/datasets_selector';
import { TopCategoriesTable } from './top_categories_table';
import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results';

export const TopCategoriesSection: React.FunctionComponent<{
availableDatasets: string[];
Expand All @@ -29,6 +30,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId: string;
timeRange: TimeRange;
topCategories: LogEntryCategory[];
sortOptions: SortOptions;
changeSortOptions: ChangeSortOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total nit: Do we want to keep the on* naming schema for such functions?

Suggested change
changeSortOptions: ChangeSortOptions;
onChangeSortOptions: ChangeSortOptions;

}> = ({
availableDatasets,
hasSetupCapabilities,
Expand All @@ -41,6 +44,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId,
timeRange,
topCategories,
sortOptions,
changeSortOptions,
}) => {
return (
<>
Expand Down Expand Up @@ -80,6 +85,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId={sourceId}
timeRange={timeRange}
topCategories={topCategories}
sortOptions={sortOptions}
changeSortOptions={changeSortOptions}
/>
</LoadingOverlayWrapper>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import useSet from 'react-use/lib/useSet';

import { euiStyled } from '../../../../../../../observability/public';
Expand All @@ -24,6 +24,7 @@ import { RegularExpressionRepresentation } from './category_expression';
import { DatasetActionsList } from './datasets_action_list';
import { DatasetsList } from './datasets_list';
import { LogEntryCountSparkline } from './log_entry_count_sparkline';
import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results';

export const TopCategoriesTable = euiStyled(
({
Expand All @@ -32,13 +33,28 @@ export const TopCategoriesTable = euiStyled(
sourceId,
timeRange,
topCategories,
sortOptions,
changeSortOptions,
}: {
categorizationJobId: string;
className?: string;
sourceId: string;
timeRange: TimeRange;
topCategories: LogEntryCategory[];
sortOptions: SortOptions;
changeSortOptions: ChangeSortOptions;
}) => {
const tableSortOptions = useMemo(() => {
return { sort: sortOptions };
}, [sortOptions]);

const handleTableChange = useCallback(
({ sort = {} }) => {
changeSortOptions(sort);
},
[changeSortOptions]
);

const [expandedCategories, { add: expandCategory, remove: collapseCategory }] = useSet<number>(
new Set()
);
Expand Down Expand Up @@ -80,6 +96,8 @@ export const TopCategoriesTable = euiStyled(
itemId="categoryId"
items={topCategories}
rowProps={{ className: `${className} euiTableRow--topAligned` }}
onChange={handleTableChange}
sorting={tableSortOptions}
/>
);
}
Expand All @@ -102,6 +120,7 @@ const createColumns = (
name: i18n.translate('xpack.infra.logs.logEntryCategories.countColumnTitle', {
defaultMessage: 'Message count',
}),
sortable: true,
render: (logEntryCount: number) => {
return numeral(logEntryCount).format('0,0');
},
Expand Down Expand Up @@ -147,6 +166,7 @@ const createColumns = (
name: i18n.translate('xpack.infra.logs.logEntryCategories.maximumAnomalyScoreColumnTitle', {
defaultMessage: 'Maximum anomaly score',
}),
sortable: true,
render: (_maximumAnomalyScore: number, item) => (
<AnomalySeverityIndicatorList datasets={item.datasets} />
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getLogEntryCategoriesRequestPayloadRT,
getLogEntryCategoriesSuccessReponsePayloadRT,
LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH,
CategorySort,
} from '../../../../../common/http_api/log_analysis';
import { decodeOrThrow } from '../../../../../common/runtime_types';

Expand All @@ -19,13 +20,14 @@ interface RequestArgs {
endTime: number;
categoryCount: number;
datasets?: string[];
sort: CategorySort;
}

export const callGetTopLogEntryCategoriesAPI = async (
requestArgs: RequestArgs,
fetch: HttpHandler
) => {
const { sourceId, startTime, endTime, categoryCount, datasets } = requestArgs;
const { sourceId, startTime, endTime, categoryCount, datasets, sort } = requestArgs;
const intervalDuration = endTime - startTime;

const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, {
Expand Down Expand Up @@ -58,6 +60,7 @@ export const callGetTopLogEntryCategoriesAPI = async (
bucketCount: 1,
},
],
sort,
},
})
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useMemo, useState } from 'react';
import {
GetLogEntryCategoriesSuccessResponsePayload,
GetLogEntryCategoryDatasetsSuccessResponsePayload,
CategorySort,
} from '../../../../common/http_api/log_analysis';
import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise';
import { callGetTopLogEntryCategoriesAPI } from './service_calls/get_top_log_entry_categories';
Expand All @@ -18,6 +19,9 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
type TopLogEntryCategories = GetLogEntryCategoriesSuccessResponsePayload['data']['categories'];
type LogEntryCategoryDatasets = GetLogEntryCategoryDatasetsSuccessResponsePayload['data']['datasets'];

export type SortOptions = CategorySort;
export type ChangeSortOptions = (sortOptions: CategorySort) => void;

export const useLogEntryCategoriesResults = ({
categoriesCount,
filteredDatasets: filteredDatasets,
Expand All @@ -35,6 +39,10 @@ export const useLogEntryCategoriesResults = ({
sourceId: string;
startTime: number;
}) => {
const [sortOptions, setSortOptions] = useState<SortOptions>({
field: 'maximumAnomalyScore',
direction: 'desc',
});
const { services } = useKibanaContextForPlugin();
const [topLogEntryCategories, setTopLogEntryCategories] = useState<TopLogEntryCategories>([]);
const [
Expand All @@ -53,6 +61,7 @@ export const useLogEntryCategoriesResults = ({
endTime,
categoryCount: categoriesCount,
datasets: filteredDatasets,
sort: sortOptions,
},
services.http.fetch
);
Expand All @@ -70,7 +79,7 @@ export const useLogEntryCategoriesResults = ({
}
},
},
[categoriesCount, endTime, filteredDatasets, sourceId, startTime]
[categoriesCount, endTime, filteredDatasets, sourceId, startTime, sortOptions]
);

const [getLogEntryCategoryDatasetsRequest, getLogEntryCategoryDatasets] = useTrackedPromise(
Expand Down Expand Up @@ -121,5 +130,7 @@ export const useLogEntryCategoriesResults = ({
isLoadingTopLogEntryCategories,
logEntryCategoryDatasets,
topLogEntryCategories,
sortOptions,
changeSortOptions: setSortOptions,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
jobCustomSettingsRT,
logEntryCategoriesJobTypes,
} from '../../../common/log_analysis';
import { CategorySort } from '../../../common/http_api/log_analysis';
import { startTracingSpan } from '../../../common/performance_tracing';
import { decodeOrThrow } from '../../../common/runtime_types';
import type { MlAnomalyDetectors, MlSystem } from '../../types';
Expand Down Expand Up @@ -49,7 +50,8 @@ export async function getTopLogEntryCategories(
endTime: number,
categoryCount: number,
datasets: string[],
histograms: HistogramParameters[]
histograms: HistogramParameters[],
sort: CategorySort
) {
const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories');

Expand All @@ -68,7 +70,8 @@ export async function getTopLogEntryCategories(
startTime,
endTime,
categoryCount,
datasets
datasets,
sort
);

const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId);
Expand Down Expand Up @@ -214,7 +217,8 @@ async function fetchTopLogEntryCategories(
startTime: number,
endTime: number,
categoryCount: number,
datasets: string[]
datasets: string[],
sort: CategorySort
) {
const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES');

Expand All @@ -225,7 +229,8 @@ async function fetchTopLogEntryCategories(
startTime,
endTime,
categoryCount,
datasets
datasets,
sort
),
[logEntryCategoriesCountJobId]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,33 @@ import {
createDatasetsFilters,
} from './common';

import { CategorySort } from '../../../../common/http_api/log_analysis';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import makes my slightly uncomfortable because it couples the query to the HTTP types, but I'm not sure it's worth dwelling on.

Do you think it would be worth it to duplicate that type here so they are technically independent but happen to line up without conversion (how convenient)?

Alternatively we could move that type to a location like common/log_analysis/log_entry_rate_analysis.ts so it's independent of the HTTP layer. 🤔


type CategoryAggregationOrder =
| 'filter_record>maximum_record_score'
| 'filter_model_plot>sum_actual';
const getAggregationOrderForSortField = (
field: CategorySort['field']
): CategoryAggregationOrder => {
switch (field) {
case 'maximumAnomalyScore':
return 'filter_record>maximum_record_score';
break;
case 'logEntryCount':
return 'filter_model_plot>sum_actual';
break;
default:
return 'filter_model_plot>sum_actual';
}
};

export const createTopLogEntryCategoriesQuery = (
logEntryCategoriesJobId: string,
startTime: number,
endTime: number,
size: number,
datasets: string[],
sortDirection: 'asc' | 'desc' = 'desc'
sort: CategorySort
) => ({
...defaultRequestParameters,
body: {
Expand Down Expand Up @@ -65,7 +85,7 @@ export const createTopLogEntryCategoriesQuery = (
field: 'by_field_value',
size,
order: {
'filter_model_plot>sum_actual': sortDirection,
[getAggregationOrderForSortField(sort.field)]: sort.direction,
},
},
aggs: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs)
sourceId,
timeRange: { startTime, endTime },
datasets,
sort,
},
} = request.body;

Expand All @@ -51,7 +52,8 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs)
endTime: histogram.timeRange.endTime,
id: histogram.id,
startTime: histogram.timeRange.startTime,
}))
})),
sort
);

return response.ok({
Expand Down