[Fleet] support sorting agent list (#135218)

* support sorting agent list

* updated openapi

* updated openapi
This commit is contained in:
Julia Bardi 2022-06-27 18:53:34 +02:00 committed by GitHub
parent 637671a43a
commit 158e4f693d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 235 additions and 14 deletions

View file

@ -1191,6 +1191,29 @@
}
},
"operationId": "get-agents",
"parameters": [
{
"$ref": "#/components/parameters/page_size"
},
{
"$ref": "#/components/parameters/page_index"
},
{
"$ref": "#/components/parameters/kuery"
},
{
"$ref": "#/components/parameters/show_inactive"
},
{
"$ref": "#/components/parameters/show_upgradeable"
},
{
"$ref": "#/components/parameters/sort_field"
},
{
"$ref": "#/components/parameters/sort_order"
}
],
"security": [
{
"basicAuth": []
@ -3314,7 +3337,7 @@
"required": false,
"schema": {
"type": "integer",
"default": 50
"default": 20
}
},
"page_index": {
@ -3333,6 +3356,42 @@
"schema": {
"type": "string"
}
},
"show_inactive": {
"name": "showInactive",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
}
},
"show_upgradeable": {
"name": "showUpgradeable",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
}
},
"sort_field": {
"name": "sortField",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
"sort_order": {
"name": "sortOrder",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"asc",
"desc"
]
}
}
},
"schemas": {

View file

@ -737,6 +737,14 @@ paths:
schema:
$ref: '#/components/schemas/get_agents_response'
operationId: get-agents
parameters:
- $ref: '#/components/parameters/page_size'
- $ref: '#/components/parameters/page_index'
- $ref: '#/components/parameters/kuery'
- $ref: '#/components/parameters/show_inactive'
- $ref: '#/components/parameters/show_upgradeable'
- $ref: '#/components/parameters/sort_field'
- $ref: '#/components/parameters/sort_order'
security:
- basicAuth: []
/agents/bulk_upgrade:
@ -2047,7 +2055,7 @@ components:
required: false
schema:
type: integer
default: 50
default: 20
page_index:
name: page
in: query
@ -2061,6 +2069,33 @@ components:
required: false
schema:
type: string
show_inactive:
name: showInactive
in: query
required: false
schema:
type: boolean
show_upgradeable:
name: showUpgradeable
in: query
required: false
schema:
type: boolean
sort_field:
name: sortField
in: query
required: false
schema:
type: string
sort_order:
name: sortOrder
in: query
required: false
schema:
type: string
enum:
- asc
- desc
schemas:
fleet_setup_response:
title: Fleet Setup response

View file

@ -4,4 +4,4 @@ description: The number of items to return
required: false
schema:
type: integer
default: 50
default: 20

View file

@ -0,0 +1,5 @@
name: showInactive
in: query
required: false
schema:
type: boolean

View file

@ -0,0 +1,5 @@
name: showUpgradeable
in: query
required: false
schema:
type: boolean

View file

@ -0,0 +1,5 @@
name: sortField
in: query
required: false
schema:
type: string

View file

@ -0,0 +1,6 @@
name: sortOrder
in: query
required: false
schema:
type: string
enum: [asc, desc]

View file

@ -9,5 +9,13 @@ get:
schema:
$ref: ../components/schemas/get_agents_response.yaml
operationId: get-agents
parameters:
- $ref: ../components/parameters/page_size.yaml
- $ref: ../components/parameters/page_index.yaml
- $ref: ../components/parameters/kuery.yaml
- $ref: ../components/parameters/show_inactive.yaml
- $ref: ../components/parameters/show_upgradeable.yaml
- $ref: ../components/parameters/sort_field.yaml
- $ref: ../components/parameters/sort_order.yaml
security:
- basicAuth: []

View file

@ -156,4 +156,43 @@ describe('agent_list_page', () => {
utils.getByText('4 agents selected');
});
it('should pass sort parameters on table sort', () => {
act(() => {
fireEvent.click(utils.getByTitle('Last activity'));
});
expect(mockedSendGetAgents).toHaveBeenCalledWith(
expect.objectContaining({
sortField: 'last_checkin',
sortOrder: 'asc',
})
);
});
it('should pass keyword field on table sort on version', () => {
act(() => {
fireEvent.click(utils.getByTitle('Version'));
});
expect(mockedSendGetAgents).toHaveBeenCalledWith(
expect.objectContaining({
sortField: 'local_metadata.elastic.agent.version.keyword',
sortOrder: 'asc',
})
);
});
it('should pass keyword field on table sort on hostname', () => {
act(() => {
fireEvent.click(utils.getByTitle('Host'));
});
expect(mockedSendGetAgents).toHaveBeenCalledWith(
expect.objectContaining({
sortField: 'local_metadata.host.hostname.keyword',
sortOrder: 'asc',
})
);
});
});

View file

