[Ingest Manager] Add ability to sort to agent configs and package configs (#70676)

* Add sorting params to list endpoints; allow sorting on agent config and package config tables; normalize casing of 'desc' and 'asc'

* Fix es archiver data

* Fix tests
This commit is contained in:
Jen Huang 2020-07-02 23:26:56 -07:00 committed by GitHub
parent 5226ea2112
commit 54348a761e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 83 additions and 49 deletions

View file

@ -5,7 +5,9 @@
*/ */
export interface ListWithKuery { export interface ListWithKuery {
page: number; page?: number;
perPage: number; perPage?: number;
sortField?: string;
sortOrder?: 'desc' | 'asc';
kuery?: string; kuery?: string;
} }

View file

@ -13,6 +13,7 @@ export { useLink } from './use_link';
export { useKibanaLink } from './use_kibana_link'; export { useKibanaLink } from './use_kibana_link';
export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type';
export { usePagination, Pagination } from './use_pagination'; export { usePagination, Pagination } from './use_pagination';
export { useSorting } from './use_sorting';
export { useDebounce } from './use_debounce'; export { useDebounce } from './use_debounce';
export * from './use_request'; export * from './use_request';
export * from './use_input'; export * from './use_input';

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useState } from 'react';
import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table';
export function useSorting<T>(defaultSorting: CriteriaWithPagination<T>['sort']) {
const [sorting, setSorting] = useState<CriteriaWithPagination<T>['sort']>(defaultSorting);
return {
sorting,
setSorting,
};
}

View file

