Skip to content

Commit

Permalink
[Fleet] Display next steps and actions in agentless integrations flyo…
Browse files Browse the repository at this point in the history
…ut (elastic#203824)

## Summary

Display next steps and actions in agentless integrations flyout. This PR
is based off the following changes:

**Agentless flyout**
Introduced with elastic#199567

**package-spec**
The definitions for package-spec have been updated in these two PRs:
- elastic/package-spec#834
- elastic/package-spec#844
Any agentless package can now define internal links with format
`kbn:/app/...` and external links with format `https://...`. This PR
shows a card or a button linking to these urls in the new agentless
flyout

**Connectors**
Agentless integration now expose connectors name and id in the package
policy (see code
[here](https://github.com/elastic/integrations/blob/69fd5a26c4d0a8e9e999c51fb49a2cf28c078dd2/packages/elastic_connectors/manifest.yml#L45-L62)
for elastic connectors integration).
<img width="1003" alt="Screenshot 2024-12-16 at 16 30 22"
src="https://github.com/user-attachments/assets/70b3471e-51bb-4e79-95a4-843e20128c26"
/>

This PR creates a dynamic link to the connector configured in the policy
and shows it in the agentless flyout.

### Testing
- First of all, enable agentless following the steps under `Testing` in
[ this PR](elastic#199567). Follow up to
step 3
- Instead of installing CSPM, install this test package
[agentless_package_links-0.0.1.zip](https://github.com/user-attachments/files/18152872/agentless_package_links-0.0.1.zip)
with the upload command
```
curl -XPOST -H 'content-type: application/zip' -H 'kbn-xsrf: true' http://localhost:5601/YOURPATH/api/fleet/epm/packages -u elastic:changeme --data-binary @agentless_package_links-0.0.1.zip
```
- Once appears installed, create a package policy with this new
integration. Make sure to choose `agentless` as deployment mode
<img width="1278" alt="Screenshot 2024-12-16 at 16 22 09"
src="https://github.com/user-attachments/assets/7104bf2a-e419-4efa-b352-278ad2057951"
/>

- Enroll an agent to the newly created "agentless" policy by using the
token (it's available in the token page)
- Go back to integrations, you should see a page like this one:
<img width="1569" alt="Screenshot 2024-12-16 at 16 38 18"
src="https://github.com/user-attachments/assets/de770984-985e-449e-b6e3-5c78eb5d3926"
/>

- Click on the state ("pending"/"healhty"/"unhealthy") and see the
flyout. If the enrollment was successful, you should see some cards and
buttons that link to internal and external links in kibana

<img width="878" alt="Screenshot 2024-12-16 at 16 21 57"
src="https://github.com/user-attachments/assets/c77b224f-882c-4d52-956a-744e94e36f1e"
/>

### Testing the connector cards
- First create a new connector: go to
`app/elasticsearch/content/connectors` and click on "new connector". For
this purpose there's no need to complete the procedure
- Note down the name and id of the connector
<img width="1789" alt="Screenshot 2024-12-16 at 16 42 00"
src="https://github.com/user-attachments/assets/b60e491c-809a-40d5-8d01-12b225896fca"
/>
<img width="1789" alt="Screenshot 2024-12-16 at 16 42 00"
src="https://github.com/user-attachments/assets/1adc65e4-0b3b-4e03-9e65-5cc01385b0db"
/>
- Go back to the integration policy previously installed. Enable the
"Test Connector" input and add the name and id from above.
- The agentless flyout should now have a card that will link the user to
`app/elasticsearch/content/connectors/<id>`


### Checklist

- [ ] 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

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored and JoseLuisGJ committed Dec 19, 2024
1 parent a636700 commit f89c3b3
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 9 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/constants/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
export const PLUGIN_ID = 'fleet' as const;
export const INTEGRATIONS_PLUGIN_ID = 'integrations' as const;
export const TRANSFORM_PLUGIN_ID = 'transform' as const;
export const ELASTICSEARCH_PLUGIN_ID = 'elasticsearch' as const;
11 changes: 11 additions & 0 deletions x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ export interface DeploymentsModes {
default?: DeploymentsModesDefault;
}

type Action = 'action';
type NextStep = 'next_step';
export interface ConfigurationLink {
title: string;
url: string;
type: Action | NextStep;
content?: string;
}

export enum RegistryPolicyTemplateKeys {
categories = 'categories',
data_streams = 'data_streams',
Expand All @@ -223,6 +232,7 @@ export enum RegistryPolicyTemplateKeys {
icons = 'icons',
screenshots = 'screenshots',
deployment_modes = 'deployment_modes',
configuration_links = 'configuration_links',
}
interface BaseTemplate {
[RegistryPolicyTemplateKeys.name]: string;
Expand All @@ -232,6 +242,7 @@ interface BaseTemplate {
[RegistryPolicyTemplateKeys.screenshots]?: RegistryImage[];
[RegistryPolicyTemplateKeys.multiple]?: boolean;
[RegistryPolicyTemplateKeys.deployment_modes]?: DeploymentsModes;
[RegistryPolicyTemplateKeys.configuration_links]?: ConfigurationLink[];
}
export interface RegistryPolicyIntegrationTemplate extends BaseTemplate {
[RegistryPolicyTemplateKeys.categories]?: Array<PackageSpecCategory | undefined>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export const AgentlessEnrollmentFlyout = ({
<AgentlessStepConfirmData
agent={agentData}
packagePolicy={packagePolicy}
policyTemplates={packageInfoData?.item.policy_templates}
setConfirmDataStatus={setConfirmDataStatus}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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, { useCallback, useMemo } from 'react';
import {
EuiSpacer,
EuiFlexItem,
EuiCard,
EuiFlexGroup,
EuiButton,
EuiHorizontalRule,
} from '@elastic/eui';

import { i18n } from '@kbn/i18n';

import { useStartServices } from '../../hooks';
import type { PackagePolicy, RegistryPolicyTemplate } from '../../types';
import { ELASTICSEARCH_PLUGIN_ID } from '../../../common/constants/plugin';

export const NextSteps = ({
packagePolicy,
policyTemplates,
}: {
packagePolicy: PackagePolicy;
policyTemplates?: RegistryPolicyTemplate[];
}) => {
const { application } = useStartServices();

const configurationLinks = useMemo(() => {
if (policyTemplates) {
return policyTemplates
?.filter(
(template) => template?.configuration_links && template.configuration_links.length > 0
)
.flatMap((template) => template.configuration_links);
}
return [];
}, [policyTemplates]);

const parseKbnLink = (url: string) => {
// matching strings with format kbn:/app/appId/path/optionalsubpath
const matches = url.match(/kbn:\/app\/(\w*)\/(\w*\/*)*/);
if (matches && matches.length > 0) {
const appId = matches[1];
const path = matches[2];
return { appId, path };
}
return undefined;
};

const isExternal = (url: string) => url.startsWith('http') || url.startsWith('https');
const onClickLink = useCallback(
(url?: string) => {
if (!url) return undefined;

if (isExternal(url)) {
application.navigateToUrl(`${url}`);
} else if (url.startsWith('kbn:/')) {
const parsedLink = parseKbnLink(url);
if (parsedLink) {
const { appId, path } = parsedLink;
application.navigateToApp(appId, {
path,
});
}
}
},
[application]
);

const nextStepsCards = configurationLinks
.filter((link) => link?.type === 'next_step')
.map((link, index) => {
return (
<EuiFlexItem key={index}>
<EuiCard
data-test-subj={`agentlessStepConfirmData.connectorCard.${link?.title}`}
title={`${link?.title}`}
description={`${link?.content}`}
onClick={() => onClickLink(link?.url)}
/>
</EuiFlexItem>
);
});

const connectorCards = packagePolicy.inputs
.filter((input) => !!input?.vars?.connector_id.value || !!input?.vars?.connector_name.value)
.map((input, index) => {
return (
<EuiFlexItem key={index}>
<EuiCard
data-test-subj={`agentlessStepConfirmData.connectorCard.${input?.vars?.connector_name.value}`}
title={`${input?.vars?.connector_name.value}`}
description={i18n.translate(
'xpack.fleet.agentlessStepConfirmData.connectorCard.description',
{
defaultMessage: 'Configure Connector',
}
)}
onClick={() => {
application.navigateToApp(ELASTICSEARCH_PLUGIN_ID, {
path: input?.vars?.connector_id.value
? `content/connectors/${input?.vars?.connector_id.value}`
: `content/connectors`,
});
}}
/>
</EuiFlexItem>
);
});

const actionButtons = configurationLinks
.filter((link) => !!link && link?.type === 'action')
.map((link, index) => {
return (
<EuiFlexItem key={index} grow={false}>
<EuiButton
data-test-subj={`agentlessStepConfirmData.connectorCard.${link?.title}`}
iconType="link"
onClick={() => onClickLink(link?.url)}
>
{link?.title}
</EuiButton>
</EuiFlexItem>
);
});

return (
<>
<EuiSpacer size="m" />
{nextStepsCards.length > 0 && (
<EuiFlexGroup alignItems="center" direction="row" wrap={true}>
{nextStepsCards}
{connectorCards}
</EuiFlexGroup>
)}
<EuiSpacer size="m" />
<EuiHorizontalRule />
{actionButtons.length > 0 && (
<EuiFlexGroup alignItems="center" direction="row" wrap={true}>
{actionButtons}
</EuiFlexGroup>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ 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 type { Agent, PackagePolicy, RegistryPolicyTemplate } from '../../types';
import {
usePollingIncomingData,
POLLING_TIMEOUT_MS,
} from '../agent_enrollment_flyout/use_get_agent_incoming_data';

import { NextSteps } from './next_steps';

export const AgentlessStepConfirmData = ({
agent,
packagePolicy,
setConfirmDataStatus,
policyTemplates,
}: {
agent: Agent;
packagePolicy: PackagePolicy;
setConfirmDataStatus: (status: EuiStepStatus) => void;
policyTemplates?: RegistryPolicyTemplate[];
}) => {
const { docLinks } = useStartServices();
const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending');
Expand Down Expand Up @@ -53,13 +57,17 @@ export const AgentlessStepConfirmData = ({

if (overallState === 'success') {
return (
<EuiCallOut
color="success"
title={i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.confirmData.successText', {
defaultMessage: 'Incoming data received from agentless integration',
})}
iconType="check"
/>
<>
<EuiCallOut
color="success"
title={i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.confirmData.successText', {
defaultMessage: 'Incoming data received from agentless integration',
})}
iconType="check"
/>
<EuiSpacer size="m" />
<NextSteps packagePolicy={packagePolicy} policyTemplates={policyTemplates} />
</>
);
} else if (overallState === 'failure') {
return (
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/services/agents/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export async function getIncomingDataByAgentsId({
} catch (error) {
logger.debug(`Error getting incoming data for agents: ${error}`);
throw new FleetError(
`Unable to retrive incoming data for agents due to error: ${error.message}`
`Unable to retrieve incoming data for agents due to error: ${error.message}`
);
}
}

0 comments on commit f89c3b3

Please sign in to comment.