@ -83,6 +83,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const [selectedAgents, setSelectedAgents] = useState<Agent[]>([]);
const tableRef = useRef<EuiBasicTable<Agent>>(null);
const { pagination, pageSizeOptions, setPagination } = usePagination();
const [sortField, setSortField] = useState<keyof Agent>('enrolled_at');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const VERSION_FIELD = 'local_metadata.elastic.agent.version';
const HOSTNAME_FIELD = 'local_metadata.host.hostname';
const onSubmitSearch = useCallback(
(newKuery: string) => {
@ -199,6 +203,20 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const [totalAgents, setTotalAgents] = useState(0);
const [totalInactiveAgents, setTotalInactiveAgents] = useState(0);
const getSortFieldForAPI = (field: keyof Agent): string => {
if ([VERSION_FIELD, HOSTNAME_FIELD].includes(field as string)) {
return `${field}.keyword`;
}
return field;
};
const sorting = {
sort: {
field: sortField,
direction: sortOrder,
},
};
// Request to fetch agents and agent status
const currentRequestRef = useRef<number>(0);
const fetchData = useCallback(
@ -214,6 +232,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
page: pagination.currentPage,
perPage: pagination.pageSize,
kuery: kuery && kuery !== '' ? kuery : undefined,
sortField: getSortFieldForAPI(sortField),
sortOrder,
showInactive,
showUpgradeable,
}),
@ -280,6 +300,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
showUpgradeable,
allTags,
notifications.toasts,
sortField,
sortOrder,
]
);
@ -360,7 +382,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const columns = [
{
field: 'local_metadata.host.hostname',
field: HOSTNAME_FIELD,
sortable: true,
name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', {
defaultMessage: 'Host',
}),
@ -373,6 +396,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'active',
sortable: false,
width: '85px',
name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', {
defaultMessage: 'Status',
@ -381,6 +405,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'tags',
sortable: false,
width: '210px',
name: i18n.translate('xpack.fleet.agentList.tagsColumnTitle', {
defaultMessage: 'Tags',
@ -389,6 +414,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'policy_id',
sortable: true,
name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', {
defaultMessage: 'Agent policy',
}),
@ -417,7 +443,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
},
{
field: 'local_metadata.elastic.agent.version',
field: VERSION_FIELD,
sortable: true,
width: '135px',
name: i18n.translate('xpack.fleet.agentList.versionTitle', {
defaultMessage: 'Version',
@ -444,6 +471,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'last_checkin',
sortable: true,
name: i18n.translate('xpack.fleet.agentList.lastCheckinTitle', {
defaultMessage: 'Last activity',
}),
@ -657,14 +685,23 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
return '';
},
}}
onChange={({ page }: { page: { index: number; size: number } }) => {
onChange={({
page,
sort,
}: {
page?: { index: number; size: number };
sort?: { field: keyof Agent; direction: 'asc' | 'desc' };
}) => {
const newPagination = {
...pagination,
currentPage: page.index + 1,
pageSize: page.size,
currentPage: page!.index + 1,
pageSize: page!.size,
};
setPagination(newPagination);
setSortField(sort!.field);
setSortOrder(sort!.direction);
}}
sorting={sorting}
/>
</>
);

View file

@ -128,6 +128,8 @@ export const getAgentsHandler: RequestHandler<
showInactive: request.query.showInactive,
showUpgradeable: request.query.showUpgradeable,
kuery: request.query.kuery,
sortField: request.query.sortField,
sortOrder: request.query.sortOrder,
});
const totalInactive = request.query.showInactive
? await AgentService.countInactiveAgents(esClient, {

View file

@ -203,6 +203,8 @@ export async function getAgentsByKuery(
esClient: ElasticsearchClient,
options: ListWithKuery & {
showInactive: boolean;
sortField?: string;
sortOrder?: 'asc' | 'desc';
}
): Promise<{
agents: Agent[];
@ -213,8 +215,8 @@ export async function getAgentsByKuery(
const {
page = 1,
perPage = 20,
sortField = 'enrolled_at',
sortOrder = 'desc',
sortField = options.sortField ?? 'enrolled_at',
sortOrder = options.sortOrder ?? 'desc',
kuery,
showInactive = false,
showUpgradeable,

View file

@ -18,6 +18,8 @@ export const GetAgentsRequestSchema = {
kuery: schema.maybe(schema.string()),
showInactive: schema.boolean({ defaultValue: false }),
showUpgradeable: schema.boolean({ defaultValue: false }),
sortField: schema.maybe(schema.string()),
sortOrder: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])),
}),
};

View file

@ -71,5 +71,17 @@ export default function ({ getService }: FtrProviderContext) {
const agent = apiResponse.items[0];
expect(agent.access_api_key_id).to.eql('api-key-2');
});
it('should return a 200 when given sort options', async () => {
const { body: apiResponse } = await supertest
.get(`/api/fleet/agents?sortField=last_checkin&sortOrder=desc`)
.expect(200);
expect(apiResponse.items.map((agent: { id: string }) => agent.id)).to.eql([
'agent4',
'agent3',
'agent2',
'agent1',
]);
});
});
}

View file

@ -10,7 +10,8 @@
"type": "PERMANENT",
"local_metadata": {},
"user_provided_metadata": {},
"enrolled_at": "2022-06-21T12:14:25Z"
"enrolled_at": "2022-06-21T12:14:25Z",
"last_checkin": "2022-06-27T12:26:29Z"
}
}
}
@ -27,7 +28,8 @@
"type": "PERMANENT",
"local_metadata": {},
"user_provided_metadata": {},
"enrolled_at": "2022-06-21T12:15:25Z"
"enrolled_at": "2022-06-21T12:15:25Z",
"last_checkin": "2022-06-27T12:27:29Z"
}
}
}
@ -44,7 +46,8 @@
"type": "PERMANENT",
"local_metadata": {},
"user_provided_metadata": {},
"enrolled_at": "2022-06-21T12:16:25Z"
"enrolled_at": "2022-06-21T12:16:25Z",
"last_checkin": "2022-06-27T12:28:29Z"
}
}
}
@ -61,7 +64,8 @@
"type": "PERMANENT",
"local_metadata": {},
"user_provided_metadata": {},
"enrolled_at": "2022-06-21T12:17:25Z"
"enrolled_at": "2022-06-21T12:17:25Z",
"last_checkin": "2022-06-27T12:29:29Z"
}
}
}