forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fleet] Added Export CSV bulk action to Agent list (elastic#196635)
## Summary Closes elastic/ingest-dev#4326 - Added bulk action to Export agents to CSV - Passing selected filters to export selected agents - Passing sort order and direction to match the sort config in the agent list - Columns are hardcoded for now, column selection will come in another issue: elastic/ingest-dev#4325 - Didn't find a way to make the exported column names more readable - Agent policy name is not enriched yet, will do as a follow up after elastic/ingest-dev#4442 is done - Agent status is calculated by the runtime field (script queried from the backend) - We might be able to replace this too with an enriched field like agent policy name Added backport to 8.x, as the feature is hidden behind a feature flag. To verify: - enable feature flag in `kibana.dev.yml`: `xpack.fleet.enableExperimental: ['enableExportCSV']` - use the `create_agents` script to create a few agent docs ``` node scripts/create_agents --count 20 --kibana http://localhost:5601 --status offline,online,inactive,error,updating,unenrolled --inactivityTimeout 36000 ``` - filter the agent list and change sort order - select a few agents / select all - click on `Export X agents as CSV` in bulk actions - click on the toast message to go to Reporting - download the report and compare the results to check that the exported csv has the same number of rows and same sort order as the selected agent list <img width="1324" alt="image" src="https://github.com/user-attachments/assets/ec27f3a2-46ae-4a08-ad42-7c668b50a57e"> <img width="1538" alt="image" src="https://github.com/user-attachments/assets/cfd60ca1-2bd9-483d-941a-366dec2cea64"> <img width="1326" alt="image" src="https://github.com/user-attachments/assets/233ba0d1-1960-4108-aabf-2b95247ec3a7"> <img width="1202" alt="image" src="https://github.com/user-attachments/assets/187de485-4c5c-4a95-9e22-e2b7771be081"> --------- Co-authored-by: kibanamachine <[email protected]>
- Loading branch information
1 parent
0fef3bd
commit 4d451f0
Showing
18 changed files
with
529 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { getSortConfig, removeSOAttributes } from './agent_utils'; | ||
|
||
describe('Agent utils', () => { | ||
it('should get sort config', () => { | ||
const sortConfig = getSortConfig('agent.id', 'asc'); | ||
expect(sortConfig).toEqual([{ 'agent.id': { order: 'asc' } }]); | ||
}); | ||
|
||
it('should get default sort config', () => { | ||
const sortConfig = getSortConfig('enrolled_at', 'desc'); | ||
expect(sortConfig).toEqual([ | ||
{ enrolled_at: { order: 'desc' } }, | ||
{ 'local_metadata.host.hostname.keyword': { order: 'asc' } }, | ||
]); | ||
}); | ||
|
||
it('should remove SO attributes', () => { | ||
const kuery = 'attributes.test AND fleet-agents.test'; | ||
const result = removeSOAttributes(kuery); | ||
expect(result).toEqual('test AND test'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
export function removeSOAttributes(kuery: string): string { | ||
return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); | ||
} | ||
|
||
export function getSortConfig( | ||
sortField: string, | ||
sortOrder: 'asc' | 'desc' | ||
): Array<Record<string, { order: 'asc' | 'desc' }>> { | ||
const isDefaultSort = sortField === 'enrolled_at' && sortOrder === 'desc'; | ||
// if using default sorting (enrolled_at), adding a secondary sort on hostname, so that the results are not changing randomly in case many agents were enrolled at the same time | ||
const secondarySort: Array<Record<string, { order: 'asc' | 'desc' }>> = isDefaultSort | ||
? [{ 'local_metadata.host.hostname.keyword': { order: 'asc' } }] | ||
: []; | ||
return [{ [sortField]: { order: sortOrder } }, ...secondarySort]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
...fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/export_csv.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import type { RenderHookResult } from '@testing-library/react-hooks'; | ||
import { act } from '@testing-library/react-hooks'; | ||
|
||
import { createFleetTestRendererMock } from '../../../../../../mock'; | ||
|
||
import type { Agent } from '../../../../../../../common'; | ||
|
||
import { useExportCSV } from './export_csv'; | ||
|
||
jest.mock('../../../../../../hooks', () => ({ | ||
useGetAgentStatusRuntimeFieldQuery: jest.fn().mockReturnValue({ | ||
data: 'emit("offline")', | ||
isLoading: false, | ||
}), | ||
useKibanaVersion: jest.fn().mockReturnValue('9.0.0'), | ||
useStartServices: jest.fn().mockReturnValue({ | ||
notifications: { | ||
toasts: { | ||
addSuccess: jest.fn(), | ||
addError: jest.fn(), | ||
}, | ||
}, | ||
http: {}, | ||
uiSettings: {}, | ||
}), | ||
})); | ||
|
||
const mockGetDecoratedJobParams = jest.fn().mockImplementation((params) => params); | ||
const mockCreateReportingShareJob = jest.fn().mockResolvedValue({}); | ||
|
||
jest.mock('@kbn/reporting-public', () => ({ | ||
ReportingAPIClient: jest.fn().mockImplementation(() => ({ | ||
getDecoratedJobParams: mockGetDecoratedJobParams, | ||
createReportingShareJob: mockCreateReportingShareJob, | ||
})), | ||
})); | ||
|
||
describe('export_csv', () => { | ||
let result: RenderHookResult<any, any>; | ||
|
||
function render() { | ||
const renderer = createFleetTestRendererMock(); | ||
return renderer.renderHook(() => useExportCSV(true)); | ||
} | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
act(() => { | ||
result = render(); | ||
}); | ||
}); | ||
|
||
it('should generate reporting job for export csv with agent ids', () => { | ||
const agents = [{ id: 'agent1' }, { id: 'agent2' }] as Agent[]; | ||
const sortOptions = { | ||
field: 'agent.id', | ||
direction: 'asc', | ||
}; | ||
|
||
act(() => { | ||
result.result.current.generateReportingJobCSV(agents, sortOptions); | ||
}); | ||
|
||
expect(mockGetDecoratedJobParams.mock.calls[0][0].columns.length).toEqual(6); | ||
expect(mockGetDecoratedJobParams.mock.calls[0][0].searchSource).toEqual( | ||
expect.objectContaining({ | ||
filter: expect.objectContaining({ | ||
query: { | ||
bool: { | ||
minimum_should_match: 1, | ||
should: [ | ||
{ | ||
bool: { | ||
minimum_should_match: 1, | ||
should: [ | ||
{ | ||
match: { | ||
'agent.id': 'agent1', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
bool: { | ||
minimum_should_match: 1, | ||
should: [ | ||
{ | ||
match: { | ||
'agent.id': 'agent2', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}), | ||
index: expect.objectContaining({ | ||
runtimeFieldMap: { | ||
status: { | ||
script: { | ||
source: 'emit("offline")', | ||
}, | ||
type: 'keyword', | ||
}, | ||
}, | ||
}), | ||
sort: [ | ||
{ | ||
'agent.id': { | ||
order: 'asc', | ||
}, | ||
}, | ||
], | ||
}) | ||
); | ||
expect(mockCreateReportingShareJob).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should generate reporting job for export csv with agents query', () => { | ||
const agents = 'policy_id:1 AND status:online'; | ||
|
||
act(() => { | ||
result.result.current.generateReportingJobCSV(agents, undefined); | ||
}); | ||
|
||
expect(mockGetDecoratedJobParams.mock.calls[0][0].columns.length).toEqual(6); | ||
expect(mockGetDecoratedJobParams.mock.calls[0][0].searchSource).toEqual( | ||
expect.objectContaining({ | ||
filter: expect.objectContaining({ | ||
query: { | ||
bool: { | ||
filter: [ | ||
{ | ||
bool: { | ||
minimum_should_match: 1, | ||
should: [ | ||
{ | ||
match: { | ||
policy_id: '1', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
{ | ||
bool: { | ||
minimum_should_match: 1, | ||
should: [ | ||
{ | ||
match: { | ||
status: 'online', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}), | ||
sort: [ | ||
{ | ||
enrolled_at: { | ||
order: 'desc', | ||
}, | ||
}, | ||
{ | ||
'local_metadata.host.hostname.keyword': { | ||
order: 'asc', | ||
}, | ||
}, | ||
], | ||
}) | ||
); | ||
expect(mockCreateReportingShareJob).toHaveBeenCalled(); | ||
}); | ||
}); |
Oops, something went wrong.