[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
>;
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({
@ -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,
}),
rt.partial({
// the datasets to filter for (optional, unfiltered if not present)

View file

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

View file

@ -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[];
@ -29,6 +30,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId: string;
timeRange: TimeRange;
topCategories: LogEntryCategory[];
sortOptions: SortOptions;
changeSortOptions: ChangeSortOptions;
}> = ({
availableDatasets,
hasSetupCapabilities,
@ -41,6 +44,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId,
timeRange,
topCategories,
sortOptions,
changeSortOptions,
}) => {
return (
<>
@ -80,6 +85,8 @@ export const TopCategoriesSection: React.FunctionComponent<{
sourceId={sourceId}
timeRange={timeRange}
topCategories={topCategories}
sortOptions={sortOptions}
changeSortOptions={changeSortOptions}
/>
</LoadingOverlayWrapper>
</>

View file

@ -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';
@ -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(
({
@ -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()
);
@ -80,6 +96,8 @@ export const TopCategoriesTable = euiStyled(
itemId="categoryId"
items={topCategories}
rowProps={{ className: `${className} euiTableRow--topAligned` }}
onChange={handleTableChange}
sorting={tableSortOptions}
/>
);
}
@ -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');
},
@ -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} />
),

View file

@ -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';
@ -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, {
@ -58,6 +60,7 @@ export const callGetTopLogEntryCategoriesAPI = async (
bucketCount: 1,
},
],
sort,
},
})
),

View file

@ -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';
@ -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,
@ -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 [
@ -53,6 +61,7 @@ export const useLogEntryCategoriesResults = ({
endTime,
categoryCount: categoriesCount,
datasets: filteredDatasets,
sort: sortOptions,
},
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(
@ -121,5 +130,7 @@ export const useLogEntryCategoriesResults = ({
isLoadingTopLogEntryCategories,
logEntryCategoryDatasets,
topLogEntryCategories,
sortOptions,
changeSortOptions: setSortOptions,
};
};

View file

@ -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';
@ -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');
@ -68,7 +70,8 @@ export async function getTopLogEntryCategories(
startTime,
endTime,
categoryCount,
datasets
datasets,
sort
);
const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId);
@ -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');
@ -225,7 +229,8 @@ async function fetchTopLogEntryCategories(
startTime,
endTime,
categoryCount,
datasets
datasets,
sort
),
[logEntryCategoriesCountJobId]
)

View file

@ -14,13 +14,33 @@ import {
createDatasetsFilters,
} 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 = (
logEntryCategoriesJobId: string,
startTime: number,
endTime: number,
size: number,
datasets: string[],
sortDirection: 'asc' | 'desc' = 'desc'
sort: CategorySort
) => ({
...defaultRequestParameters,
body: {
@ -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: {

View file

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