@ -31,7 +31,12 @@ export const StepSelectConfig: React.FunctionComponent<{
data: agentConfigsData, data: agentConfigsData,
error: agentConfigsError, error: agentConfigsError,
isLoading: isAgentConfigsLoading, isLoading: isAgentConfigsLoading,
} = useGetAgentConfigs(); } = useGetAgentConfigs({
page: 1,
perPage: 1000,
sortField: 'name',
sortOrder: 'asc',
});
const agentConfigs = agentConfigsData?.items || []; const agentConfigs = agentConfigsData?.items || [];
const agentConfigsById = agentConfigs.reduce( const agentConfigsById = agentConfigs.reduce(
(acc: { [key: string]: GetAgentConfigsResponseItem }, config) => { (acc: { [key: string]: GetAgentConfigsResponseItem }, config) => {

View file

@ -118,6 +118,7 @@ export const PackageConfigsTable: React.FunctionComponent<Props> = ({
(): EuiInMemoryTableProps<InMemoryPackageConfig>['columns'] => [ (): EuiInMemoryTableProps<InMemoryPackageConfig>['columns'] => [
{ {
field: 'name', field: 'name',
sortable: true,
name: i18n.translate( name: i18n.translate(
'xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle', 'xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle',
{ {
@ -137,6 +138,7 @@ export const PackageConfigsTable: React.FunctionComponent<Props> = ({
}, },
{ {
field: 'packageTitle', field: 'packageTitle',
sortable: true,
name: i18n.translate( name: i18n.translate(
'xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle', 'xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle',
{ {

View file

@ -17,6 +17,7 @@ import {
EuiTableFieldDataColumnType, EuiTableFieldDataColumnType,
EuiTextColor, EuiTextColor,
} from '@elastic/eui'; } from '@elastic/eui';
import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -27,6 +28,7 @@ import {
useCapabilities, useCapabilities,
useGetAgentConfigs, useGetAgentConfigs,
usePagination, usePagination,
useSorting,
useLink, useLink,
useConfig, useConfig,
useUrlParams, useUrlParams,
@ -84,6 +86,10 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
: urlParams.kuery ?? '' : urlParams.kuery ?? ''
); );
const { pagination, pageSizeOptions, setPagination } = usePagination(); const { pagination, pageSizeOptions, setPagination } = usePagination();
const { sorting, setSorting } = useSorting<AgentConfig>({
field: 'updated_at',
direction: 'desc',
});
const history = useHistory(); const history = useHistory();
const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const isCreateAgentConfigFlyoutOpen = 'create' in urlParams;
const setIsCreateAgentConfigFlyoutOpen = useCallback( const setIsCreateAgentConfigFlyoutOpen = useCallback(
@ -106,6 +112,8 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
const { isLoading, data: agentConfigData, sendRequest } = useGetAgentConfigs({ const { isLoading, data: agentConfigData, sendRequest } = useGetAgentConfigs({
page: pagination.currentPage, page: pagination.currentPage,
perPage: pagination.pageSize, perPage: pagination.pageSize,
sortField: sorting?.field,
sortOrder: sorting?.direction,
kuery: search, kuery: search,
}); });
@ -116,6 +124,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
> = [ > = [
{ {
field: 'name', field: 'name',
sortable: true,
name: i18n.translate('xpack.ingestManager.agentConfigList.nameColumnTitle', { name: i18n.translate('xpack.ingestManager.agentConfigList.nameColumnTitle', {
defaultMessage: 'Name', defaultMessage: 'Name',
}), }),
@ -158,6 +167,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
}, },
{ {
field: 'updated_at', field: 'updated_at',
sortable: true,
name: i18n.translate('xpack.ingestManager.agentConfigList.updatedOnColumnTitle', { name: i18n.translate('xpack.ingestManager.agentConfigList.updatedOnColumnTitle', {
defaultMessage: 'Last updated on', defaultMessage: 'Last updated on',
}), }),
@ -240,6 +250,16 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
[createAgentConfigButton] [createAgentConfigButton]
); );
const onTableChange = (criteria: CriteriaWithPagination<AgentConfig>) => {
const newPagination = {
...pagination,
currentPage: criteria.page.index + 1,
pageSize: criteria.page.size,
};
setPagination(newPagination);
setSorting(criteria.sort);
};
return ( return (
<AgentConfigListPageLayout> <AgentConfigListPageLayout>
{isCreateAgentConfigFlyoutOpen ? ( {isCreateAgentConfigFlyoutOpen ? (
@ -276,7 +296,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
</EuiFlexGroup> </EuiFlexGroup>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<EuiBasicTable <EuiBasicTable<AgentConfig>
loading={isLoading} loading={isLoading}
hasActions={true} hasActions={true}
noItemsMessage={ noItemsMessage={
@ -314,14 +334,8 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
totalItemCount: agentConfigData ? agentConfigData.total : 0, totalItemCount: agentConfigData ? agentConfigData.total : 0,
pageSizeOptions, pageSizeOptions,
}} }}
onChange={({ page }: { page: { index: number; size: number } }) => { sorting={{ sort: sorting }}
const newPagination = { onChange={onTableChange}
...pagination,
currentPage: page.index + 1,
pageSize: page.size,
};
setPagination(newPagination);
}}
/> />
</AgentConfigListPageLayout> </AgentConfigListPageLayout>
); );

View file

@ -36,7 +36,10 @@ export const AgentReassignConfigFlyout: React.FunctionComponent<Props> = ({ onCl
agent.config_id agent.config_id
); );
const agentConfigsRequest = useGetAgentConfigs(); const agentConfigsRequest = useGetAgentConfigs({
page: 1,
perPage: 1000,
});
const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : [];
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);

View file

@ -119,8 +119,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
}, },
mappings: { mappings: {
properties: { properties: {
id: { type: 'keyword' }, name: { type: 'keyword' },
name: { type: 'text' },
description: { type: 'text' }, description: { type: 'text' },
namespace: { type: 'keyword' }, namespace: { type: 'keyword' },
is_default: { type: 'boolean' }, is_default: { type: 'boolean' },

View file

@ -143,10 +143,12 @@ class AgentConfigService {
soClient: SavedObjectsClientContract, soClient: SavedObjectsClientContract,
options: ListWithKuery options: ListWithKuery
): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> {
const { page = 1, perPage = 20, kuery } = options; const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options;
const agentConfigs = await soClient.find<AgentConfigSOAttributes>({ const agentConfigs = await soClient.find<AgentConfigSOAttributes>({
type: SAVED_OBJECT_TYPE, type: SAVED_OBJECT_TYPE,
sortField,
sortOrder,
page, page,
perPage, perPage,
// To ensure users don't need to know about SO data structure... // To ensure users don't need to know about SO data structure...
@ -273,7 +275,6 @@ class AgentConfigService {
soClient, soClient,
id, id,
{ {
...oldAgentConfig,
package_configs: uniq( package_configs: uniq(
[...((oldAgentConfig.package_configs || []) as string[])].filter( [...((oldAgentConfig.package_configs || []) as string[])].filter(
(pkgConfigId) => !packageConfigIds.includes(pkgConfigId) (pkgConfigId) => !packageConfigIds.includes(pkgConfigId)

View file

@ -12,20 +12,24 @@ import {
AGENT_TYPE_EPHEMERAL, AGENT_TYPE_EPHEMERAL,
AGENT_POLLING_THRESHOLD_MS, AGENT_POLLING_THRESHOLD_MS,
} from '../../constants'; } from '../../constants';
import { AgentSOAttributes, Agent, AgentEventSOAttributes } from '../../types'; import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types';
import { savedObjectToAgent } from './saved_objects'; import { savedObjectToAgent } from './saved_objects';
import { escapeSearchQueryPhrase } from '../saved_object'; import { escapeSearchQueryPhrase } from '../saved_object';
export async function listAgents( export async function listAgents(
soClient: SavedObjectsClientContract, soClient: SavedObjectsClientContract,
options: { options: ListWithKuery & {
page: number;
perPage: number;
kuery?: string;
showInactive: boolean; showInactive: boolean;
} }
) { ) {
const { page, perPage, kuery, showInactive = false } = options; const {
page = 1,
perPage = 20,
sortField = 'enrolled_at',
sortOrder = 'desc',
kuery,
showInactive = false,
} = options;
const filters = []; const filters = [];
@ -49,10 +53,11 @@ export async function listAgents(
const { saved_objects, total } = await soClient.find<AgentSOAttributes>({ const { saved_objects, total } = await soClient.find<AgentSOAttributes>({
type: AGENT_SAVED_OBJECT_TYPE, type: AGENT_SAVED_OBJECT_TYPE,
sortField,
sortOrder,
page, page,
perPage, perPage,
filter: _joinFilters(filters), filter: _joinFilters(filters),
..._getSortFields(),
}); });
const agents: Agent[] = saved_objects.map(savedObjectToAgent); const agents: Agent[] = saved_objects.map(savedObjectToAgent);
@ -137,23 +142,6 @@ export async function deleteAgent(soClient: SavedObjectsClientContract, agentId:
}); });
} }
function _getSortFields(sortOption?: string) {
switch (sortOption) {
case 'ASC':
return {
sortField: 'enrolled_at',
sortOrder: 'ASC',
};
case 'DESC':
default:
return {
sortField: 'enrolled_at',
sortOrder: 'DESC',
};
}
}
function _joinFilters(filters: string[], operator = 'AND') { function _joinFilters(filters: string[], operator = 'AND') {
return filters.reduce((acc: string | undefined, filter) => { return filters.reduce((acc: string | undefined, filter) => {
if (acc) { if (acc) {

View file

@ -31,7 +31,7 @@ export async function getAgentEvents(
perPage, perPage,
page, page,
sortField: 'timestamp', sortField: 'timestamp',
sortOrder: 'DESC', sortOrder: 'desc',
defaultSearchOperator: 'AND', defaultSearchOperator: 'AND',
search: agentId, search: agentId,
searchFields: ['agent_id'], searchFields: ['agent_id'],

View file

@ -61,7 +61,7 @@ async function getEventsCount(soClient: SavedObjectsClientContract, configId?: s
perPage: 0, perPage: 0,
page: 1, page: 1,
sortField: 'timestamp', sortField: 'timestamp',
sortOrder: 'DESC', sortOrder: 'desc',
defaultSearchOperator: 'AND', defaultSearchOperator: 'AND',
}); });

View file

@ -29,7 +29,7 @@ export async function listEnrollmentApiKeys(
page, page,
perPage, perPage,
sortField: 'created_at', sortField: 'created_at',
sortOrder: 'DESC', sortOrder: 'desc',
filter: filter:
kuery && kuery !== '' kuery && kuery !== ''
? kuery.replace( ? kuery.replace(

View file

@ -145,10 +145,12 @@ class PackageConfigService {
soClient: SavedObjectsClientContract, soClient: SavedObjectsClientContract,
options: ListWithKuery options: ListWithKuery
): Promise<{ items: PackageConfig[]; total: number; page: number; perPage: number }> { ): Promise<{ items: PackageConfig[]; total: number; page: number; perPage: number }> {
const { page = 1, perPage = 20, kuery } = options; const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options;
const packageConfigs = await soClient.find<PackageConfigSOAttributes>({ const packageConfigs = await soClient.find<PackageConfigSOAttributes>({
type: SAVED_OBJECT_TYPE, type: SAVED_OBJECT_TYPE,
sortField,
sortOrder,
page, page,
perPage, perPage,
// To ensure users don't need to know about SO data structure... // To ensure users don't need to know about SO data structure...

View file

@ -6,8 +6,10 @@
import { schema, TypeOf } from '@kbn/config-schema'; import { schema, TypeOf } from '@kbn/config-schema';
export const ListWithKuerySchema = schema.object({ export const ListWithKuerySchema = schema.object({
page: schema.number({ defaultValue: 1 }), page: schema.maybe(schema.number({ defaultValue: 1 })),
perPage: schema.number({ defaultValue: 20 }), perPage: schema.maybe(schema.number({ defaultValue: 20 })),
sortField: schema.maybe(schema.string()),
sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])),
kuery: schema.maybe(schema.string()), kuery: schema.maybe(schema.string()),
}); });

View file

@ -220,8 +220,7 @@
], ],
"revision": 2, "revision": 2,
"updated_at": "2020-05-07T19:34:42.533Z", "updated_at": "2020-05-07T19:34:42.533Z",
"updated_by": "system", "updated_by": "system"
"id": "config1"
} }
} }
} }