[UII] Add status tracking for agentless integrations (#199567)

## Summary

Resolves https://github.com/elastic/ingest-dev/issues/3933. For
deployments that support agentless, integrations with agentless
deployment mode enabled will allow the status of agentless integration
policies to be tracked.

### Key technical changes

- A new field `supports_agentless` was added to package policies. This
field already exists on agent policies. When an agentless integration is
created, `supports_agentless: true` is now added to both the package
policy and its parent agent policy.
- This allows easier filtering for agentless integrations as we avoid
having to retrieve & check against every parent agent policy.
- This also means existing agentless policies do not get this new status
tracking UI, only new ones created after this change. Since agentless is
not yet GA, I think this is okay.
- `/api/fleet/agent_status/data` now takes optional query params
`pkgName` and `pkgVersion`. When both are specified, the API will check
if agent(s) have ingested data for only that package's datastreams.

## UI walkthrough
<details>
<summary>🖼️ Click to show screenshots</summary>

1. **Integration policies** page now shows two tables for integrations
meeting the above condition, one for agentless policies and one for
agent-based policies:


![image](https://github.com/user-attachments/assets/58c6a932-9bda-4229-ba5f-d341bdbd539a)

2. Clicking the status badge in the agentless policies table opens a
flyout with two steps: confirm agentless enrollment and confirm incoming
data:


![image](https://github.com/user-attachments/assets/e19e6ba0-f40d-48a7-a524-0373934ac46a)

3. Confirm agentless enrollment polls for an agent enrolled into that
integration policy's agent policy. If that agent is reporting an
unhealthy status, the integration component UI is shown. This UI is the
same one used on Fleet > Agents > Agent details page and shows all
components reported by that agent:


![image](https://github.com/user-attachments/assets/ce214f7f-4bdd-48e5-a5eb-a1e8fcc7a512)

4. Once a healthy agentless enrollment is established, confirm incoming
data starts polling for data for that integration ingested by that agent
ID in the past 5 minutes:


![image](https://github.com/user-attachments/assets/7f3de40b-3418-4174-b529-e805407949b6)

5. If data could not be retrieved in 5 minutes, an error message shows
while polling continues in the background:


![image](https://github.com/user-attachments/assets/a3fd198e-1570-4357-9b7f-e541a769d33f)

6. If data is retrieved, a success message is shown:


![image](https://github.com/user-attachments/assets/f4e442af-ca60-4448-9bfb-3f244cd03c2d)
</details>

## Testing
Easiest way to test is use the Cloud deployment from this PR. Enable
Beta integrations and navigate to CSPM. Add a CSPM integration using
`Agentless` setup technology. Then you can track the status of the
agentless deployment on the Integrations policies tab.

For local testing, the following is required to simulate agentless
agent:
1. Add the following to kibana.dev.yml:
```
xpack.cloud.id: 'anything-to-pass-cloud-validation-checks'
xpack.fleet.agentless.enabled: true
xpack.fleet.agentless.api.url: 'https://localhost:8443'
xpack.fleet.agentless.api.tls.certificate: './config/certs/ess-client.crt'
xpack.fleet.agentless.api.tls.key: './config/certs/ess-client.key'
xpack.fleet.agentless.api.tls.ca: './config/certs/ca.crt'
```
2. Apply [this
patch](https://gist.github.com/jen-huang/dfc3e02ceb63976ad54bd1f50c524cb4)
to prevent attempt to create agentless pod
3. Enroll a Fleet Server as usual
4. Enable Beta integrations and navigate to CSPM. Add a CSPM integration
using `Agentless` setup technology.
5. Enroll a normal Elastic Agent to the agent policy for that CSPM
integration by using the token from Enrollment tokens

## To-do
- [x] API tests
- [x] Unit UI tests
- [x] Manual Cloud tests
- [x] File docs request
  - https://github.com/elastic/ingest-docs/issues/1466
- [ ] Update troubleshooting guide link once available

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jen Huang 2024-11-26 00:12:14 -08:00 committed by GitHub
parent f2da55a5f4
commit 3188cda4e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2664 additions and 595 deletions

View file

@ -6919,6 +6919,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -7943,6 +7949,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -8736,6 +8748,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -9790,6 +9808,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -10813,6 +10837,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -11607,6 +11637,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -12744,6 +12780,22 @@
]
}
},
{
"in": "query",
"name": "pkgName",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "pkgVersion",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "previewData",
@ -30076,6 +30128,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -30563,6 +30621,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"additionalProperties": false,
@ -30800,6 +30864,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"anyOf": [
@ -31336,6 +31406,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -32038,6 +32114,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -33210,6 +33292,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -33622,6 +33710,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"additionalProperties": false,
@ -34314,6 +34408,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -34808,6 +34908,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"additionalProperties": false,
@ -35044,6 +35150,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"anyOf": [
@ -35579,6 +35691,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},

View file

@ -6919,6 +6919,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -7943,6 +7949,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -8736,6 +8748,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -9790,6 +9808,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -10813,6 +10837,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -11607,6 +11637,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -12744,6 +12780,22 @@
]
}
},
{
"in": "query",
"name": "pkgName",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "pkgVersion",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "previewData",
@ -30076,6 +30128,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -30563,6 +30621,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"additionalProperties": false,
@ -30800,6 +30864,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"anyOf": [
@ -31336,6 +31406,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -32038,6 +32114,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -33210,6 +33292,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -33622,6 +33710,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"additionalProperties": false,
@ -34314,6 +34408,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},
@ -34808,6 +34908,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"additionalProperties": false,
@ -35044,6 +35150,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"vars": {
"additionalProperties": {
"anyOf": [
@ -35579,6 +35691,12 @@
},
"type": "array"
},
"supports_agentless": {
"default": false,
"description": "Indicates whether the package policy belongs to an agentless agent policy.",
"nullable": true,
"type": "boolean"
},
"updated_at": {
"type": "string"
},

View file

@ -9909,6 +9909,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -10618,6 +10623,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -11167,6 +11177,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -11696,6 +11711,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -12404,6 +12424,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -12953,6 +12978,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -13891,6 +13921,16 @@ paths:
type: string
type: array
- type: string
- in: query
name: pkgName
required: false
schema:
type: string
- in: query
name: pkgVersion
required: false
schema:
type: string
- in: query
name: previewData
required: false
@ -25537,6 +25577,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -25865,6 +25910,11 @@ paths:
description: Agent policy IDs where that package policy will be added
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
additionalProperties: false
@ -26015,6 +26065,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
anyOf:
@ -26366,6 +26421,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -26826,6 +26886,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -27325,6 +27390,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -27655,6 +27725,11 @@ paths:
description: Agent policy IDs where that package policy will be added
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
additionalProperties: false
@ -27804,6 +27879,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
anyOf:
@ -28154,6 +28234,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -28931,6 +29016,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -29210,6 +29300,11 @@ paths:
description: Agent policy IDs where that package policy will be added
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
additionalProperties: false

View file

@ -12769,6 +12769,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -13477,6 +13482,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -14025,6 +14035,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -14553,6 +14568,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -15260,6 +15280,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -15808,6 +15833,11 @@ paths:
required:
- id
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -16739,6 +16769,16 @@ paths:
type: string
type: array
- type: string
- in: query
name: pkgName
required: false
schema:
type: string
- in: query
name: pkgVersion
required: false
schema:
type: string
- in: query
name: previewData
required: false
@ -28320,6 +28360,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -28647,6 +28692,11 @@ paths:
description: Agent policy IDs where that package policy will be added
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
additionalProperties: false
@ -28797,6 +28847,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
anyOf:
@ -29148,6 +29203,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -29607,6 +29667,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -30104,6 +30169,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -30433,6 +30503,11 @@ paths:
description: Agent policy IDs where that package policy will be added
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
additionalProperties: false
@ -30582,6 +30657,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
anyOf:
@ -30932,6 +31012,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -31706,6 +31791,11 @@ paths:
items:
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
updated_at:
type: string
updated_by:
@ -31985,6 +32075,11 @@ paths:
description: Agent policy IDs where that package policy will be added
type: string
type: array
supports_agentless:
default: false
description: Indicates whether the package policy belongs to an agentless agent policy.
nullable: true
type: boolean
vars:
additionalProperties:
additionalProperties: false

View file

@ -533,6 +533,7 @@
"revision",
"secret_references",
"secret_references.id",
"supports_agentless",
"updated_at",
"updated_by",
"vars"
@ -715,6 +716,7 @@
"revision",
"secret_references",
"secret_references.id",
"supports_agentless",
"updated_at",
"updated_by",
"vars"

View file

@ -1786,6 +1786,9 @@
}
}
},
"supports_agentless": {
"type": "boolean"
},
"updated_at": {
"type": "date"
},
@ -2374,6 +2377,9 @@
}
}
},
"supports_agentless": {
"type": "boolean"
},
"updated_at": {
"type": "date"
},

View file

@ -108,7 +108,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"fleet-agent-policies": "f57d3b70e4175a19a18f18ee72a379ceec82e1fc",
"fleet-fleet-server-host": "69be15f6b6f2a2875ad3c7050ddea7a87f505417",
"fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f",
"fleet-package-policies": "8be2cabfed89e103e0d413f2900e9cf6cd31bc68",
"fleet-package-policies": "0206c20f27286787b91814a2e7872f06dc1e8e47",
"fleet-preconfiguration-deletion-record": "c52ea1e13c919afe8a5e8e3adbb7080980ecc08e",
"fleet-proxy": "6cb688f0d2dd856400c1dbc998b28704ff70363d",
"fleet-setup-lock": "0dc784792c79b5af5a6e6b5dcac06b0dbaa90bde",
@ -124,7 +124,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-agent-policies": "5e95e539826a40ad08fd0c1d161da0a4d86ffc6d",
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-outputs": "55988d5f778bbe0e76caa7e6468707a0a056bdd8",
"ingest-package-policies": "dfa7b1045a2667a822181f40f012786724492439",
"ingest-package-policies": "60d43f475f91417d14d9df05476acf2e63e99435",
"ingest_manager_settings": "111a616eb72627c002029c19feb9e6c439a10505",
"inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83",
"kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad",

View file

@ -72,6 +72,7 @@ export const PACKAGE_POLICIES_MAPPINGS = {
properties: {},
},
secret_references: { properties: { id: { type: 'keyword' } } },
supports_agentless: { type: 'boolean' },
revision: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },

View file

