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

* Add sorting capabilities to categories page

Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
This commit is contained in:
Kerry Gallagher 2021-01-18 14:12:29 +00:00 committed by GitHub
parent b8d43139f3
commit 53f4b21a81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 11 deletions

View file

@ -30,6 +30,23 @@ export type LogEntryCategoriesHistogramParameters = rt.TypeOf<
typeof logEntryCategoriesHistogramParametersRT 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({ export const getLogEntryCategoriesRequestPayloadRT = rt.type({
data: rt.intersection([ data: rt.intersection([
rt.type({ rt.type({
@ -41,6 +58,8 @@ export const getLogEntryCategoriesRequestPayloadRT = rt.type({
timeRange: timeRangeRT, timeRange: timeRangeRT,
// a list of histograms to create // a list of histograms to create
histograms: rt.array(logEntryCategoriesHistogramParametersRT), histograms: rt.array(logEntryCategoriesHistogramParametersRT),
// the criteria to the categories by
sort: categorySortRT,
}), }),
rt.partial({ rt.partial({
// the datasets to filter for (optional, unfiltered if not present) // the datasets to filter for (optional, unfiltered if not present)

View file

@ -87,6 +87,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
isLoadingTopLogEntryCategories, isLoadingTopLogEntryCategories,
logEntryCategoryDatasets, logEntryCategoryDatasets,
topLogEntryCategories, topLogEntryCategories,
sortOptions,
changeSortOptions,
} = useLogEntryCategoriesResults({ } = useLogEntryCategoriesResults({
categoriesCount: 25, categoriesCount: 25,
endTime: categoryQueryTimeRange.timeRange.endTime, endTime: categoryQueryTimeRange.timeRange.endTime,
@ -145,7 +147,12 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
useEffect(() => { useEffect(() => {
getTopLogEntryCategories(); getTopLogEntryCategories();
}, [getTopLogEntryCategories, categoryQueryDatasets, categoryQueryTimeRange.lastChangedTime]); }, [
getTopLogEntryCategories,
categoryQueryDatasets,
categoryQueryTimeRange.lastChangedTime,
sortOptions,
]);
useEffect(() => { useEffect(() => {
getLogEntryCategoryDatasets(); getLogEntryCategoryDatasets();
@ -219,6 +226,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
sourceId={sourceId} sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange} timeRange={categoryQueryTimeRange.timeRange}
topCategories={topLogEntryCategories} topCategories={topLogEntryCategories}
sortOptions={sortOptions}
changeSortOptions={changeSortOptions}
/> />
</EuiPanel> </EuiPanel>
</EuiFlexItem> </EuiFlexItem>

View file

@ -16,6 +16,7 @@ import { RecreateJobButton } from '../../../../../components/logging/log_analysi
import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results';
import { DatasetsSelector } from '../../../../../components/logging/log_analysis_results/datasets_selector'; import { DatasetsSelector } from '../../../../../components/logging/log_analysis_results/datasets_selector';
import { TopCategoriesTable } from './top_categories_table'; import { TopCategoriesTable } from './top_categories_table';
import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results';
export const TopCategoriesSection: React.FunctionComponent<{ export const TopCategoriesSection: React.FunctionComponent<{
availableDatasets: string[]; availableDatasets: string[];
@ -29,6 +30,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId: string; sourceId: string;
timeRange: TimeRange; timeRange: TimeRange;
topCategories: LogEntryCategory[]; topCategories: LogEntryCategory[];
sortOptions: SortOptions;
changeSortOptions: ChangeSortOptions;
}> = ({ }> = ({
availableDatasets, availableDatasets,
hasSetupCapabilities, hasSetupCapabilities,
@ -41,6 +44,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId, sourceId,
timeRange, timeRange,
topCategories, topCategories,
sortOptions,
changeSortOptions,
}) => { }) => {
return ( return (
<> <>
@ -80,6 +85,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId={sourceId} sourceId={sourceId}
timeRange={timeRange} timeRange={timeRange}
topCategories={topCategories} topCategories={topCategories}
sortOptions={sortOptions}
changeSortOptions={changeSortOptions}
/> />
</LoadingOverlayWrapper> </LoadingOverlayWrapper>
</> </>

View file

@ -7,7 +7,7 @@
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import numeral from '@elastic/numeral'; import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react'; import React, { useMemo, useCallback } from 'react';
import useSet from 'react-use/lib/useSet'; import useSet from 'react-use/lib/useSet';
import { euiStyled } from '../../../../../../../observability/public'; import { euiStyled } from '../../../../../../../observability/public';
@ -24,6 +24,7 @@ import { RegularExpressionRepresentation } from './category_expression';
import { DatasetActionsList } from './datasets_action_list'; import { DatasetActionsList } from './datasets_action_list';
import { DatasetsList } from './datasets_list'; import { DatasetsList } from './datasets_list';
import { LogEntryCountSparkline } from './log_entry_count_sparkline'; import { LogEntryCountSparkline } from './log_entry_count_sparkline';
import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results';
export const TopCategoriesTable = euiStyled( export const TopCategoriesTable = euiStyled(
({ ({
@ -32,13 +33,28 @@ export const TopCategoriesTable = euiStyled(
sourceId, sourceId,
timeRange, timeRange,
topCategories, topCategories,
sortOptions,
changeSortOptions,
}: { }: {
categorizationJobId: string; categorizationJobId: string;
className?: string; className?: string;
sourceId: string; sourceId: string;
timeRange: TimeRange; timeRange: TimeRange;
topCategories: LogEntryCategory[]; 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>( const [expandedCategories, { add: expandCategory, remove: collapseCategory }] = useSet<number>(
new Set() new Set()
); );
@ -80,6 +96,8 @@ export const TopCategoriesTable = euiStyled(
itemId="categoryId" itemId="categoryId"
items={topCategories} items={topCategories}
rowProps={{ className: `${className} euiTableRow--topAligned` }} rowProps={{ className: `${className} euiTableRow--topAligned` }}
onChange={handleTableChange}
sorting={tableSortOptions}
/> />
); );
} }
@ -102,6 +120,7 @@ const createColumns = (
name: i18n.translate('xpack.infra.logs.logEntryCategories.countColumnTitle', { name: i18n.translate('xpack.infra.logs.logEntryCategories.countColumnTitle', {
defaultMessage: 'Message count', defaultMessage: 'Message count',
}), }),
sortable: true,
render: (logEntryCount: number) => { render: (logEntryCount: number) => {
return numeral(logEntryCount).format('0,0'); return numeral(logEntryCount).format('0,0');
}, },
@ -147,6 +166,7 @@ const createColumns = (
name: i18n.translate('xpack.infra.logs.logEntryCategories.maximumAnomalyScoreColumnTitle', { name: i18n.translate('xpack.infra.logs.logEntryCategories.maximumAnomalyScoreColumnTitle', {
defaultMessage: 'Maximum anomaly score', defaultMessage: 'Maximum anomaly score',
}), }),
sortable: true,
render: (_maximumAnomalyScore: number, item) => ( render: (_maximumAnomalyScore: number, item) => (
<AnomalySeverityIndicatorList datasets={item.datasets} /> <AnomalySeverityIndicatorList datasets={item.datasets} />
), ),

View file

@ -10,6 +10,7 @@ import {
getLogEntryCategoriesRequestPayloadRT, getLogEntryCategoriesRequestPayloadRT,
getLogEntryCategoriesSuccessReponsePayloadRT, getLogEntryCategoriesSuccessReponsePayloadRT,
LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH,
CategorySort,
} from '../../../../../common/http_api/log_analysis'; } from '../../../../../common/http_api/log_analysis';
import { decodeOrThrow } from '../../../../../common/runtime_types'; import { decodeOrThrow } from '../../../../../common/runtime_types';
@ -19,13 +20,14 @@ interface RequestArgs {
endTime: number; endTime: number;
categoryCount: number; categoryCount: number;
datasets?: string[]; datasets?: string[];
sort: CategorySort;
} }
export const callGetTopLogEntryCategoriesAPI = async ( export const callGetTopLogEntryCategoriesAPI = async (
requestArgs: RequestArgs, requestArgs: RequestArgs,
fetch: HttpHandler fetch: HttpHandler
) => { ) => {
const { sourceId, startTime, endTime, categoryCount, datasets } = requestArgs; const { sourceId, startTime, endTime, categoryCount, datasets, sort } = requestArgs;
const intervalDuration = endTime - startTime; const intervalDuration = endTime - startTime;
const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, { const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, {
@ -58,6 +60,7 @@ export const callGetTopLogEntryCategoriesAPI = async (
bucketCount: 1, bucketCount: 1,
}, },
], ],
sort,
}, },
}) })
), ),

View file

@ -9,6 +9,7 @@ import { useMemo, useState } from 'react';
import { import {
GetLogEntryCategoriesSuccessResponsePayload, GetLogEntryCategoriesSuccessResponsePayload,
GetLogEntryCategoryDatasetsSuccessResponsePayload, GetLogEntryCategoryDatasetsSuccessResponsePayload,
CategorySort,
} from '../../../../common/http_api/log_analysis'; } from '../../../../common/http_api/log_analysis';
import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise'; import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise';
import { callGetTopLogEntryCategoriesAPI } from './service_calls/get_top_log_entry_categories'; import { callGetTopLogEntryCategoriesAPI } from './service_calls/get_top_log_entry_categories';
@ -18,6 +19,9 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
type TopLogEntryCategories = GetLogEntryCategoriesSuccessResponsePayload['data']['categories']; type TopLogEntryCategories = GetLogEntryCategoriesSuccessResponsePayload['data']['categories'];
type LogEntryCategoryDatasets = GetLogEntryCategoryDatasetsSuccessResponsePayload['data']['datasets']; type LogEntryCategoryDatasets = GetLogEntryCategoryDatasetsSuccessResponsePayload['data']['datasets'];
export type SortOptions = CategorySort;
export type ChangeSortOptions = (sortOptions: CategorySort) => void;
export const useLogEntryCategoriesResults = ({ export const useLogEntryCategoriesResults = ({
categoriesCount, categoriesCount,
filteredDatasets: filteredDatasets, filteredDatasets: filteredDatasets,
@ -35,6 +39,10 @@ export const useLogEntryCategoriesResults = ({
sourceId: string; sourceId: string;
startTime: number; startTime: number;
}) => { }) => {
const [sortOptions, setSortOptions] = useState<SortOptions>({
field: 'maximumAnomalyScore',
direction: 'desc',
});
const { services } = useKibanaContextForPlugin(); const { services } = useKibanaContextForPlugin();
const [topLogEntryCategories, setTopLogEntryCategories] = useState<TopLogEntryCategories>([]); const [topLogEntryCategories, setTopLogEntryCategories] = useState<TopLogEntryCategories>([]);
const [ const [
@ -53,6 +61,7 @@ export const useLogEntryCategoriesResults = ({
endTime, endTime,
categoryCount: categoriesCount, categoryCount: categoriesCount,
datasets: filteredDatasets, datasets: filteredDatasets,
sort: sortOptions,
}, },
services.http.fetch services.http.fetch
); );
@ -70,7 +79,7 @@ export const useLogEntryCategoriesResults = ({
} }
}, },
}, },
[categoriesCount, endTime, filteredDatasets, sourceId, startTime] [categoriesCount, endTime, filteredDatasets, sourceId, startTime, sortOptions]
); );
const [getLogEntryCategoryDatasetsRequest, getLogEntryCategoryDatasets] = useTrackedPromise( const [getLogEntryCategoryDatasetsRequest, getLogEntryCategoryDatasets] = useTrackedPromise(
@ -121,5 +130,7 @@ export const useLogEntryCategoriesResults = ({
isLoadingTopLogEntryCategories, isLoadingTopLogEntryCategories,
logEntryCategoryDatasets, logEntryCategoryDatasets,
topLogEntryCategories, topLogEntryCategories,
sortOptions,
changeSortOptions: setSortOptions,
}; };
}; };

View file

@ -12,6 +12,7 @@ import {
jobCustomSettingsRT, jobCustomSettingsRT,
logEntryCategoriesJobTypes, logEntryCategoriesJobTypes,
} from '../../../common/log_analysis'; } from '../../../common/log_analysis';
import { CategorySort } from '../../../common/http_api/log_analysis';
import { startTracingSpan } from '../../../common/performance_tracing'; import { startTracingSpan } from '../../../common/performance_tracing';
import { decodeOrThrow } from '../../../common/runtime_types'; import { decodeOrThrow } from '../../../common/runtime_types';
import type { MlAnomalyDetectors, MlSystem } from '../../types'; import type { MlAnomalyDetectors, MlSystem } from '../../types';
@ -49,7 +50,8 @@ export async function getTopLogEntryCategories(
endTime: number, endTime: number,
categoryCount: number, categoryCount: number,
datasets: string[], datasets: string[],
histograms: HistogramParameters[] histograms: HistogramParameters[],
sort: CategorySort
) { ) {
const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories'); const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories');
@ -68,7 +70,8 @@ export async function getTopLogEntryCategories(
startTime, startTime,
endTime, endTime,
categoryCount, categoryCount,
datasets datasets,
sort
); );
const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId); const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId);
@ -214,7 +217,8 @@ async function fetchTopLogEntryCategories(
startTime: number, startTime: number,
endTime: number, endTime: number,
categoryCount: number, categoryCount: number,
datasets: string[] datasets: string[],
sort: CategorySort
) { ) {
const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES'); const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES');
@ -225,7 +229,8 @@ async function fetchTopLogEntryCategories(
startTime, startTime,
endTime, endTime,
categoryCount, categoryCount,
datasets datasets,
sort
), ),
[logEntryCategoriesCountJobId] [logEntryCategoriesCountJobId]
) )

View file

@ -14,13 +14,33 @@ import {
createDatasetsFilters, createDatasetsFilters,
} from './common'; } from './common';
import { CategorySort } from '../../../../common/http_api/log_analysis';
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 = ( export const createTopLogEntryCategoriesQuery = (
logEntryCategoriesJobId: string, logEntryCategoriesJobId: string,
startTime: number, startTime: number,
endTime: number, endTime: number,
size: number, size: number,
datasets: string[], datasets: string[],
sortDirection: 'asc' | 'desc' = 'desc' sort: CategorySort
) => ({ ) => ({
...defaultRequestParameters, ...defaultRequestParameters,
body: { body: {
@ -65,7 +85,7 @@ export const createTopLogEntryCategoriesQuery = (
field: 'by_field_value', field: 'by_field_value',
size, size,
order: { order: {
'filter_model_plot>sum_actual': sortDirection, [getAggregationOrderForSortField(sort.field)]: sort.direction,
}, },
}, },
aggs: { aggs: {

View file

@ -33,6 +33,7 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs)
sourceId, sourceId,
timeRange: { startTime, endTime }, timeRange: { startTime, endTime },
datasets, datasets,
sort,
}, },
} = request.body; } = request.body;
@ -51,7 +52,8 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs)
endTime: histogram.timeRange.endTime, endTime: histogram.timeRange.endTime,
id: histogram.id, id: histogram.id,
startTime: histogram.timeRange.startTime, startTime: histogram.timeRange.startTime,
})) })),
sort
); );
return response.ok({ return response.ok({