@ -238,6 +238,7 @@ Object {
"policy_ids": Array [
"policy123",
],
"supports_agentless": undefined,
"vars": undefined,
}
`;

View file

@ -49,6 +49,7 @@ export interface SimplifiedPackagePolicy {
description?: string;
vars?: SimplifiedVars;
inputs?: SimplifiedInputs;
supports_agentless?: boolean | null;
}
export interface FormattedPackagePolicy extends Omit<PackagePolicy, 'inputs' | 'vars'> {
@ -154,18 +155,19 @@ export function simplifiedPackagePolicytoNewPackagePolicy(
description,
inputs = {},
vars: packageLevelVars,
supports_agentless: supportsAgentless,
} = data;
const packagePolicy = packageToPackagePolicy(
packageInfo,
policyId && isEmpty(policyIds) ? policyId : policyIds,
namespace,
name,
description
);
if (outputId) {
packagePolicy.output_id = outputId;
}
const packagePolicy = {
...packageToPackagePolicy(
packageInfo,
policyId && isEmpty(policyIds) ? policyId : policyIds,
namespace,
name,
description
),
supports_agentless: supportsAgentless,
output_id: outputId,
};
if (packagePolicy.package && options?.experimental_data_stream_features) {
packagePolicy.package.experimental_data_stream_features =

View file

@ -93,6 +93,7 @@ export interface NewPackagePolicy {
[key: string]: any;
};
overrides?: { inputs?: { [key: string]: any } } | null;
supports_agentless?: boolean | null;
}
export interface UpdatePackagePolicy extends NewPackagePolicy {

View file

@ -220,6 +220,8 @@ export interface GetAgentStatusResponse {
export interface GetAgentIncomingDataRequest {
query: {
agentsIds: string[];
pkgName?: string;
pkgVersion?: string;
previewData?: boolean;
};
}

View file

@ -131,11 +131,11 @@ export const ConfirmIncomingDataWithPreview: React.FunctionComponent<Props> = ({
setAgentDataConfirmed,
troubleshootLink,
}) => {
const { incomingData, dataPreview, isLoading, hasReachedTimeout } = usePollingIncomingData(
const { incomingData, dataPreview, isLoading, hasReachedTimeout } = usePollingIncomingData({
agentIds,
true,
MAX_AGENT_DATA_PREVIEW_COUNT
);
previewData: true,
stopPollingAfterPreviewLength: MAX_AGENT_DATA_PREVIEW_COUNT,
});
const { enrolledAgents, numAgentsWithData } = useGetAgentIncomingData(incomingData, packageInfo);
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(packageInfo?.name);

View file

@ -115,6 +115,7 @@ describe('useAgentless', () => {
describe('useSetupTechnology', () => {
const setNewAgentPolicy = jest.fn();
const updateAgentPoliciesMock = jest.fn();
const updatePackagePolicyMock = jest.fn();
const setSelectedPolicyTabMock = jest.fn();
const newAgentPolicyMock = {
name: 'mock_new_agent_policy',
@ -183,6 +184,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -212,6 +214,7 @@ describe('useSetupTechnology', () => {
packagePolicy: packagePolicyMock,
isEditPage: true,
agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any],
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -239,6 +242,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -248,6 +252,7 @@ describe('useSetupTechnology', () => {
result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS);
});
await waitFor(() => {
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true });
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
expect(setNewAgentPolicy).toHaveBeenCalledWith({
name: 'Agentless policy for endpoint-1',
@ -278,6 +283,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
};
const { result, rerender } = renderHook((props = initialProps) => useSetupTechnology(props), {
@ -291,6 +297,7 @@ describe('useSetupTechnology', () => {
});
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true });
expect(setNewAgentPolicy).toHaveBeenCalledWith({
inactivity_timeout: 3600,
name: 'Agentless policy for endpoint-1',
@ -306,9 +313,11 @@ describe('useSetupTechnology', () => {
...packagePolicyMock,
name: 'endpoint-2',
},
updatePackagePolicy: updatePackagePolicyMock,
});
await waitFor(() => {
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
expect(setNewAgentPolicy).toHaveBeenCalledWith({
name: 'Agentless policy for endpoint-2',
inactivity_timeout: 3600,
@ -332,6 +341,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -366,6 +376,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -376,12 +387,14 @@ describe('useSetupTechnology', () => {
});
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true });
act(() => {
result.current.handleSetupTechnologyChange(SetupTechnology.AGENT_BASED);
});
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: false });
await waitFor(() => {
expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock);
@ -397,6 +410,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -410,6 +424,7 @@ describe('useSetupTechnology', () => {
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
expect(updatePackagePolicyMock).not.toHaveBeenCalled();
expect(setNewAgentPolicy).not.toHaveBeenCalled();
expect(setSelectedPolicyTabMock).not.toHaveBeenCalled();
});
@ -435,6 +450,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -454,6 +470,7 @@ describe('useSetupTechnology', () => {
supports_agentless: true,
inactivity_timeout: 3600,
});
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true });
});
act(() => {
@ -463,6 +480,7 @@ describe('useSetupTechnology', () => {
await waitFor(() => {
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock);
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: false });
});
});
@ -489,6 +507,7 @@ describe('useSetupTechnology', () => {
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -533,6 +552,7 @@ describe('useSetupTechnology', () => {
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -582,6 +602,7 @@ describe('useSetupTechnology', () => {
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -627,6 +648,7 @@ describe('useSetupTechnology', () => {
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
@ -673,6 +695,7 @@ describe('useSetupTechnology', () => {
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);

View file

@ -61,6 +61,7 @@ export function useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy,
updateAgentPolicies,
updatePackagePolicy,
setSelectedPolicyTab,
packageInfo,
packagePolicy,
@ -70,6 +71,7 @@ export function useSetupTechnology({
setNewAgentPolicy: (policy: NewAgentPolicy) => void;
newAgentPolicy: NewAgentPolicy;
updateAgentPolicies: (policies: AgentPolicy[]) => void;
updatePackagePolicy: (policy: Partial<NewPackagePolicy>) => void;
setSelectedPolicyTab: (tab: SelectedPolicyTab) => void;
packageInfo?: PackageInfo;
packagePolicy: NewPackagePolicy;
@ -112,16 +114,33 @@ export function useSetupTechnology({
updateAgentPolicies([nextNewAgentlessPolicy] as AgentPolicy[]);
}
}
if (
selectedSetupTechnology === SetupTechnology.AGENTLESS &&
!packagePolicy.supports_agentless
) {
updatePackagePolicy({
supports_agentless: true,
});
} else if (
selectedSetupTechnology !== SetupTechnology.AGENTLESS &&
packagePolicy.supports_agentless
) {
updatePackagePolicy({
supports_agentless: false,
});
}
}, [
isAgentlessEnabled,
isEditPage,
newAgentlessPolicy,
packagePolicy.name,
packagePolicy.supports_agentless,
selectedSetupTechnology,
updateAgentPolicies,
setNewAgentPolicy,
agentPolicies,
setSelectedSetupTechnology,
updatePackagePolicy,
]);
const handleSetupTechnologyChange = useCallback(
@ -142,11 +161,17 @@ export function useSetupTechnology({
setSelectedPolicyTab(SelectedPolicyTab.NEW);
updateAgentPolicies([agentlessPolicy] as AgentPolicy[]);
}
updatePackagePolicy({
supports_agentless: true,
});
} else if (setupTechnology === SetupTechnology.AGENT_BASED) {
setNewAgentPolicy({
...newAgentBasedPolicy.current,
supports_agentless: false,
});
updatePackagePolicy({
supports_agentless: false,
});
setSelectedPolicyTab(SelectedPolicyTab.NEW);
updateAgentPolicies([newAgentBasedPolicy.current] as AgentPolicy[]);
}
@ -155,11 +180,12 @@ export function useSetupTechnology({
[
isAgentlessEnabled,
selectedSetupTechnology,
updatePackagePolicy,
setNewAgentPolicy,
newAgentlessPolicy,
packageInfo,
setSelectedPolicyTab,
updateAgentPolicies,
packageInfo,
]
);

View file

@ -355,6 +355,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
newAgentPolicy,
setNewAgentPolicy,
updateAgentPolicies,
updatePackagePolicy,
setSelectedPolicyTab,
packageInfo,
packagePolicy,

View file

@ -134,6 +134,7 @@ export function usePackagePolicySteps({
newAgentPolicy,
setNewAgentPolicy,
updateAgentPolicies,
updatePackagePolicy,
setSelectedPolicyTab,
packagePolicy,
isEditPage: true,

View file

@ -97,114 +97,122 @@ export const AgentDetailsIntegration: React.FunctionComponent<{
agent: Agent;
agentPolicy: AgentPolicy;
packagePolicy: PackagePolicy;
linkToLogs: boolean;
'data-test-subj'?: string;
}> = memo(({ agent, agentPolicy, packagePolicy, 'data-test-subj': dataTestSubj }) => {
const { getHref } = useLink();
const theme = useEuiTheme();
}> = memo(
({ agent, agentPolicy, packagePolicy, linkToLogs = true, 'data-test-subj': dataTestSubj }) => {
const { getHref } = useLink();
const theme = useEuiTheme();
const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] =
useState(false);
const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] =
useState(false);
const policyResponseExtensionView = useUIExtension(
packagePolicy.package?.name ?? '',
'package-policy-response'
);
const policyResponseExtensionViewWrapper = useMemo(() => {
return (
policyResponseExtensionView && (
<ExtensionWrapper>
<policyResponseExtensionView.Component
agent={agent}
onShowNeedsAttentionBadge={setIsAttentionBadgeNeededForPolicyResponse}
/>
</ExtensionWrapper>
)
const policyResponseExtensionView = useUIExtension(
packagePolicy.package?.name ?? '',
'package-policy-response'
);
}, [agent, policyResponseExtensionView]);
const packageErrors = useMemo(() => {
if (!agent.components) {
return [];
}
return getInputUnitsByPackage(agent.components, packagePolicy).filter(
(u) => u.status === 'DEGRADED' || u.status === 'FAILED'
);
}, [agent.components, packagePolicy]);
const policyResponseExtensionViewWrapper = useMemo(() => {
return (
policyResponseExtensionView && (
<ExtensionWrapper>
<policyResponseExtensionView.Component
agent={agent}
onShowNeedsAttentionBadge={setIsAttentionBadgeNeededForPolicyResponse}
/>
</ExtensionWrapper>
)
);
}, [agent, policyResponseExtensionView]);
const showNeedsAttentionBadge = isAttentionBadgeNeededForPolicyResponse || !!packageErrors.length;
const genericErrorsListExtensionView = useUIExtension(
packagePolicy.package?.name ?? '',
'package-generic-errors-list'
);
const genericErrorsListExtensionViewWrapper = useMemo(() => {
return (
genericErrorsListExtensionView && (
<ExtensionWrapper>
<genericErrorsListExtensionView.Component packageErrors={packageErrors} />
</ExtensionWrapper>
)
);
}, [packageErrors, genericErrorsListExtensionView]);
return (
<CollapsablePanel
id={packagePolicy.id}
data-test-subj={dataTestSubj}
title={
<EuiTitle size="xs">
<h3>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
{packagePolicy.package ? (
<PackageIcon
packageName={packagePolicy.package.name}
version={packagePolicy.package.version}
size="l"
tryApi={true}
/>
) : (
<PackageIcon size="l" packageName="default" version="0" />
)}
</EuiFlexItem>
<EuiFlexItem className="eui-textTruncate">
<EuiLink
className="eui-textTruncate"
data-test-subj="agentPolicyDetailsLink"
href={getHref('edit_integration', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
})}
>
{packagePolicy.name}
</EuiLink>
</EuiFlexItem>
{showNeedsAttentionBadge && (
<EuiFlexItem grow={false}>
<EuiBadge
color={theme.euiTheme.colors.danger}
iconType="warning"
iconSide="left"
data-test-subj={dataTestSubj ? `${dataTestSubj}-needsAttention` : undefined}
>
<FormattedMessage
id="xpack.fleet.agentDetailsIntegrations.needsAttention.label"
defaultMessage="Needs attention"
/>
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
</h3>
</EuiTitle>
const packageErrors = useMemo(() => {
if (!agent.components) {
return [];
}
>
<AgentDetailsIntegrationInputs agent={agent} packagePolicy={packagePolicy} />
{policyResponseExtensionViewWrapper}
{genericErrorsListExtensionViewWrapper}
<EuiSpacer />
</CollapsablePanel>
);
});
return getInputUnitsByPackage(agent.components, packagePolicy).filter(
(u) => u.status === 'DEGRADED' || u.status === 'FAILED'
);
}, [agent.components, packagePolicy]);
const showNeedsAttentionBadge =
isAttentionBadgeNeededForPolicyResponse || !!packageErrors.length;
const genericErrorsListExtensionView = useUIExtension(
packagePolicy.package?.name ?? '',
'package-generic-errors-list'
);
const genericErrorsListExtensionViewWrapper = useMemo(() => {
return (
genericErrorsListExtensionView && (
<ExtensionWrapper>
<genericErrorsListExtensionView.Component packageErrors={packageErrors} />
</ExtensionWrapper>
)
);
}, [packageErrors, genericErrorsListExtensionView]);
return (
<CollapsablePanel
id={packagePolicy.id}
data-test-subj={dataTestSubj}
title={
<EuiTitle size="xs">
<h3>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
{packagePolicy.package ? (
<PackageIcon
packageName={packagePolicy.package.name}
version={packagePolicy.package.version}
size="l"
tryApi={true}
/>
) : (
<PackageIcon size="l" packageName="default" version="0" />
)}
</EuiFlexItem>
<EuiFlexItem className="eui-textTruncate">
<EuiLink
className="eui-textTruncate"
data-test-subj="agentPolicyDetailsLink"
href={getHref('edit_integration', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
})}
>
{packagePolicy.name}
</EuiLink>
</EuiFlexItem>
{showNeedsAttentionBadge && (
<EuiFlexItem grow={false}>
<EuiBadge
color={theme.euiTheme.colors.danger}
iconType="warning"
iconSide="left"
data-test-subj={dataTestSubj ? `${dataTestSubj}-needsAttention` : undefined}
>
<FormattedMessage
id="xpack.fleet.agentDetailsIntegrations.needsAttention.label"
defaultMessage="Needs attention"
/>
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
</h3>
</EuiTitle>
}
>
<AgentDetailsIntegrationInputs
agent={agent}
packagePolicy={packagePolicy}
linkToLogs={linkToLogs}
/>
{policyResponseExtensionViewWrapper}
{genericErrorsListExtensionViewWrapper}
<EuiSpacer size="s" />
</CollapsablePanel>
);
}
);

View file

@ -66,8 +66,9 @@ const StyledEuiTreeView = styled(EuiTreeView)`
export const AgentDetailsIntegrationInputs: React.FunctionComponent<{
agent: Agent;
packagePolicy: PackagePolicy;
linkToLogs?: boolean;
'data-test-subj'?: string;
}> = memo(({ agent, packagePolicy, 'data-test-subj': dataTestSubj }) => {
}> = memo(({ agent, packagePolicy, linkToLogs = true, 'data-test-subj': dataTestSubj }) => {
const { getHref } = useLink();
const inputStatusMap = useMemo(
@ -138,21 +139,25 @@ export const AgentDetailsIntegrationInputs: React.FunctionComponent<{
defaultMessage: 'View logs',
})}
>
<StyledEuiLink
href={getHref('agent_details', {
agentId: agent.id,
tabId: 'logs',
logQuery: getLogsQueryByInputType(current.type),
})}
aria-label={i18n.translate(
'xpack.fleet.agentDetailsIntegrations.viewLogsButton',
{
defaultMessage: 'View logs',
}
)}
>
{displayInputType(current.type)}
</StyledEuiLink>
{linkToLogs ? (
<StyledEuiLink
href={getHref('agent_details', {
agentId: agent.id,
tabId: 'logs',
logQuery: getLogsQueryByInputType(current.type),
})}
aria-label={i18n.translate(
'xpack.fleet.agentDetailsIntegrations.viewLogsButton',
{
defaultMessage: 'View logs',
}
)}
>
{displayInputType(current.type)}
</StyledEuiLink>
) : (
<>{displayInputType(current.type)}</>
)}
</EuiToolTip>
),
id: current.type,

View file

@ -15,7 +15,8 @@ import { AgentDetailsIntegration } from './agent_details_integration';
export const AgentDetailsIntegrations: React.FunctionComponent<{
agent: Agent;
agentPolicy?: AgentPolicy;
}> = memo(({ agent, agentPolicy }) => {
linkToLogs?: boolean;
}> = memo(({ agent, agentPolicy, linkToLogs = true }) => {
if (!agentPolicy || !agentPolicy.package_policies) {
return null;
}
@ -31,6 +32,7 @@ export const AgentDetailsIntegrations: React.FunctionComponent<{
agent={agent}
agentPolicy={agentPolicy}
packagePolicy={packagePolicy}
linkToLogs={linkToLogs}
data-test-subj={`${testSubj}-accordion`}
/>
</EuiFlexItem>

View file

@ -8,6 +8,7 @@
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import type { EuiBadgeProps } from '@elastic/eui';
import {
EuiBadge,
EuiButton,
@ -34,67 +35,75 @@ import { useAgentRefresh } from '../agent_details_page/hooks';
import { AgentUpgradeAgentModal } from './agent_upgrade_modal';
interface Props {
type Props = EuiBadgeProps & {
agent: Agent;
fromDetails?: boolean;
}
const Status = {
Healthy: (
<EuiBadge color="success">
<FormattedMessage id="xpack.fleet.agentHealth.healthyStatusText" defaultMessage="Healthy" />
</EuiBadge>
),
Offline: (
<EuiBadge color="default">
<FormattedMessage id="xpack.fleet.agentHealth.offlineStatusText" defaultMessage="Offline" />
</EuiBadge>
),
Inactive: (
<EuiBadge color={euiVars.euiColorDarkShade}>
<FormattedMessage id="xpack.fleet.agentHealth.inactiveStatusText" defaultMessage="Inactive" />
</EuiBadge>
),
Unenrolled: (
<EuiBadge color={euiVars.euiColorDisabled}>
<FormattedMessage
id="xpack.fleet.agentHealth.unenrolledStatusText"
defaultMessage="Unenrolled"
/>
</EuiBadge>
),
Unhealthy: (
<EuiBadge color="warning">
<FormattedMessage
id="xpack.fleet.agentHealth.unhealthyStatusText"
defaultMessage="Unhealthy"
/>
</EuiBadge>
),
Updating: (
<EuiBadge color="primary">
<FormattedMessage id="xpack.fleet.agentHealth.updatingStatusText" defaultMessage="Updating" />
</EuiBadge>
),
};
function getStatusComponent(status: Agent['status']): React.ReactElement {
function getStatusComponent({
status,
...restOfProps
}: {
status: Agent['status'];
} & EuiBadgeProps): React.ReactElement {
switch (status) {
case 'error':
case 'degraded':
return Status.Unhealthy;
return (
<EuiBadge color="warning" {...restOfProps}>
<FormattedMessage
id="xpack.fleet.agentHealth.unhealthyStatusText"
defaultMessage="Unhealthy"
/>
</EuiBadge>
);
case 'inactive':
return Status.Inactive;
return (
<EuiBadge color={euiVars.euiColorDarkShade} {...restOfProps}>
<FormattedMessage
id="xpack.fleet.agentHealth.inactiveStatusText"
defaultMessage="Inactive"
/>
</EuiBadge>
);
case 'offline':
return Status.Offline;
return (
<EuiBadge color="default" {...restOfProps}>
<FormattedMessage
id="xpack.fleet.agentHealth.offlineStatusText"
defaultMessage="Offline"
/>
</EuiBadge>
);
case 'unenrolling':
case 'enrolling':
case 'updating':
return Status.Updating;
return (
<EuiBadge color="primary" {...restOfProps}>
<FormattedMessage
id="xpack.fleet.agentHealth.updatingStatusText"
defaultMessage="Updating"
/>
</EuiBadge>
);
case 'unenrolled':
return Status.Unenrolled;
return (
<EuiBadge color={euiVars.euiColorDisabled} {...restOfProps}>
<FormattedMessage
id="xpack.fleet.agentHealth.unenrolledStatusText"
defaultMessage="Unenrolled"
/>
</EuiBadge>
);
default:
return Status.Healthy;
return (
<EuiBadge color="success" {...restOfProps}>
<FormattedMessage
id="xpack.fleet.agentHealth.healthyStatusText"
defaultMessage="Healthy"
/>
</EuiBadge>
);
}
}
@ -102,7 +111,11 @@ const WrappedEuiCallOut = styled(EuiCallOut)`
white-space: wrap !important;
`;
export const AgentHealth: React.FunctionComponent<Props> = ({ agent, fromDetails }) => {
export const AgentHealth: React.FunctionComponent<Props> = ({
agent,
fromDetails,
...restOfProps
}) => {
const { last_checkin: lastCheckIn, last_checkin_message: lastCheckInMessage } = agent;
const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
const lastCheckInMessageText = lastCheckInMessage ? (
@ -172,14 +185,16 @@ export const AgentHealth: React.FunctionComponent<Props> = ({ agent, fromDetails
>
{isStuckInUpdating(agent) && !fromDetails ? (
<div className="eui-textNoWrap">
{getStatusComponent(agent.status)}
{getStatusComponent({ status: agent.status, ...restOfProps })}
&nbsp;
<EuiIcon type="warning" color="warning" />
</div>
) : (
<>
{getStatusComponent(agent.status)}
{previousToOfflineStatus ? getStatusComponent(previousToOfflineStatus) : null}
{getStatusComponent({ status: agent.status, ...restOfProps })}
{previousToOfflineStatus
? getStatusComponent({ status: previousToOfflineStatus, ...restOfProps })
: null}
</>
)}
</EuiToolTip>

View file

@ -846,7 +846,7 @@ export function Detail() {
</Route>
<Route path={INTEGRATIONS_ROUTING_PATHS.integration_details_policies}>
{canReadIntegrationPolicies ? (
<PackagePoliciesPage name={packageInfo.name} version={packageInfo.version} />
<PackagePoliciesPage packageInfo={packageInfo} />
) : (
<PermissionsError
error="MISSING_PRIVILEGES"

View file

@ -0,0 +1,126 @@
/*
* 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 React from 'react';
import { screen, fireEvent, act } from '@testing-library/react';
import { createIntegrationsTestRendererMock } from '../../../../../../../../mock';
import { AgentBasedPackagePoliciesTable } from './agent_based_table';
const mockPackagePolicies = [
{
agentPolicies: [
{
id: '1',
name: 'Agent Policy 1',
status: 'active' as const,
is_managed: false,
is_protected: false,
updated_at: '2023-01-01T00:00:00Z',
updated_by: 'user',
created_at: '2023-01-01T00:00:00Z',
created_by: 'user',
namespace: 'default',
revision: 1,
monitoring_enabled: [],
},
],
packagePolicy: {
id: 'pkg1',
name: 'Package Policy 1',
package: { name: 'package-name', title: 'Package Title', version: '1.0.0' },
hasUpgrade: true,
inputs: [],
revision: 1,
updated_at: '2023-01-01T00:00:00Z',
updated_by: 'user',
created_at: '2023-01-01T00:00:00Z',
created_by: 'user',
namespace: 'default',
policy_id: 'policy1',
policy_ids: ['policy1'],
enabled: true,
},
rowIndex: 0,
},
];
const mockPagination = {
pagination: { currentPage: 1, pageSize: 10, totalItemCount: 1, pageSizeOptions: [10, 20, 50] },
setPagination: jest.fn(),
pageSizeOptions: [10, 20, 50],
};
describe('AgentBasedPackagePoliciesTable', () => {
it('renders the table with package policies', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(
<AgentBasedPackagePoliciesTable
isLoading={false}
packagePolicies={mockPackagePolicies}
packagePoliciesTotal={1}
refreshPackagePolicies={jest.fn()}
pagination={mockPagination}
/>
);
await act(async () => {
expect(result.getByText('Integration policy')).toBeInTheDocument();
expect(result.getByText('Package Policy 1')).toBeInTheDocument();
expect(result.getByText('v1.0.0')).toBeInTheDocument();
});
});
it('shows loading message when isLoading is true', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(
<AgentBasedPackagePoliciesTable
isLoading={true}
packagePolicies={[]}
packagePoliciesTotal={0}
refreshPackagePolicies={jest.fn()}
pagination={mockPagination}
/>
);
await act(async () => {
expect(result.getByText('Loading integration policies…')).toBeInTheDocument();
});
});
it('shows no policies message when there are no package policies', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(
<AgentBasedPackagePoliciesTable
isLoading={false}
packagePolicies={[]}
packagePoliciesTotal={0}
refreshPackagePolicies={jest.fn()}
pagination={mockPagination}
/>
);
await act(async () => {
expect(result.getByText('No integration policies')).toBeInTheDocument();
});
});
it('opens the agent enrollment flyout when add agent button is clicked', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(
<AgentBasedPackagePoliciesTable
isLoading={false}
packagePolicies={mockPackagePolicies}
packagePoliciesTotal={1}
refreshPackagePolicies={jest.fn()}
pagination={mockPagination}
/>
);
await act(async () => {
fireEvent.click(screen.getByText('Add agent'));
});
expect(result.getByTestId('agentEnrollmentFlyout')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,346 @@
/*
* 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 { stringify, parse } from 'query-string';
import React, { useEffect, useState } from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import {
EuiBasicTable,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react';
import type { AgentPolicy, InMemoryPackagePolicy, PackagePolicy } from '../../../../../../types';
import type { usePagination } from '../../../../../../hooks';
import { useLink, useAuthz, useMultipleAgentPolicies } from '../../../../../../hooks';
import {
AgentEnrollmentFlyout,
MultipleAgentPoliciesSummaryLine,
AgentPolicySummaryLine,
PackagePolicyActionsMenu,
} from '../../../../../../components';
import { Persona } from '../persona';
import { PackagePolicyAgentsCell } from './package_policy_agents_cell';
export const AgentBasedPackagePoliciesTable = ({
isLoading,
packagePolicies,
packagePoliciesTotal,
refreshPackagePolicies,
pagination,
addAgentToPolicyIdFromParams,
showAddAgentHelpForPolicyId,
}: {
isLoading: boolean;
packagePolicies: Array<{
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}>;
packagePoliciesTotal: number;
refreshPackagePolicies: () => void;
pagination: ReturnType<typeof usePagination>;
addAgentToPolicyIdFromParams?: string | null;
showAddAgentHelpForPolicyId?: string | null;
}) => {
const { getHref } = useLink();
const { search } = useLocation();
const history = useHistory();
const [selectedTableIndex, setSelectedTableIndex] = useState<number | undefined>();
const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies();
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies;
const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies;
const canShowMultiplePoliciesCell =
canUseMultipleAgentPolicies && canReadIntegrationPolicies && canReadAgentPolicies;
// Show tour help for adding agents to a policy
const addAgentHelpForPolicyId = packagePolicies.find(({ agentPolicies }) =>
agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId)
)?.packagePolicy?.id;
// Handle the "add agent" link displayed in post-installation toast notifications in the case
// where a user is clicking the link while on the package policies listing page
const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState<string | null>(
addAgentToPolicyIdFromParams || null
);
useEffect(() => {
const unlisten = history.listen((location) => {
const params = new URLSearchParams(location.search);
const addAgentToPolicyId = params.get('addAgentToPolicyId');
if (addAgentToPolicyId) {
setFlyoutOpenForPolicyId(addAgentToPolicyId);
}
});
return () => unlisten();
}, [history]);
const selectedPolicies =
selectedTableIndex !== undefined ? packagePolicies[selectedTableIndex] : undefined;
const selectedAgentPolicies = selectedPolicies?.agentPolicies;
const selectedPackagePolicy = selectedPolicies?.packagePolicy;
const flyoutPolicy = selectedAgentPolicies?.length === 1 ? selectedAgentPolicies[0] : undefined;
return (
<>
<EuiBasicTable<{
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}>
items={packagePolicies || []}
columns={[
{
field: 'packagePolicy.name',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', {
defaultMessage: 'Integration policy',
}),
render(_, { agentPolicies, packagePolicy }) {
return (
<EuiLink
className="eui-textTruncate"
data-test-subj="integrationNameLink"
href={getHref('integration_policy_edit', {
packagePolicyId: packagePolicy.id,
})}
>
{packagePolicy.name}
</EuiLink>
);
},
},
{
field: 'packagePolicy.package.version',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', {
defaultMessage: 'Version',
}),
render(_version, { agentPolicies, packagePolicy }) {
return (
<EuiFlexGroup gutterSize="s" alignItems="center" wrap={true}>
<EuiFlexItem grow={false}>
<EuiText
size="s"
className="eui-textNoWrap"
data-test-subj="packageVersionText"
>
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.packageVersion"
defaultMessage="v{version}"
values={{ version: _version }}
/>
</EuiText>
</EuiFlexItem>
{agentPolicies.length > 0 && packagePolicy.hasUpgrade && (
<EuiFlexItem grow={false}>
<EuiButton
size="s"
minWidth="0"
href={`${getHref('upgrade_package_policy', {
policyId: agentPolicies[0].id,
packagePolicyId: packagePolicy.id,
})}?from=integrations-policy-list`}
data-test-subj="integrationPolicyUpgradeBtn"
isDisabled={!canWriteIntegrationPolicies}
>
<FormattedMessage
id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeButton"
defaultMessage="Upgrade"
/>
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
},
},
{
field: 'packagePolicy.policy_ids',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', {
defaultMessage: 'Agent policies',
}),
truncateText: true,
render(ids, { agentPolicies, packagePolicy }) {
return agentPolicies.length > 0 ? (
canShowMultiplePoliciesCell ? (
<MultipleAgentPoliciesSummaryLine
policies={agentPolicies}
packagePolicyId={packagePolicy.id}
onAgentPoliciesChange={refreshPackagePolicies}
/>
) : (
<AgentPolicySummaryLine policy={agentPolicies[0]} />
)
) : ids.length === 0 ? (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.noAgentPolicies"
defaultMessage="No agent policies"
/>
</EuiText>
) : (
<EuiText color="subdued" size="xs">
<EuiIcon size="m" type="warning" color="warning" />
&nbsp;
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.agentPolicyDeletedWarning"
defaultMessage="Policy not found"
/>
</EuiText>
);
},
},
{
field: 'packagePolicy.updated_by',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', {
defaultMessage: 'Last updated by',
}),
truncateText: true,
render(updatedBy: PackagePolicy['updated_by']) {
return <Persona size="s" name={updatedBy} title={updatedBy} />;
},
},
{
field: 'packagePolicy.updated_at',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', {
defaultMessage: 'Last updated',
}),
truncateText: true,
render(updatedAt: PackagePolicy['updated_at']) {
return (
<span className="eui-textTruncate" title={updatedAt}>
<FormattedRelative value={updatedAt} />
</span>
);
},
},
{
field: '',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', {
defaultMessage: 'Agents',
}),
render({
agentPolicies,
packagePolicy,
rowIndex,
}: {
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}) {
if (agentPolicies.length === 0) {
return (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.noAgents"
defaultMessage="No agents"
/>
</EuiText>
);
}
return (
<PackagePolicyAgentsCell
agentPolicies={agentPolicies}
onAddAgent={() => {
setSelectedTableIndex(rowIndex);
setFlyoutOpenForPolicyId(agentPolicies[0].id);
}}
hasHelpPopover={addAgentHelpForPolicyId === packagePolicy.id}
/>
);
},
},
{
field: '',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', {
defaultMessage: 'Actions',
}),
width: '8ch',
align: 'right',
render({
agentPolicies,
packagePolicy,
}: {
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
}) {
const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies
return (
<PackagePolicyActionsMenu
agentPolicies={agentPolicies}
packagePolicy={packagePolicy}
showAddAgent={true}
upgradePackagePolicyHref={
agentPolicy
? `${getHref('upgrade_package_policy', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
})}?from=integrations-policy-list`
: undefined
}
/>
);
},
},
]}
loading={isLoading}
data-test-subj="integrationPolicyTable"
pagination={{
pageIndex: pagination.pagination.currentPage - 1,
pageSize: pagination.pagination.pageSize,
totalItemCount: packagePoliciesTotal,
pageSizeOptions: pagination.pageSizeOptions,
}}
onChange={({ page }: { page: { index: number; size: number } }) => {
pagination.setPagination({
currentPage: page.index + 1,
pageSize: page.size,
});
}}
noItemsMessage={
isLoading ? (
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.loadingPoliciesMessage"
defaultMessage="Loading integration policies…"
/>
) : (
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.noPoliciesMessage"
defaultMessage="No integration policies"
/>
)
}
/>
{flyoutOpenForPolicyId && selectedAgentPolicies && !isLoading && (
<AgentEnrollmentFlyout
onClose={() => {
setFlyoutOpenForPolicyId(null);
const { addAgentToPolicyId, ...rest } = parse(search);
history.replace({ search: stringify(rest) });
}}
agentPolicy={flyoutPolicy}
selectedAgentPolicies={selectedAgentPolicies}
isIntegrationFlow={true}
installedPackagePolicy={{
name: selectedPackagePolicy?.package?.name || '',
version: selectedPackagePolicy?.package?.version || '',
}}
/>
)}
</>
);
};

View file

@ -0,0 +1,158 @@
/*
* 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 React from 'react';
import { fireEvent, act, waitFor } from '@testing-library/react';
import { AGENTS_PREFIX } from '../../../../../../../../../common/constants';
import { sendGetAgents } from '../../../../../../hooks';
import { createIntegrationsTestRendererMock } from '../../../../../../../../mock';
import { AgentlessPackagePoliciesTable } from './agentless_table';
jest.mock('../../../../../../hooks', () => ({
...jest.requireActual('../../../../../../hooks'),
useConfirmForceInstall: jest.fn(),
sendGetAgents: jest.fn(),
}));
describe('AgentlessPackagePoliciesTable', () => {
const mockSendGetAgents = sendGetAgents as jest.MockedFunction<typeof sendGetAgents>;
beforeEach(() => {
mockSendGetAgents.mockResolvedValue({
data: {
items: [
{
policy_id: 'policy1',
id: 'agent1',
packages: ['package'],
type: 'PERMANENT',
active: true,
enrolled_at: '2023-01-01T00:00:00Z',
local_metadata: {},
status: 'online',
},
],
total: 1,
page: 1,
perPage: 10000,
},
error: null,
});
});
afterEach(() => {
jest.clearAllMocks();
});
const defaultProps = {
isLoading: false,
packagePolicies: [
{
agentPolicies: [
{
id: 'policy1',
name: 'Policy 1',
status: 'active' as const,
is_managed: false,
updated_at: '2023-01-01T00:00:00Z',
updated_by: 'user1',
namespace: 'default',
monitoring_enabled: [],
revision: 1,
is_protected: false,
},
],
packagePolicy: {
id: 'packagePolicy1',
name: 'Package Policy 1',
updated_by: 'user1',
updated_at: '2023-01-01T00:00:00Z',
inputs: [],
policy_id: 'policy1',
namespace: 'default',
enabled: true,
package: {
name: 'package',
title: 'Package',
version: '1.0.0',
},
hasUpgrade: false,
revision: 1,
created_at: '2023-01-01T00:00:00Z',
created_by: 'user1',
policy_ids: ['policy1'],
},
rowIndex: 0,
},
],
packagePoliciesTotal: 1,
refreshPackagePolicies: jest.fn(),
pagination: {
pagination: { currentPage: 1, pageSize: 10 },
setPagination: jest.fn(),
pageSizeOptions: [10, 20, 50],
},
};
it('shows loading message when isLoading is true', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(
<AgentlessPackagePoliciesTable {...defaultProps} packagePolicies={[]} isLoading={true} />
);
await act(async () => {
expect(result.getByText('Loading integration policies…')).toBeInTheDocument();
});
});
it('shows no items message when there are no package policies', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(
<AgentlessPackagePoliciesTable {...defaultProps} packagePolicies={[]} />
);
await act(async () => {
expect(result.getByText('No agentless integration policies')).toBeInTheDocument();
});
});
it('renders the table with package policies', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(<AgentlessPackagePoliciesTable {...defaultProps} />);
await act(async () => {
expect(result.getByText('Package Policy 1')).toBeInTheDocument();
expect(result.getByText('user1')).toBeInTheDocument();
});
});
it('displays agent health status when agents are loaded', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(<AgentlessPackagePoliciesTable {...defaultProps} />);
await waitFor(() => {
expect(mockSendGetAgents).toHaveBeenCalledWith({
perPage: 10000,
kuery: `${AGENTS_PREFIX}.policy_id: "policy1"`,
});
});
expect(await result.findByText('Healthy')).toBeInTheDocument();
});
it('opens flyout when status badge is clicked', async () => {
const renderer = createIntegrationsTestRendererMock();
const result = renderer.render(<AgentlessPackagePoliciesTable {...defaultProps} />);
await waitFor(() => {
expect(mockSendGetAgents).toHaveBeenCalledWith({
perPage: 10000,
kuery: `${AGENTS_PREFIX}.policy_id: "policy1"`,
});
});
await act(async () => {
fireEvent.click(await result.findByText('Healthy'));
});
expect(result.getByText('Confirm agentless enrollment')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,300 @@
/*
* 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 React, { useEffect, useMemo, useState } from 'react';
import type { HorizontalAlignment } from '@elastic/eui';
import { EuiBadge, EuiBasicTable, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react';
import type {
Agent,
AgentPolicy,
InMemoryPackagePolicy,
PackagePolicy,
} from '../../../../../../types';
import { AGENTS_PREFIX, SO_SEARCH_LIMIT } from '../../../../../../../../../common/constants';
import type { usePagination } from '../../../../../../hooks';
import { useLink, sendGetAgents, useAuthz, useStartServices } from '../../../../../../hooks';
import {
Loading,
PackagePolicyActionsMenu,
AgentlessEnrollmentFlyout,
} from '../../../../../../components';
import { Persona } from '../persona';
import { AgentHealth } from '../../../../../../../fleet/sections/agents/components';
const REFRESH_INTERVAL_MS = 30000;
export const AgentlessPackagePoliciesTable = ({
isLoading,
packagePolicies,
packagePoliciesTotal,
refreshPackagePolicies,
pagination,
}: {
isLoading: boolean;
packagePolicies: Array<{
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}>;
packagePoliciesTotal: number;
refreshPackagePolicies: () => void;
pagination: ReturnType<typeof usePagination>;
}) => {
const core = useStartServices();
const { notifications } = core;
const authz = useAuthz();
const { getHref } = useLink();
const [isAgentsLoading, setIsAgentsLoading] = useState<boolean>(false);
const [agentsByPolicyId, setAgentsByPolicyId] = useState<Record<string, Agent>>({});
const canReadAgents = authz.fleet.readAgents;
// Kuery for all agents enrolled into the agent policies associated with the package policies
// We use the first agent policy as agentless package policies have a 1:1 relationship with agent policies
// Maximum # of agent policies is 50, based on the max page size in UI
const agentsKuery = useMemo(() => {
return packagePolicies
.reduce((policyIds, { agentPolicies }) => {
return [...policyIds, ...(agentPolicies[0] ? [agentPolicies[0]?.id] : [])];
}, [] as string[])
.map((policyId) => `${AGENTS_PREFIX}.policy_id: "${policyId}"`)
.join(' or ');
}, [packagePolicies]);
// Fetch agents using above kuery, if the user has access to read agents
// Polls every 30 seconds
useEffect(() => {
const fetchAgents = async () => {
const { data: agentsData, error } = await sendGetAgents({
perPage: SO_SEARCH_LIMIT,
kuery: agentsKuery,
});
setAgentsByPolicyId(
(agentsData?.items || []).reduce((acc, agent) => {
if (agent.policy_id) {
acc[agent.policy_id] = agent;
}
return acc;
}, {} as Record<string, Agent>)
);
if (error) {
notifications.toasts.addError(error, {
title: i18n.translate(
'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusError',
{
defaultMessage: 'Error fetching agentless status information',
}
),
});
}
setIsAgentsLoading(false);
};
if (canReadAgents) {
setIsAgentsLoading(true);
fetchAgents();
const interval = setInterval(() => {
fetchAgents();
}, REFRESH_INTERVAL_MS);
return () => clearInterval(interval);
}
}, [agentsKuery, canReadAgents, notifications.toasts]);
// Flyout state
const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState<string>();
const [flyoutPackagePolicy, setFlyoutPackagePolicy] = useState<PackagePolicy>();
const [flyoutAgentPolicy, setFlyoutAgentPolicy] = useState<AgentPolicy>();
return (
<>
<EuiBasicTable<{
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}>
items={packagePolicies || []}
columns={[
{
field: 'packagePolicy.name',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', {
defaultMessage: 'Integration policy',
}),
render(_, { agentPolicies, packagePolicy }) {
return (
<EuiLink
className="eui-textTruncate"
data-test-subj="agentlessIntegrationNameLink"
href={getHref('integration_policy_edit', {
packagePolicyId: packagePolicy.id,
})}
>
{packagePolicy.name}
</EuiLink>
);
},
},
{
field: 'packagePolicy.updated_by',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', {
defaultMessage: 'Last updated by',
}),
truncateText: true,
render(updatedBy: PackagePolicy['updated_by']) {
return <Persona size="s" name={updatedBy} title={updatedBy} />;
},
},
{
field: 'packagePolicy.updated_at',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', {
defaultMessage: 'Last updated',
}),
truncateText: true,
render(updatedAt: PackagePolicy['updated_at']) {
return (
<span className="eui-textTruncate" title={updatedAt}>
<FormattedRelative value={updatedAt} />
</span>
);
},
},
...(canReadAgents
? [
{
field: '',
name: i18n.translate(
'xpack.fleet.epm.packageDetails.integrationList.agentlessStatus',
{
defaultMessage: 'Status',
}
),
align: 'left' as HorizontalAlignment,
render({
agentPolicies,
packagePolicy,
}: {
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}) {
if (isAgentsLoading) {
return <Loading size="s" />;
}
// Use the first agent policy ID associated with the package policy
// because agentless package policies are only associated with one agent policy
const agentPolicy = agentPolicies[0];
const agent =
(agentPolicy?.id && agentsByPolicyId[agentPolicy.id]) || undefined;
// Status badge click handler
const statusBadgeProps = {
onClick: () => {
setFlyoutOpenForPolicyId(packagePolicy.id);
setFlyoutPackagePolicy(packagePolicy);
setFlyoutAgentPolicy(agentPolicy);
},
'data-test-subj': 'agentlessStatusBadge',
onClickAriaLabel: i18n.translate(
'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusAriaLabel',
{
defaultMessage: 'Open status details',
}
),
};
return agent ? (
<AgentHealth agent={agent} {...statusBadgeProps} />
) : (
<EuiBadge color="default" {...statusBadgeProps}>
<FormattedMessage
id="xpack.fleet.packageDetails.integrationList.pendingAgentlessStatus"
defaultMessage="Pending"
/>
</EuiBadge>
);
},
},
]
: []),
{
field: '',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', {
defaultMessage: 'Actions',
}),
width: '8ch',
align: 'right' as HorizontalAlignment,
render({
agentPolicies,
packagePolicy,
}: {
agentPolicies: AgentPolicy[];
packagePolicy: InMemoryPackagePolicy;
}) {
const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies
return (
<PackagePolicyActionsMenu
agentPolicies={agentPolicies}
packagePolicy={packagePolicy}
showAddAgent={true}
upgradePackagePolicyHref={
agentPolicy
? `${getHref('upgrade_package_policy', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
})}?from=integrations-policy-list`
: undefined
}
/>
);
},
},
]}
loading={isLoading}
data-test-subj="integrationPolicyTable"
pagination={{
pageIndex: pagination.pagination.currentPage - 1,
pageSize: pagination.pagination.pageSize,
totalItemCount: packagePoliciesTotal,
pageSizeOptions: pagination.pageSizeOptions,
}}
onChange={({ page }: { page: { index: number; size: number } }) => {
pagination.setPagination({
currentPage: page.index + 1,
pageSize: page.size,
});
}}
noItemsMessage={
isLoading ? (
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.loadingPoliciesMessage"
defaultMessage="Loading integration policies…"
/>
) : (
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.noAgentlessPoliciesMessage"
defaultMessage="No agentless integration policies"
/>
)
}
/>
{flyoutOpenForPolicyId && flyoutPackagePolicy && (
<AgentlessEnrollmentFlyout
onClose={() => {
setFlyoutOpenForPolicyId(undefined);
setFlyoutPackagePolicy(undefined);
setFlyoutAgentPolicy(undefined);
}}
packagePolicy={flyoutPackagePolicy}
agentPolicy={flyoutAgentPolicy}
/>
)}
</>
);
};

View file

@ -4,80 +4,49 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { stringify, parse } from 'query-string';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Redirect, useLocation, useHistory } from 'react-router-dom';
import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Redirect, useLocation } from 'react-router-dom';
import {
EuiBasicTable,
EuiLink,
EuiAccordion,
EuiFlexGroup,
EuiFlexItem,
EuiNotificationBadge,
EuiPanel,
EuiSpacer,
EuiText,
EuiButton,
EuiIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react';
import { FormattedMessage } from '@kbn/i18n-react';
import { InstallStatus } from '../../../../../types';
import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types';
import type {
AgentPolicy,
GetAgentPoliciesResponseItem,
InMemoryPackagePolicy,
PackageInfo,
PackagePolicy,
} from '../../../../../types';
import {
useLink,
useUrlPagination,
useGetPackageInstallStatus,
AgentPolicyRefreshContext,
useIsPackagePolicyUpgradable,
useAuthz,
useMultipleAgentPolicies,
usePagination,
} from '../../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import {
AgentEnrollmentFlyout,
MultipleAgentPoliciesSummaryLine,
AgentPolicySummaryLine,
PackagePolicyActionsMenu,
} from '../../../../../components';
import { SideBarColumn } from '../../../components/side_bar_column';
import { PackagePolicyAgentsCell } from './components/package_policy_agents_cell';
import { useAgentless } from '../../../../../../fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology';
import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy';
import { Persona } from './persona';
import { AgentBasedPackagePoliciesTable } from './components/agent_based_table';
import { AgentlessPackagePoliciesTable } from './components/agentless_table';
interface PackagePoliciesPanelProps {
name: string;
version: string;
}
interface InMemoryPackagePolicyAndAgentPolicy {
packagePolicy: InMemoryPackagePolicy;
agentPolicies: GetAgentPoliciesResponseItem[];
rowIndex: number;
}
const IntegrationDetailsLink = memo<{
packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy'];
agentPolicies: InMemoryPackagePolicyAndAgentPolicy['agentPolicies'];
}>(({ packagePolicy }) => {
const { getHref } = useLink();
return (
<EuiLink
className="eui-textTruncate"
data-test-subj="integrationNameLink"
href={getHref('integration_policy_edit', {
packagePolicyId: packagePolicy.id,
})}
>
{packagePolicy.name}
</EuiLink>
);
});
export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => {
export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo }) => {
const { name, version } = packageInfo;
const { search } = useLocation();
const history = useHistory();
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
const agentPolicyIdFromParams = useMemo(
const addAgentToPolicyIdFromParams = useMemo(
() => queryParams.get('addAgentToPolicyId'),
[queryParams]
);
@ -85,44 +54,27 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
() => queryParams.get('showAddAgentHelpForPolicyId'),
[queryParams]
);
const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState<string | null>(
agentPolicyIdFromParams
);
const [selectedTableIndex, setSelectedTableIndex] = useState<number | undefined>();
const { getPath, getHref } = useLink();
const { getPath } = useLink();
const getPackageInstallStatus = useGetPackageInstallStatus();
const packageInstallStatus = getPackageInstallStatus(name);
const { pagination, pageSizeOptions, setPagination } = useUrlPagination();
const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies();
const {
data,
isLoading,
resendRequest: refreshPolicies,
} = usePackagePoliciesWithAgentPolicy({
page: pagination.currentPage,
perPage: pagination.pageSize,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`,
});
const { isPackagePolicyUpgradable } = useIsPackagePolicyUpgradable();
const { isAgentlessIntegration } = useAgentless();
const canHaveAgentlessPolicies = useMemo(
() => isAgentlessIntegration(packageInfo),
[isAgentlessIntegration, packageInfo]
);
const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies;
const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies;
const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies;
const packageAndAgentPolicies = useMemo((): Array<{
agentPolicies: GetAgentPoliciesResponseItem[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}> => {
if (!data?.items) {
return [];
}
const newPolicies = data.items.map(({ agentPolicies, packagePolicy }, index) => {
// Helper function to map raw policies data for consumption by the table
const mapPoliciesData = useCallback(
(
{
agentPolicies,
packagePolicy,
}: { agentPolicies: AgentPolicy[]; packagePolicy: PackagePolicy },
index: number
) => {
const hasUpgrade = isPackagePolicyUpgradable(packagePolicy);
return {
agentPolicies,
packagePolicy: {
@ -131,284 +83,204 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
},
rowIndex: index,
};
});
return newPolicies;
}, [data?.items, isPackagePolicyUpgradable]);
const showAddAgentHelpForPackagePolicyId = packageAndAgentPolicies.find(({ agentPolicies }) =>
agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId)
)?.packagePolicy?.id;
// Handle the "add agent" link displayed in post-installation toast notifications in the case
// where a user is clicking the link while on the package policies listing page
useEffect(() => {
const unlisten = history.listen((location) => {
const params = new URLSearchParams(location.search);
const addAgentToPolicyId = params.get('addAgentToPolicyId');
if (addAgentToPolicyId) {
setFlyoutOpenForPolicyId(addAgentToPolicyId);
}
});
return () => unlisten();
}, [history]);
const handleTableOnChange = useCallback(
({ page }: CriteriaWithPagination<InMemoryPackagePolicyAndAgentPolicy>) => {
setPagination({
currentPage: page.index + 1,
pageSize: page.size,
});
},
[setPagination]
);
const canShowMultiplePoliciesCell =
canUseMultipleAgentPolicies && canReadIntegrationPolicies && canReadAgentPolicies;
const columns: Array<EuiTableFieldDataColumnType<InMemoryPackagePolicyAndAgentPolicy>> = useMemo(
() => [
{
field: 'packagePolicy.name',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', {
defaultMessage: 'Integration policy',
}),
render(_, { agentPolicies, packagePolicy }) {
return (
<IntegrationDetailsLink packagePolicy={packagePolicy} agentPolicies={agentPolicies} />
);
},
},
{
field: 'packagePolicy.package.version',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', {
defaultMessage: 'Version',
}),
render(_version, { agentPolicies, packagePolicy }) {
return (
<EuiFlexGroup gutterSize="s" alignItems="center" wrap={true}>
<EuiFlexItem grow={false}>
<EuiText size="s" className="eui-textNoWrap" data-test-subj="packageVersionText">
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.packageVersion"
defaultMessage="v{version}"
values={{ version: _version }}
/>
</EuiText>
</EuiFlexItem>
{agentPolicies.length > 0 && packagePolicy.hasUpgrade && (
<EuiFlexItem grow={false}>
<EuiButton
size="s"
minWidth="0"
href={`${getHref('upgrade_package_policy', {
policyId: agentPolicies[0].id,
packagePolicyId: packagePolicy.id,
})}?from=integrations-policy-list`}
data-test-subj="integrationPolicyUpgradeBtn"
isDisabled={!canWriteIntegrationPolicies}
>
<FormattedMessage
id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeButton"
defaultMessage="Upgrade"
/>
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
},
},
{
field: 'packagePolicy.policy_ids',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', {
defaultMessage: 'Agent policies',
}),
truncateText: true,
render(ids, { agentPolicies, packagePolicy }) {
return agentPolicies.length > 0 ? (
canShowMultiplePoliciesCell ? (
<MultipleAgentPoliciesSummaryLine
policies={agentPolicies}
packagePolicyId={packagePolicy.id}
onAgentPoliciesChange={refreshPolicies}
/>
) : (
<AgentPolicySummaryLine policy={agentPolicies[0]} />
)
) : ids.length === 0 ? (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.noAgentPolicies"
defaultMessage="No agent policies"
/>
</EuiText>
) : (
<EuiText color="subdued" size="xs">
<EuiIcon size="m" type="warning" color="warning" />
&nbsp;
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.agentPolicyDeletedWarning"
defaultMessage="Policy not found"
/>
</EuiText>
);
},
},
{
field: 'packagePolicy.updated_by',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', {
defaultMessage: 'Last updated by',
}),
truncateText: true,
render(updatedBy) {
return <Persona size="s" name={updatedBy} title={updatedBy} />;
},
},
{
field: 'packagePolicy.updated_at',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', {
defaultMessage: 'Last updated',
}),
truncateText: true,
render(updatedAt: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']['updated_at']) {
return (
<span className="eui-textTruncate" title={updatedAt}>
<FormattedRelative value={updatedAt} />
</span>
);
},
},
{
field: '',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', {
defaultMessage: 'Agents',
}),
render({ agentPolicies, packagePolicy, rowIndex }: InMemoryPackagePolicyAndAgentPolicy) {
if (agentPolicies.length === 0) {
return (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.noAgents"
defaultMessage="No agents"
/>
</EuiText>
);
}
return (
<PackagePolicyAgentsCell
agentPolicies={agentPolicies}
onAddAgent={() => {
setSelectedTableIndex(rowIndex);
setFlyoutOpenForPolicyId(agentPolicies[0].id);
}}
hasHelpPopover={showAddAgentHelpForPackagePolicyId === packagePolicy.id}
/>
);
},
},
{
field: '',
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', {
defaultMessage: 'Actions',
}),
width: '8ch',
align: 'right',
render({ agentPolicies, packagePolicy }) {
const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies
return (
<PackagePolicyActionsMenu
agentPolicies={agentPolicies}
packagePolicy={packagePolicy}
showAddAgent={true}
upgradePackagePolicyHref={
agentPolicy
? `${getHref('upgrade_package_policy', {
policyId: agentPolicy.id,
packagePolicyId: packagePolicy.id,
})}?from=integrations-policy-list`
: undefined
}
/>
);
},
},
],
[
getHref,
canWriteIntegrationPolicies,
canShowMultiplePoliciesCell,
showAddAgentHelpForPackagePolicyId,
refreshPolicies,
]
[isPackagePolicyUpgradable]
);
const noItemsMessage = useMemo(() => {
return isLoading ? (
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.loadingPoliciesMessage"
defaultMessage="Loading integration policies…"
/>
) : undefined;
}, [isLoading]);
// States and data for agent-based policies table
// If agentless is not supported or not an agentless integration, skip the
// conditional in the kuery
const {
pagination: agentBasedPagination,
pageSizeOptions: agentBasedPageSizeOptions,
setPagination: agentBasedSetPagination,
} = usePagination();
const [agentBasedPackageAndAgentPolicies, setAgentBasedPackageAndAgentPolicies] = useState<
Array<{
agentPolicies: GetAgentPoliciesResponseItem[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}>
>([]);
const {
data: agentBasedData,
isLoading: agentBasedIsLoading,
resendRequest: refreshAgentBasedPolicies,
} = usePackagePoliciesWithAgentPolicy({
page: agentBasedPagination.currentPage,
perPage: agentBasedPagination.pageSize,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${name}" ${
canHaveAgentlessPolicies
? `AND NOT ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.supports_agentless: true`
: ``
}`,
});
useEffect(() => {
setAgentBasedPackageAndAgentPolicies(
!agentBasedData?.items ? [] : agentBasedData.items.map(mapPoliciesData)
);
}, [agentBasedData, mapPoliciesData]);
const tablePagination = useMemo(() => {
return {
pageIndex: pagination.currentPage - 1,
pageSize: pagination.pageSize,
totalItemCount: data?.total ?? 0,
pageSizeOptions,
};
}, [data?.total, pageSizeOptions, pagination.currentPage, pagination.pageSize]);
// States and data for agentless policies table
// If agentless is not supported or not an agentless integration, this block and
// initial request is unnessary but reduces code complexity
const {
pagination: agentlessPagination,
pageSizeOptions: agentlessPageSizeOptions,
setPagination: agentlessSetPagination,
} = usePagination();
const [agentlessPackageAndAgentPolicies, setAgentlessPackageAndAgentPolicies] = useState<
Array<{
agentPolicies: GetAgentPoliciesResponseItem[];
packagePolicy: InMemoryPackagePolicy;
rowIndex: number;
}>
>([]);
const {
data: agentlessData,
isLoading: agentlessIsLoading,
resendRequest: refreshAgentlessPolicies,
} = usePackagePoliciesWithAgentPolicy({
page: agentlessPagination.currentPage,
perPage: agentlessPagination.pageSize,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${name}" AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.supports_agentless: true`,
});
useEffect(() => {
setAgentlessPackageAndAgentPolicies(
!agentlessData?.items ? [] : agentlessData.items.map(mapPoliciesData)
);
}, [agentlessData, mapPoliciesData]);
// if they arrive at this page and the package is not installed, send them to overview
// this happens if they arrive with a direct url or they uninstall while on this tab
// Check flyoutOpenForPolicyId otherwise right after installing a new integration the flyout won't open
if (packageInstallStatus.status !== InstallStatus.installed && !flyoutOpenForPolicyId) {
// Check `addAgentToPolicyIdFromParams` otherwise right after installing a new integration the flyout won't open
if (packageInstallStatus.status !== InstallStatus.installed && !addAgentToPolicyIdFromParams) {
return (
<Redirect to={getPath('integration_details_overview', { pkgkey: `${name}-${version}` })} />
);
}
const selectedPolicies =
selectedTableIndex !== undefined ? packageAndAgentPolicies[selectedTableIndex] : undefined;
const agentPolicies = selectedPolicies?.agentPolicies;
const packagePolicy = selectedPolicies?.packagePolicy;
const flyoutPolicy = agentPolicies?.length === 1 ? agentPolicies[0] : undefined;
return (
<AgentPolicyRefreshContext.Provider value={{ refresh: refreshPolicies }}>
<AgentPolicyRefreshContext.Provider
value={{
refresh: () => {
refreshAgentBasedPolicies();
refreshAgentlessPolicies();
},
}}
>
<EuiFlexGroup alignItems="flexStart">
<SideBarColumn grow={1} />
<EuiFlexItem grow={7}>
<EuiBasicTable
items={packageAndAgentPolicies || []}
columns={columns}
loading={isLoading}
data-test-subj="integrationPolicyTable"
pagination={tablePagination}
onChange={handleTableOnChange}
noItemsMessage={noItemsMessage}
/>
{!canHaveAgentlessPolicies ? (
<AgentBasedPackagePoliciesTable
isLoading={agentBasedIsLoading}
packagePolicies={agentBasedPackageAndAgentPolicies}
packagePoliciesTotal={agentBasedData?.total ?? 0}
refreshPackagePolicies={refreshAgentBasedPolicies}
pagination={{
pagination: agentBasedPagination,
pageSizeOptions: agentBasedPageSizeOptions,
setPagination: agentBasedSetPagination,
}}
addAgentToPolicyIdFromParams={addAgentToPolicyIdFromParams}
showAddAgentHelpForPolicyId={showAddAgentHelpForPolicyId}
/>
) : (
<>
<EuiAccordion
id="agentBasedAccordion"
initialIsOpen={true}
buttonContent={
<EuiFlexGroup
justifyContent="center"
alignItems="center"
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.agentlessHeader"
defaultMessage="Agentless"
/>
</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge color="subdued" size="m">
<h3>{agentlessData?.total ?? 0}</h3>
</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true} hasShadow={false}>
<AgentlessPackagePoliciesTable
isLoading={agentlessIsLoading}
packagePolicies={agentlessPackageAndAgentPolicies}
packagePoliciesTotal={agentlessData?.total ?? 0}
refreshPackagePolicies={refreshAgentlessPolicies}
pagination={{
pagination: agentlessPagination,
pageSizeOptions: agentlessPageSizeOptions,
setPagination: agentlessSetPagination,
}}
/>
</EuiPanel>
</EuiAccordion>
<EuiSpacer size="l" />
<EuiAccordion
id="agentBasedAccordion"
initialIsOpen={true}
buttonContent={
<EuiFlexGroup
justifyContent="center"
alignItems="center"
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>
<FormattedMessage
id="xpack.fleet.epm.packageDetails.integrationList.agentBasedHeader"
defaultMessage="Agent-based"
/>
</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge color="subdued" size="m">
<h3>{agentBasedData?.total ?? 0}</h3>
</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true} hasShadow={false}>
<AgentBasedPackagePoliciesTable
isLoading={agentBasedIsLoading}
packagePolicies={agentBasedPackageAndAgentPolicies}
packagePoliciesTotal={agentBasedData?.total ?? 0}
refreshPackagePolicies={refreshAgentBasedPolicies}
pagination={{
pagination: agentBasedPagination,
pageSizeOptions: agentBasedPageSizeOptions,
setPagination: agentBasedSetPagination,
}}
addAgentToPolicyIdFromParams={addAgentToPolicyIdFromParams}
showAddAgentHelpForPolicyId={showAddAgentHelpForPolicyId}
/>
</EuiPanel>
</EuiAccordion>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
{flyoutOpenForPolicyId && agentPolicies && !isLoading && (
<AgentEnrollmentFlyout
onClose={() => {
setFlyoutOpenForPolicyId(null);
const { addAgentToPolicyId, ...rest } = parse(search);
history.replace({ search: stringify(rest) });
}}
agentPolicy={flyoutPolicy}
selectedAgentPolicies={agentPolicies}
isIntegrationFlow={true}
installedPackagePolicy={{
name: packagePolicy?.package?.name || '',
version: packagePolicy?.package?.version || '',
}}
/>
)}
</AgentPolicyRefreshContext.Provider>
);
};

View file

@ -30,7 +30,7 @@ export const ConfirmIncomingData: React.FunctionComponent<Props> = ({
setAgentDataConfirmed,
troubleshootLink,
}) => {
const { incomingData, isLoading } = usePollingIncomingData(agentIds);
const { incomingData, isLoading } = usePollingIncomingData({ agentIds });
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(installedPolicy?.name);
const { guidedOnboarding } = useStartServices();

View file

@ -75,18 +75,26 @@ export const useGetAgentIncomingData = (
};
const POLLING_INTERVAL_MS = 5 * 1000; // 5 sec
const POLLING_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
export const POLLING_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
/**
* Hook for polling incoming data for the selected agent policy.
* Hook for polling incoming data for the selected agent(s).
* @param agentIds
* @returns incomingData, isLoading
*/
export const usePollingIncomingData = (
agentIds: string[],
previewData?: boolean,
stopPollingAfterPreviewLength: number = 0
) => {
export const usePollingIncomingData = ({
agentIds,
pkgName,
pkgVersion,
previewData,
stopPollingAfterPreviewLength = 0,
}: {
agentIds: string[];
pkgName?: string;
pkgVersion?: string;
previewData?: boolean;
stopPollingAfterPreviewLength?: number;
}) => {
const timeout = useRef<number | undefined>(undefined);
const [result, setResult] = useState<{
incomingData: IncomingDataList[];
@ -117,7 +125,12 @@ export const usePollingIncomingData = (
setHasReachedTimeout(true);
}
const { data } = await sendGetAgentIncomingData({ agentsIds: agentIds, previewData });
const { data } = await sendGetAgentIncomingData({
agentsIds: agentIds,
previewData,
pkgName,
pkgVersion,
});
if (data?.items) {
// filter out agents that have `data = false` and keep polling
const filtered = data?.items.filter((item) => {
@ -153,7 +166,15 @@ export const usePollingIncomingData = (
return () => {
isAborted = true;
};
}, [agentIds, result, previewData, stopPollingAfterPreviewLength, startedPollingAt]);
}, [
agentIds,
result,
previewData,
stopPollingAfterPreviewLength,
startedPollingAt,
pkgName,
pkgVersion,
]);
return {
...result,

View file

@ -0,0 +1,151 @@
/*
* 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 React from 'react';
import { act, waitFor } from '@testing-library/react';
import { sendGetAgents, useGetPackageInfoByKeyQuery } from '../../hooks';
import { usePollingIncomingData } from '../agent_enrollment_flyout/use_get_agent_incoming_data';
import { createIntegrationsTestRendererMock } from '../../mock';
import { AGENTS_PREFIX } from '../../constants';
import type { PackagePolicy } from '../../types';
import { AgentlessEnrollmentFlyout } from '.';
jest.mock('../../hooks', () => ({
...jest.requireActual('../../hooks'),
useGetPackageInfoByKeyQuery: jest.fn(),
sendGetAgents: jest.fn(),
}));
jest.mock('../agent_enrollment_flyout/use_get_agent_incoming_data', () => ({
usePollingIncomingData: jest.fn(),
}));
const mockSendGetAgents = sendGetAgents as jest.Mock;
const mockUseGetPackageInfoByKeyQuery = useGetPackageInfoByKeyQuery as jest.Mock;
const mockUsePollingIncomingData = usePollingIncomingData as jest.Mock;
describe('AgentlessEnrollmentFlyout', () => {
const onClose = jest.fn();
const packagePolicy: PackagePolicy = {
id: 'test-package-policy-id',
name: 'test-package-policy',
namespace: 'default',
policy_ids: ['test-policy-id'],
policy_id: 'test-policy-id',
enabled: true,
output_id: '',
package: { name: 'test-package', title: 'Test Package', version: '1.0.0' },
inputs: [{ enabled: true, policy_template: 'test-template', type: 'test-type', streams: [] }],
revision: 1,
created_at: '',
created_by: '',
updated_at: '',
updated_by: '',
};
beforeEach(() => {
mockSendGetAgents.mockResolvedValue({ data: { items: [] } });
mockUseGetPackageInfoByKeyQuery.mockReturnValue({ data: { item: { title: 'Test Package' } } });
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders the flyout with initial loading state', async () => {
const renderer = createIntegrationsTestRendererMock();
const { getByText } = renderer.render(
<AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} />
);
await act(async () => {
expect(getByText('Confirm agentless enrollment')).toBeInTheDocument();
expect(getByText('Step 1 is loading')).toBeInTheDocument();
expect(
getByText('Listening for agentless connection... this could take several minutes')
).toBeInTheDocument();
expect(getByText('Confirm incoming data')).toBeInTheDocument();
expect(getByText('Step 2 is disabled')).toBeInTheDocument();
});
});
it('updates step statuses when agent deployment fails', async () => {
const renderer = createIntegrationsTestRendererMock();
const agentData = { status: 'error' };
mockSendGetAgents.mockResolvedValueOnce({ data: { items: [agentData] } });
const { getByText } = renderer.render(
<AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} />
);
await waitFor(() => {
expect(getByText('Confirm agentless enrollment')).toBeInTheDocument();
expect(getByText('Step 1 has errors')).toBeInTheDocument();
expect(getByText('Agentless deployment failed')).toBeInTheDocument();
expect(getByText('Confirm incoming data')).toBeInTheDocument();
expect(getByText('Step 2 is disabled')).toBeInTheDocument();
});
});
it('fetches agents data on mount and sets step statuses when agent deployment succeeds', async () => {
const renderer = createIntegrationsTestRendererMock();
const agentData = { status: 'online' };
mockSendGetAgents.mockResolvedValueOnce({ data: { items: [agentData] } });
mockUsePollingIncomingData.mockReturnValue({ incomingData: [], hasReachedTimeout: false });
const { getByText } = renderer.render(
<AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} />
);
await waitFor(() => {
expect(mockSendGetAgents).toHaveBeenCalledWith({
kuery: `${AGENTS_PREFIX}.policy_id: "test-policy-id"`,
});
expect(getByText('Confirm agentless enrollment')).toBeInTheDocument();
expect(getByText('Step 1 is complete')).toBeInTheDocument();
expect(getByText('Agentless deployment was successful')).toBeInTheDocument();
expect(getByText('Confirm incoming data')).toBeInTheDocument();
expect(getByText('Step 2 is loading')).toBeInTheDocument();
});
});
it('shows confirm data step as failed when timeout has been reached', async () => {
const renderer = createIntegrationsTestRendererMock();
mockSendGetAgents.mockResolvedValueOnce({ data: { items: [{ status: 'online' }] } });
mockUsePollingIncomingData.mockReturnValue({ incomingData: [], hasReachedTimeout: true });
const { getByText } = renderer.render(
<AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} />
);
await waitFor(() => {
expect(getByText('Step 1 is complete')).toBeInTheDocument();
expect(getByText('Confirm incoming data')).toBeInTheDocument();
expect(getByText('Step 2 has errors')).toBeInTheDocument();
expect(getByText('No incoming data received from agentless integration')).toBeInTheDocument();
});
});
it('shows confirm data step as successful when incoming data is received', async () => {
const renderer = createIntegrationsTestRendererMock();
mockSendGetAgents.mockResolvedValueOnce({ data: { items: [{ status: 'online' }] } });
mockUsePollingIncomingData.mockReturnValue({ incomingData: [{ data: 'test-data' }] });
const { getByText } = renderer.render(
<AgentlessEnrollmentFlyout onClose={onClose} packagePolicy={packagePolicy} />
);
await waitFor(() => {
expect(getByText('Step 1 is complete')).toBeInTheDocument();
expect(getByText('Confirm incoming data')).toBeInTheDocument();
expect(getByText('Step 2 is complete')).toBeInTheDocument();
expect(getByText('Incoming data received from agentless integration')).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,212 @@
/*
* 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 React, { useEffect, useMemo, useRef, useState } from 'react';
import type { EuiStepStatus } from '@elastic/eui';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiFlyoutFooter,
EuiSteps,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { AGENTS_PREFIX, MAX_FLYOUT_WIDTH } from '../../constants';
import type { Agent, AgentPolicy, PackagePolicy } from '../../types';
import { sendGetAgents, useStartServices, useGetPackageInfoByKeyQuery } from '../../hooks';
import { AgentlessStepConfirmEnrollment } from './step_confirm_enrollment';
import { AgentlessStepConfirmData } from './step_confirm_data';
const REFRESH_INTERVAL_MS = 30000;
/**
* This component displays additional status details of an agentless agent enrolled
* the chosen package policy (and its agent policy).
* It also displays confirmation that the agentless agent is ingesting data from
* the chosen package policy.
*/
export const AgentlessEnrollmentFlyout = ({
onClose,
packagePolicy,
agentPolicy,
}: {
onClose: () => void;
packagePolicy: PackagePolicy;
agentPolicy?: AgentPolicy;
}) => {
const core = useStartServices();
const { notifications } = core;
const [confirmEnrollmentStatus, setConfirmEnrollmentStatus] = useState<EuiStepStatus>('loading');
const [confirmDataStatus, setConfirmDataStatus] = useState<EuiStepStatus>('disabled');
const [agentData, setAgentData] = useState<Agent>();
// Clear agent data polling
// Called when component is unmounted or when agent is healthy
const agentDataInterval = useRef<NodeJS.Timeout>();
const clearAgentDataPolling = useMemo(() => {
return () => {
if (agentDataInterval.current) {
clearInterval(agentDataInterval.current);
}
};
}, [agentDataInterval]);
// Fetch agent(s) data for the first associated agent policy
// Polls every 30 seconds until agent is found and healthy
useEffect(() => {
const fetchAgents = async () => {
const { data: agentsData, error } = await sendGetAgents({
kuery: `${AGENTS_PREFIX}.policy_id: "${packagePolicy.policy_ids[0]}"`,
});
if (error) {
notifications.toasts.addError(error, {
title: i18n.translate(
'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusError',
{
defaultMessage: 'Error fetching agentless status information',
}
),
});
}
if (agentsData?.items?.[0]) {
setAgentData(agentsData.items?.[0]);
}
};
fetchAgents();
agentDataInterval.current = setInterval(() => {
fetchAgents();
}, REFRESH_INTERVAL_MS);
return () => clearAgentDataPolling();
}, [clearAgentDataPolling, notifications.toasts, packagePolicy.policy_ids]);
// Watches agent data and updates step statuses and clears polling when agent is healthy
useEffect(() => {
if (agentData) {
if (agentData.status === 'online') {
setConfirmEnrollmentStatus('complete');
setConfirmDataStatus('loading');
clearAgentDataPolling();
} else if (agentData.status === 'error' || agentData.status === 'degraded') {
setConfirmEnrollmentStatus('danger');
setConfirmDataStatus('disabled');
} else {
setConfirmEnrollmentStatus('loading');
setConfirmDataStatus('disabled');
}
} else {
setConfirmEnrollmentStatus('loading');
setConfirmDataStatus('disabled');
}
}, [agentData, clearAgentDataPolling]);
// Calculate integration title from the base package info and what
// is configured on the package policy.
const { data: packageInfoData } = useGetPackageInfoByKeyQuery(
packagePolicy.package!.name,
packagePolicy.package!.version,
{
prerelease: true,
}
);
const integrationTitle = useMemo(() => {
if (packageInfoData?.item) {
const enabledInputs = packagePolicy.inputs?.filter((input) => input.enabled);
// If only one input is enabled, find the input name from the package info and
// and use that for integration title. Otherwise, use the package name.
if (enabledInputs.length === 1 && enabledInputs[0].policy_template) {
const policyTemplate = packageInfoData.item.policy_templates?.find(
(template) => template.name === enabledInputs[0].policy_template
);
const input =
policyTemplate && 'inputs' in policyTemplate
? policyTemplate.inputs?.find((i) => i.type === enabledInputs[0].type)
: null;
return input?.title || packageInfoData.item.title;
} else {
return packageInfoData.item.title;
}
}
return packagePolicy.name;
}, [packageInfoData, packagePolicy]);
return (
<EuiFlyout
data-test-subj="agentlessEnrollmentFlyout"
onClose={onClose}
maxWidth={MAX_FLYOUT_WIDTH}
>
<EuiFlyoutHeader hasBorder aria-labelledby="FleetAgentlessEnrollmentFlyoutTitle">
<EuiTitle size="m">
<h2 id="FleetAgentlessEnrollmentFlyoutTitle">{packagePolicy.name}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiSteps
steps={[
{
title: i18n.translate(
'xpack.fleet.agentlessEnrollmentFlyout.stepConfirmEnrollmentTitle',
{
defaultMessage: 'Confirm agentless enrollment',
}
),
children: (
<AgentlessStepConfirmEnrollment
agent={agentData}
agentPolicy={agentPolicy}
integrationTitle={integrationTitle}
/>
),
status: confirmEnrollmentStatus,
},
{
title: i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.stepConfirmDataTitle', {
defaultMessage: 'Confirm incoming data',
}),
children:
agentData && confirmEnrollmentStatus === 'complete' ? (
<AgentlessStepConfirmData
agent={agentData}
packagePolicy={packagePolicy}
setConfirmDataStatus={setConfirmDataStatus}
/>
) : (
<></> // Avoids React error about null children prop
),
status: confirmDataStatus,
},
]}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose}>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.closeFlyoutButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -0,0 +1,99 @@
/*
* 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 React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EuiStepStatus } from '@elastic/eui';
import { EuiText, EuiLink, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { useStartServices } from '../../hooks';
import type { Agent, PackagePolicy } from '../../types';
import {
usePollingIncomingData,
POLLING_TIMEOUT_MS,
} from '../agent_enrollment_flyout/use_get_agent_incoming_data';
export const AgentlessStepConfirmData = ({
agent,
packagePolicy,
setConfirmDataStatus,
}: {
agent: Agent;
packagePolicy: PackagePolicy;
setConfirmDataStatus: (status: EuiStepStatus) => void;
}) => {
const { docLinks } = useStartServices();
const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending');
// Fetch integration data for the given agent and package policy
const { incomingData, hasReachedTimeout } = usePollingIncomingData({
agentIds: [agent.id],
pkgName: packagePolicy.package!.name,
pkgVersion: packagePolicy.package!.version,
});
// Calculate overall UI state from polling data
useEffect(() => {
if (incomingData.length > 0) {
setConfirmDataStatus('complete');
setOverallState('success');
} else if (hasReachedTimeout) {
setConfirmDataStatus('danger');
setOverallState('failure');
} else {
setConfirmDataStatus('loading');
setOverallState('pending');
}
}, [incomingData, hasReachedTimeout, setConfirmDataStatus]);
if (overallState === 'success') {
return (
<EuiCallOut
color="success"
title={i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.confirmData.successText', {
defaultMessage: 'Incoming data received from agentless integration',
})}
iconType="check"
/>
);
} else if (overallState === 'failure') {
return (
<>
<EuiCallOut
color="danger"
title={i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.confirmData.failureText', {
defaultMessage: 'No incoming data received from agentless integration',
})}
iconType="warning"
/>
<EuiSpacer size="m" />
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmData.failureHelperText"
defaultMessage="No integration data receieved in the past {num} minutes. Check out the {troubleshootingGuideLink} for help."
values={{
num: POLLING_TIMEOUT_MS / 1000 / 60,
troubleshootingGuideLink: (
<EuiLink href={docLinks.links.fleet.troubleshooting} target="_blank">
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmData.pendingHelperText.troubleshootingLinkLabel"
defaultMessage="troubleshooting guide"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
</>
);
}
return null;
};

View file

@ -0,0 +1,151 @@
/*
* 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 React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiPanel, EuiText, EuiLink, EuiSpacer, EuiCallOut } from '@elastic/eui';
import type { Agent, AgentPolicy } from '../../types';
import { useStartServices } from '../../hooks';
import { AgentDetailsIntegrations } from '../../applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations';
export const AgentlessStepConfirmEnrollment = ({
agent,
agentPolicy,
integrationTitle,
}: {
agent?: Agent;
agentPolicy?: AgentPolicy;
integrationTitle: string;
}) => {
const { docLinks } = useStartServices();
const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending');
// Calculate overall UI state from agent status
useEffect(() => {
if (agent && agent.status === 'online') {
setOverallState('success');
} else if (agent && (agent.status === 'error' || agent.status === 'degraded')) {
setOverallState('failure');
} else {
setOverallState('pending');
}
}, [agent]);
if (overallState === 'success') {
return (
<>
<EuiCallOut
color="success"
title={i18n.translate(
'xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.successText',
{
defaultMessage: 'Agentless deployment was successful',
}
)}
iconType="check"
/>
<EuiSpacer size="m" />
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.successHelperText"
defaultMessage="{integrationTitle} agentless integration has been successfully established. You can now seamlessly monitor and manage your {integrationTitle} resources without the need for any additional agents."
values={{
integrationTitle,
}}
/>
</p>
</EuiText>
</>
);
} else if (overallState === 'failure') {
return (
<>
<EuiCallOut
color="danger"
title={i18n.translate(
'xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.failureText',
{
defaultMessage: 'Agentless deployment failed',
}
)}
iconType="warning"
>
{agent?.last_checkin_message && <p>{agent.last_checkin_message}</p>}
</EuiCallOut>
<EuiSpacer size="m" />
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.failureHelperText"
defaultMessage="{integrationTitle} agentless integration failed to establish. Check out the {troubleshootingGuideLink} for help."
values={{
integrationTitle,
troubleshootingGuideLink: (
<EuiLink href={docLinks.links.fleet.troubleshooting} target="_blank">
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText.troubleshootingLinkLabel"
defaultMessage="troubleshooting guide"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
{agent && agentPolicy && (
<>
<EuiSpacer size="m" />
<AgentDetailsIntegrations agent={agent} agentPolicy={agentPolicy} linkToLogs={false} />
</>
)}
</>
);
}
return (
<>
<EuiPanel color="subdued" paddingSize="xl" className="eui-textCenter">
<EuiButton disabled={true} size="s" isLoading={true}>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingText"
defaultMessage="Listening for agentless connection... this could take several minutes"
/>
</EuiButton>
</EuiPanel>
<EuiSpacer size="m" />
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText"
defaultMessage="Getting ready to connect with your cloud account and confirm incoming data. If you're having trouble connecting, check out the {troubleshootingGuideLink}. You can track the latest status from {policyPagePath} Status column."
values={{
troubleshootingGuideLink: (
<EuiLink href={docLinks.links.fleet.troubleshooting} target="_blank">
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText.troubleshootingLinkLabel"
defaultMessage="troubleshooting guide"
/>
</EuiLink>
),
policyPagePath: (
<strong>
<FormattedMessage
id="xpack.fleet.agentlessEnrollmentFlyout.confirmEnrollment.pendingHelperText.policyPagePath"
defaultMessage="Integration policies &rarr; Agentless Integrations"
/>
</strong>
),
}}
/>
</p>
</EuiText>
</>
);
};

View file

@ -29,3 +29,4 @@ export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge';
export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour';
export { UninstallCommandFlyout } from './uninstall_command_flyout';
export { MultipleAgentPoliciesSummaryLine } from './multiple_agent_policy_summary_line';
export { AgentlessEnrollmentFlyout } from './agentless_enrollment_flyout';

View file

@ -115,13 +115,12 @@ describe.skip('PackagePolicyActionsMenu', () => {
useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: false });
});
it('Should disable upgrade button if package does not have upgrade', async () => {
it('Should not have upgrade button if package does not have upgrade', async () => {
const agentPolicies = createMockAgentPolicies();
const packagePolicy = createMockPackagePolicy({ hasUpgrade: false });
const { utils } = renderMenu({ agentPolicies, packagePolicy });
await act(async () => {
const upgradeButton = utils.getByTestId('PackagePolicyActionsUpgradeItem');
expect(upgradeButton).toBeDisabled();
expect(utils.queryByTestId('PackagePolicyActionsUpgradeItem')).toBeNull();
});
});

View file

@ -108,24 +108,27 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
defaultMessage="Edit integration"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="PackagePolicyActionsUpgradeItem"
disabled={
!packagePolicy.hasUpgrade ||
!canWriteIntegrationPolicies ||
!upgradePackagePolicyHref ||
agentPolicy?.supports_agentless === true
}
icon="refresh"
href={upgradePackagePolicyHref}
key="packagePolicyUpgrade"
>
<FormattedMessage
id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeActionTitle"
data-test-subj="UpgradeIntegrationPolicy"
defaultMessage="Upgrade integration policy"
/>
</EuiContextMenuItem>,
...(packagePolicy.hasUpgrade
? [
<EuiContextMenuItem
data-test-subj="PackagePolicyActionsUpgradeItem"
disabled={
!canWriteIntegrationPolicies ||
!upgradePackagePolicyHref ||
agentPolicy?.supports_agentless === true
}
icon="refresh"
href={upgradePackagePolicyHref}
key="packagePolicyUpgrade"
>
<FormattedMessage
id="xpack.fleet.policyDetails.packagePoliciesTable.upgradeActionTitle"
data-test-subj="UpgradeIntegrationPolicy"
defaultMessage="Upgrade integration policy"
/>
</EuiContextMenuItem>,
]
: []),
// FIXME: implement Copy package policy action
// <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="packagePolicyCopy">
// <FormattedMessage

View file

@ -46,6 +46,8 @@ import { fetchAndAssignAgentMetrics } from '../../services/agents/agent_metrics'
import { getAgentStatusForAgentPolicy } from '../../services/agents';
import { isAgentInNamespace } from '../../services/spaces/agent_namespaces';
import { getCurrentNamespace } from '../../services/spaces/get_current_namespace';
import { getPackageInfo } from '../../services/epm/packages';
import { generateTemplateIndexPattern } from '../../services/epm/elasticsearch/template/template';
import { buildAgentStatusRuntimeField } from '../../services/agents/build_status_runtime_field';
async function verifyNamespace(agent: Agent, namespace?: string) {
@ -308,17 +310,32 @@ export const getAgentDataHandler: RequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const returnDataPreview = request.query.previewData;
const agentIds = isStringArray(request.query.agentsIds)
const agentsIds = isStringArray(request.query.agentsIds)
? request.query.agentsIds
: [request.query.agentsIds];
const { pkgName, pkgVersion, previewData: returnDataPreview } = request.query;
const { items, dataPreview } = await AgentService.getIncomingDataByAgentsId(
// If a package is specified, get data stream patterns for that package
// and scope incoming data query to that pattern
let dataStreamPattern: string | undefined;
if (pkgName && pkgVersion) {
const packageInfo = await getPackageInfo({
savedObjectsClient: coreContext.savedObjects.client,
prerelease: true,
pkgName,
pkgVersion,
});
dataStreamPattern = (packageInfo.data_streams || [])
.map((ds) => generateTemplateIndexPattern(ds))
.join(',');
}
const { items, dataPreview } = await AgentService.getIncomingDataByAgentsId({
esClient,
agentIds,
returnDataPreview
);
agentsIds,
dataStreamPattern,
returnDataPreview,
});
const body = { items, dataPreview };

View file

@ -614,6 +614,7 @@ export const getSavedObjectTypes = (
},
secret_references: { properties: { id: { type: 'keyword' } } },
overrides: { type: 'flattened', index: false },
supports_agentless: { type: 'boolean' },
revision: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },
@ -774,6 +775,16 @@ export const getSavedObjectTypes = (
},
],
},
'16': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
supports_agentless: { type: 'boolean' },
},
},
],
},
},
migrations: {
'7.10.0': migratePackagePolicyToV7100,
@ -829,6 +840,7 @@ export const getSavedObjectTypes = (
},
secret_references: { properties: { id: { type: 'keyword' } } },
overrides: { type: 'flattened', index: false },
supports_agentless: { type: 'boolean' },
revision: { type: 'integer' },
updated_at: { type: 'date' },
updated_by: { type: 'keyword' },
@ -848,6 +860,16 @@ export const getSavedObjectTypes = (
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
supports_agentless: { type: 'boolean' },
},
},
],
},
},
},
[PACKAGES_SAVED_OBJECT_TYPE]: {

View file

@ -177,11 +177,17 @@ export async function getAgentStatusForAgentPolicy(
};
}
export async function getIncomingDataByAgentsId(
esClient: ElasticsearchClient,
agentsIds: string[],
returnDataPreview: boolean = false
) {
export async function getIncomingDataByAgentsId({
esClient,
agentsIds,
dataStreamPattern = DATA_STREAM_INDEX_PATTERN,
returnDataPreview = false,
}: {
esClient: ElasticsearchClient;
agentsIds: string[];
dataStreamPattern?: string;
returnDataPreview?: boolean;
}) {
const logger = appContextService.getLogger();
try {
@ -189,7 +195,7 @@ export async function getIncomingDataByAgentsId(
body: {
index: [
{
names: [DATA_STREAM_INDEX_PATTERN],
names: [dataStreamPattern],
privileges: ['read'],
},
],
@ -203,7 +209,7 @@ export async function getIncomingDataByAgentsId(
const searchResult = await retryTransientEsErrors(
() =>
esClient.search({
index: DATA_STREAM_INDEX_PATTERN,
index: dataStreamPattern,
allow_partial_search_results: true,
_source: returnDataPreview,
timeout: '5s',
@ -244,9 +250,9 @@ export async function getIncomingDataByAgentsId(
if (!searchResult.aggregations?.agent_ids) {
return {
items: agentsIds.map((id) => {
return { items: { [id]: { data: false } } };
return { [id]: { data: false } };
}),
data: [],
dataPreview: [],
};
}

View file

@ -1929,6 +1929,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
output_id: newPolicy.output_id,
inputs: newPolicy.inputs[0]?.streams ? newPolicy.inputs : inputs,
vars: newPolicy.vars || newPP.vars,
supports_agentless: newPolicy.supports_agentless,
};
}
}

View file

@ -172,6 +172,16 @@ export const PackagePolicyBaseSchema = {
),
])
),
supports_agentless: schema.maybe(
schema.nullable(
schema.boolean({
defaultValue: false,
meta: {
description: 'Indicates whether the package policy belongs to an agentless agent policy.',
},
})
)
),
};
export const NewPackagePolicySchema = schema.object({
@ -286,6 +296,16 @@ export const SimplifiedPackagePolicyBaseSchema = schema.object({
output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])),
vars: schema.maybe(SimplifiedVarsSchema),
inputs: SimplifiedPackagePolicyInputsSchema,
supports_agentless: schema.maybe(
schema.nullable(
schema.boolean({
defaultValue: false,
meta: {
description: 'Indicates whether the package policy belongs to an agentless agent policy.',
},
})
)
),
});
export const SimplifiedPackagePolicyPreconfiguredSchema = SimplifiedPackagePolicyBaseSchema.extends(

View file

@ -527,6 +527,8 @@ export const GetAgentStatusResponseSchema = schema.object({
export const GetAgentDataRequestSchema = {
query: schema.object({
agentsIds: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]),
pkgName: schema.maybe(schema.string()),
pkgVersion: schema.maybe(schema.string()),
previewData: schema.boolean({ defaultValue: false }),
}),
};

View file

@ -65,12 +65,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await cisIntegration.navigateToIntegrationCspList();
await pageObjects.header.waitUntilLoadingHasFinished();
expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be(
expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessIntegration()).to.be(
integrationPolicyName
);
expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be(
`Agentless policy for ${integrationPolicyName}`
);
expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessStatus()).to.be('Pending');
});
it(`should show setup technology selector in edit mode`, async () => {
@ -97,7 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await cisIntegration.navigateToIntegrationCspList();
await pageObjects.header.waitUntilLoadingHasFinished();
await cisIntegration.navigateToEditIntegrationPage();
await cisIntegration.navigateToEditAgentlessIntegrationPage();
await pageObjects.header.waitUntilLoadingHasFinished();
expect(await cisIntegration.showSetupTechnologyComponent()).to.be(true);

View file

@ -218,6 +218,10 @@ export function AddCisIntegrationFormPageProvider({
await testSubjects.click('integrationNameLink');
};
const navigateToEditAgentlessIntegrationPage = async () => {
await testSubjects.click('agentlessIntegrationNameLink');
};
const navigateToAddIntegrationKspmPage = async (space?: string) => {
const options = space
? {
@ -485,7 +489,7 @@ export function AddCisIntegrationFormPageProvider({
await navigateToIntegrationCspList();
await PageObjects.header.waitUntilLoadingHasFinished();
await navigateToEditIntegrationPage();
await navigateToEditAgentlessIntegrationPage();
await PageObjects.header.waitUntilLoadingHasFinished();
// Fill out form to edit an agentless integration
@ -498,7 +502,7 @@ export function AddCisIntegrationFormPageProvider({
// Check if the Direct Access Key is updated package policy api with successful toast
expect(await testSubjects.exists('policyUpdateSuccessToast')).to.be(true);
await navigateToEditIntegrationPage();
await navigateToEditAgentlessIntegrationPage();
await PageObjects.header.waitUntilLoadingHasFinished();
};
@ -511,12 +515,23 @@ export function AddCisIntegrationFormPageProvider({
return await integration.getVisibleText();
};
const getFirstCspmIntegrationPageAgentlessIntegration = async () => {
const integration = await testSubjects.find('agentlessIntegrationNameLink');
return await integration.getVisibleText();
};
const getFirstCspmIntegrationPageAgent = async () => {
const agent = await testSubjects.find('agentPolicyNameLink');
// this is assuming that the agent was just created therefor should be the first element
return await agent.getVisibleText();
};
const getFirstCspmIntegrationPageAgentlessStatus = async () => {
const agent = await testSubjects.find('agentlessStatusBadge');
// this is assuming that the agent was just created therefor should be the first element
return await agent.getVisibleText();
};
const getAgentBasedPolicyValue = async () => {
const agentName = await testSubjects.find('createAgentPolicyNameField');
return await agentName.getAttribute('value');
@ -568,10 +583,13 @@ export function AddCisIntegrationFormPageProvider({
testSubjectIds,
inputIntegrationName,
getFirstCspmIntegrationPageIntegration,
getFirstCspmIntegrationPageAgentlessIntegration,
getFirstCspmIntegrationPageAgent,
getFirstCspmIntegrationPageAgentlessStatus,
getAgentBasedPolicyValue,
showSuccessfulToast,
showSetupTechnologyComponent,
navigateToEditIntegrationPage,
navigateToEditAgentlessIntegrationPage,
};
}

View file

@ -334,5 +334,31 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'xxxx')
.expect(400);
});
it('should return incoming data status for specified agents', async () => {
// force install the system package to override package verification
await supertest
.post(`/api/fleet/epm/packages/system/1.50.0`)
.set('kbn-xsrf', 'xxxx')
.send({ force: true })
.expect(200);
const { body: apiResponse1 } = await supertest
.get(`/api/fleet/agent_status/data?agentsIds=agent1&agentsIds=agent2`)
.expect(200);
const { body: apiResponse2 } = await supertest
.get(
`/api/fleet/agent_status/data?agentsIds=agent1&agentsIds=agent2&pkgName=system&pkgVersion=1.50.0`
)
.expect(200);
expect(apiResponse1).to.eql({
items: [{ agent1: { data: false } }, { agent2: { data: false } }],
dataPreview: [],
});
expect(apiResponse2).to.eql({
items: [{ agent1: { data: false } }, { agent2: { data: false } }],
dataPreview: [],
});
});
});
}

View file

@ -78,12 +78,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await cisIntegration.navigateToIntegrationCspList();
await pageObjects.header.waitUntilLoadingHasFinished();
expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be(
expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessIntegration()).to.be(
integrationPolicyName
);
expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be(
`Agentless policy for ${integrationPolicyName}`
);
expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessStatus()).to.be('Pending');
});
it(`should create default agent-based agent`, async () => {