diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx index c6ef212b3995..7ad034b1cc05 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx @@ -17,15 +17,21 @@ import { DefaultPageTitle } from './default_page_title'; interface Props { section?: Section; children?: React.ReactNode; + rightColumn?: JSX.Element; } -export const DefaultLayout: React.FunctionComponent = ({ section, children }) => { +export const DefaultLayout: React.FunctionComponent = ({ + section, + children, + rightColumn, +}) => { const { getHref } = useLink(); const { agents } = useConfig(); return ( } + rightColumn={rightColumn} tabs={[ { name: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx new file mode 100644 index 000000000000..3b9d297f37df --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx @@ -0,0 +1,116 @@ +/* + * 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 } from 'react'; +import { + EuiButton, + EuiPanel, + EuiLink, + EuiEmptyPrompt, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiLoadingSpinner, + EuiSpacer, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { useFleetStatus, useStartServices } from '../../../../hooks'; + +const REFRESH_INTERVAL = 10000; + +export const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => { + const { docLinks } = useStartServices(); + + const { refresh } = useFleetStatus(); + + useEffect(() => { + const interval = setInterval(() => { + refresh(); + }, REFRESH_INTERVAL); + + return () => clearInterval(interval); + }, [refresh]); + + return ( + <> + + + + + } + body={ + + + + ), + }} + /> + } + actions={ + <> + + + + + } + /> + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx new file mode 100644 index 000000000000..d2ff0d081b97 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -0,0 +1,789 @@ +/* + * 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, { useState, useMemo, useCallback, useEffect } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + EuiLink, + EuiSteps, + EuiCode, + EuiCodeBlock, + EuiCallOut, + EuiSelect, + EuiRadioGroup, + EuiFieldText, + EuiForm, + EuiFormErrorText, +} from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DownloadStep } from '../../../../components'; +import { + useStartServices, + useGetOutputs, + sendGenerateServiceToken, + usePlatform, + PLATFORM_OPTIONS, + useGetAgentPolicies, + useGetSettings, + sendPutSettings, + sendGetFleetStatus, + useFleetStatus, + useUrlModal, +} from '../../../../hooks'; +import type { PLATFORM_TYPE } from '../../../../hooks'; +import type { PackagePolicy } from '../../../../types'; +import { FLEET_SERVER_PACKAGE } from '../../../../constants'; + +import { getInstallCommandForPlatform } from './install_command_utils'; + +const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; +const REFRESH_INTERVAL = 10000; + +type DeploymentMode = 'production' | 'quickstart'; + +const FlexItemWithMinWidth = styled(EuiFlexItem)` + min-width: 0px; + max-width: 100%; +`; + +export const ContentWrapper = styled(EuiFlexGroup)` + height: 100%; + margin: 0 auto; + max-width: 800px; +`; + +// Otherwise the copy button is over the text +const CommandCode = styled.div.attrs(() => { + return { + className: 'eui-textBreakAll', + }; +})` + margin-right: ${(props) => props.theme.eui.paddingSizes.m}; +`; + +export const ServiceTokenStep = ({ + disabled = false, + serviceToken, + getServiceToken, + isLoadingServiceToken, +}: { + disabled?: boolean; + serviceToken?: string; + getServiceToken: () => void; + isLoadingServiceToken: boolean; +}): EuiStepProps => { + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepGenerateServiceTokenTitle', { + defaultMessage: 'Generate a service token', + }), + status: disabled ? 'disabled' : undefined, + children: !disabled && ( + <> + + + + + {!serviceToken ? ( + + + { + getServiceToken(); + }} + > + + + + + ) : ( + <> + + } + /> + + + + + + + + + + {serviceToken} + + + + + )} + + ), + }; +}; + +export const FleetServerCommandStep = ({ + serviceToken, + installCommand, + platform, + setPlatform, +}: { + serviceToken?: string; + installCommand: string; + platform: string; + setPlatform: (platform: PLATFORM_TYPE) => void; +}): EuiStepProps => { + const { docLinks } = useStartServices(); + + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', { + defaultMessage: 'Start Fleet Server', + }), + status: !serviceToken ? 'disabled' : undefined, + children: serviceToken ? ( + <> + + + + + ), + }} + /> + + + + + + } + options={PLATFORM_OPTIONS} + value={platform} + onChange={(e) => setPlatform(e.target.value as PLATFORM_TYPE)} + aria-label={i18n.translate('xpack.fleet.fleetServerSetup.platformSelectAriaLabel', { + defaultMessage: 'Platform', + })} + /> + + + {installCommand} + + + + + + + ), + }} + /> + + + ) : null, + }; +}; + +export const useFleetServerInstructions = (policyId?: string) => { + const outputsRequest = useGetOutputs(); + const { notifications } = useStartServices(); + const [serviceToken, setServiceToken] = useState(); + const [isLoadingServiceToken, setIsLoadingServiceToken] = useState(false); + const { platform, setPlatform } = usePlatform(); + const [deploymentMode, setDeploymentMode] = useState('production'); + const { data: settings, resendRequest: refreshSettings } = useGetSettings(); + const fleetServerHost = settings?.item.fleet_server_hosts?.[0]; + const output = outputsRequest.data?.items?.[0]; + const esHost = output?.hosts?.[0]; + + const installCommand = useMemo((): string => { + if (!serviceToken || !esHost) { + return ''; + } + + return getInstallCommandForPlatform( + platform, + esHost, + serviceToken, + policyId, + fleetServerHost, + deploymentMode === 'production' + ); + }, [serviceToken, esHost, platform, policyId, fleetServerHost, deploymentMode]); + + const getServiceToken = useCallback(async () => { + setIsLoadingServiceToken(true); + try { + const { data } = await sendGenerateServiceToken(); + if (data?.value) { + setServiceToken(data?.value); + } + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.fleetServerSetup.errorGeneratingTokenTitleText', { + defaultMessage: 'Error generating token', + }), + }); + } + + setIsLoadingServiceToken(false); + }, [notifications.toasts]); + + const refresh = useCallback(() => { + return Promise.all([outputsRequest.resendRequest(), refreshSettings()]); + }, [outputsRequest, refreshSettings]); + + const addFleetServerHost = useCallback( + async (host: string) => { + try { + await sendPutSettings({ + fleet_server_hosts: [host, ...(settings?.item.fleet_server_hosts || [])], + }); + await refreshSettings(); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.fleetServerSetup.errorAddingFleetServerHostTitle', { + defaultMessage: 'Error adding Fleet Server host', + }), + }); + } + }, + [refreshSettings, notifications.toasts, settings?.item.fleet_server_hosts] + ); + + return { + addFleetServerHost, + fleetServerHost, + deploymentMode, + setDeploymentMode, + serviceToken, + getServiceToken, + isLoadingServiceToken, + installCommand, + platform, + setPlatform, + refresh, + }; +}; + +const AgentPolicySelectionStep = ({ + policyId, + setPolicyId, +}: { + policyId?: string; + setPolicyId: (v: string) => void; +}): EuiStepProps => { + const { data } = useGetAgentPolicies({ full: true }); + + const agentPolicies = useMemo( + () => + data + ? data.items.filter((item) => { + return item.package_policies.some( + (p: string | PackagePolicy) => + (p as PackagePolicy).package?.name === FLEET_SERVER_PACKAGE + ); + return false; + }) + : [], + [data] + ); + + const options = useMemo(() => { + return agentPolicies.map((policy) => ({ text: policy.name, value: policy.id })); + }, [agentPolicies]); + + useEffect(() => { + // Select default value + if (agentPolicies.length && !policyId) { + const defaultPolicy = + agentPolicies.find((p) => p.is_default_fleet_server) || agentPolicies[0]; + setPolicyId(defaultPolicy.id); + } + }, [options, agentPolicies, policyId, setPolicyId]); + + const onChangeCallback = useCallback( + (e: React.ChangeEvent) => { + setPolicyId(e.target.value); + }, + [setPolicyId] + ); + + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepSelectAgentPolicyTitle', { + defaultMessage: 'Select an Agent policy', + }), + status: undefined, + children: ( + <> + + + + + + + + } + options={options} + value={policyId} + onChange={onChangeCallback} + aria-label={i18n.translate('xpack.fleet.fleetServerSetup.agentPolicySelectAraiLabel', { + defaultMessage: 'Agent policy', + })} + /> + + ), + }; +}; + +export const addFleetServerHostStep = ({ + addFleetServerHost, +}: { + addFleetServerHost: (v: string) => Promise; +}): EuiStepProps => { + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.addFleetServerHostStepTitle', { + defaultMessage: 'Add your Fleet Server host', + }), + status: undefined, + children: , + }; +}; + +export const AddFleetServerHostStepContent = ({ + addFleetServerHost, +}: { + addFleetServerHost: (v: string) => Promise; +}) => { + const [calloutHost, setCalloutHost] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [fleetServerHost, setFleetServerHost] = useState(''); + const [error, setError] = useState(); + const { getModalHref } = useUrlModal(); + + const validate = useCallback( + (host: string) => { + if (host.match(URL_REGEX)) { + setError(undefined); + return true; + } else { + setError( + i18n.translate('xpack.fleet.fleetServerSetup.addFleetServerHostInvalidUrlError', { + defaultMessage: 'Invalid URL', + }) + ); + return false; + } + }, + [setError] + ); + + const onSubmit = useCallback(async () => { + try { + setIsLoading(true); + if (validate(fleetServerHost)) { + await addFleetServerHost(fleetServerHost); + } + setCalloutHost(fleetServerHost); + setFleetServerHost(''); + } finally { + setIsLoading(false); + } + }, [fleetServerHost, addFleetServerHost, validate]); + + const onChange = useCallback( + (e: React.ChangeEvent) => { + setFleetServerHost(e.target.value); + if (error) { + validate(e.target.value); + } + }, + [error, validate, setFleetServerHost] + ); + + return ( + + + 8220 }} + /> + + + + + + + + } + /> + {error && {error}} + + + + + + + + {calloutHost && ( + <> + + + } + > + + + + ), + }} + /> + + + )} + + ); +}; + +export const deploymentModeStep = ({ + deploymentMode, + setDeploymentMode, +}: { + deploymentMode: DeploymentMode; + setDeploymentMode: (v: DeploymentMode) => void; +}): EuiStepProps => { + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepDeploymentModeTitle', { + defaultMessage: 'Choose a deployment mode for security', + }), + status: undefined, + children: ( + + ), + }; +}; + +const DeploymentModeStepContent = ({ + deploymentMode, + setDeploymentMode, +}: { + deploymentMode: DeploymentMode; + setDeploymentMode: (v: DeploymentMode) => void; +}) => { + const onChangeCallback = useCallback( + (v: string) => { + if (v === 'production' || v === 'quickstart') { + setDeploymentMode(v); + } + }, + [setDeploymentMode] + ); + + return ( + <> + + + + + + + + ), + }} + /> + ), + }, + { + id: 'production', + label: ( + + + + ), + }} + /> + ), + }, + ]} + idSelected={deploymentMode} + onChange={onChangeCallback} + name="radio group" + /> + + ); +}; + +const WaitingForFleetServerStep = ({ + status, +}: { + status: 'loading' | 'disabled' | 'complete'; +}): EuiStepProps => { + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepWaitingForFleetServerTitle', { + defaultMessage: 'Waiting for Fleet Server to connect...', + }), + status, + children: undefined, + }; +}; + +const CompleteStep = (): EuiStepProps => { + const fleetStatus = useFleetStatus(); + + const onContinueClick = () => { + fleetStatus.refresh(); + }; + + return { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepFleetServerCompleteTitle', { + defaultMessage: 'Fleet Server connected', + }), + status: 'complete', + children: ( + <> + + + + + + + ), + }; +}; + +export const OnPremInstructions: React.FC = () => { + const { notifications } = useStartServices(); + const [policyId, setPolicyId] = useState(); + + const { + serviceToken, + getServiceToken, + isLoadingServiceToken, + installCommand, + platform, + setPlatform, + refresh, + deploymentMode, + setDeploymentMode, + fleetServerHost, + addFleetServerHost, + } = useFleetServerInstructions(policyId); + + const { modal } = useUrlModal(); + useEffect(() => { + // Refresh settings when the settings modal is closed + if (!modal) { + refresh(); + } + }, [modal, refresh]); + + const { docLinks } = useStartServices(); + + const [isWaitingForFleetServer, setIsWaitingForFleetServer] = useState(true); + + useEffect(() => { + const interval = setInterval(async () => { + try { + const res = await sendGetFleetStatus(); + if (res.error) { + throw res.error; + } + if (res.data?.isReady && !res.data?.missing_requirements?.includes('fleet_server')) { + setIsWaitingForFleetServer(false); + } + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.fleetServerSetup.errorRefreshingFleetServerStatus', { + defaultMessage: 'Error refreshing Fleet Server status', + }), + }); + } + }, REFRESH_INTERVAL); + + return () => clearInterval(interval); + }, [notifications.toasts]); + + return ( + + + +

+ +

+ + + + + ), + }} + /> +
+ + +
+ ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/index.tsx new file mode 100644 index 000000000000..3118fda75400 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CloudInstructions } from './fleet_server_cloud_instructions'; +export * from './fleet_server_on_prem_instructions'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts new file mode 100644 index 000000000000..e9e7e0920799 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts @@ -0,0 +1,189 @@ +/* + * 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 { getInstallCommandForPlatform } from './install_command_utils'; + +describe('getInstallCommandForPlatform', () => { + describe('without policy id', () => { + it('should return the correct command if the the policyId is not set for linux-mac', () => { + const res = getInstallCommandForPlatform( + 'linux-mac', + 'http://elasticsearch:9200', + 'service-token-1' + ); + + expect(res).toMatchInlineSnapshot(` + "sudo ./elastic-agent install -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" + `); + }); + + it('should return the correct command if the the policyId is not set for windows', () => { + const res = getInstallCommandForPlatform( + 'windows', + 'http://elasticsearch:9200', + 'service-token-1' + ); + + expect(res).toMatchInlineSnapshot(` + ".\\\\elastic-agent.exe install -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" + `); + }); + + it('should return the correct command if the the policyId is not set for rpm-deb', () => { + const res = getInstallCommandForPlatform( + 'rpm-deb', + 'http://elasticsearch:9200', + 'service-token-1' + ); + + expect(res).toMatchInlineSnapshot(` + "sudo elastic-agent enroll -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" + `); + }); + }); + + describe('with policy id', () => { + it('should return the correct command if the the policyId is set for linux-mac', () => { + const res = getInstallCommandForPlatform( + 'linux-mac', + 'http://elasticsearch:9200', + 'service-token-1', + 'policy-1' + ); + + expect(res).toMatchInlineSnapshot(` + "sudo ./elastic-agent install -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-policy=policy-1" + `); + }); + + it('should return the correct command if the the policyId is set for windows', () => { + const res = getInstallCommandForPlatform( + 'windows', + 'http://elasticsearch:9200', + 'service-token-1', + 'policy-1' + ); + + expect(res).toMatchInlineSnapshot(` + ".\\\\elastic-agent.exe install -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-policy=policy-1" + `); + }); + + it('should return the correct command if the the policyId is set for rpm-deb', () => { + const res = getInstallCommandForPlatform( + 'rpm-deb', + 'http://elasticsearch:9200', + 'service-token-1', + 'policy-1' + ); + + expect(res).toMatchInlineSnapshot(` + "sudo elastic-agent enroll -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-policy=policy-1" + `); + }); + }); + + describe('with policy id and fleet server host and production deployment', () => { + it('should return the correct command if the the policyId is set for linux-mac', () => { + const res = getInstallCommandForPlatform( + 'linux-mac', + 'http://elasticsearch:9200', + 'service-token-1', + 'policy-1', + 'http://fleetserver:8220', + true + ); + + expect(res).toMatchInlineSnapshot(` + "sudo ./elastic-agent install --url=http://fleetserver:8220 \\\\ + -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-policy=policy-1 \\\\ + --certificate-authorities= \\\\ + --fleet-server-es-ca= \\\\ + --fleet-server-cert= \\\\ + --fleet-server-cert-key=" + `); + }); + + it('should return the correct command if the the policyId is set for windows', () => { + const res = getInstallCommandForPlatform( + 'windows', + 'http://elasticsearch:9200', + 'service-token-1', + 'policy-1', + 'http://fleetserver:8220', + true + ); + + expect(res).toMatchInlineSnapshot(` + ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \\\\ + -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-policy=policy-1 \\\\ + --certificate-authorities= \\\\ + --fleet-server-es-ca= \\\\ + --fleet-server-cert= \\\\ + --fleet-server-cert-key=" + `); + }); + + it('should return the correct command if the the policyId is set for rpm-deb', () => { + const res = getInstallCommandForPlatform( + 'rpm-deb', + 'http://elasticsearch:9200', + 'service-token-1', + 'policy-1', + 'http://fleetserver:8220', + true + ); + + expect(res).toMatchInlineSnapshot(` + "sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ + -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-policy=policy-1 \\\\ + --certificate-authorities= \\\\ + --fleet-server-es-ca= \\\\ + --fleet-server-cert= \\\\ + --fleet-server-cert-key=" + `); + }); + }); + + it('should return nothing for an invalid platform', () => { + const res = getInstallCommandForPlatform( + 'rpm-deb', + 'http://elasticsearch:9200', + 'service-token-1' + ); + + expect(res).toMatchInlineSnapshot(` + "sudo elastic-agent enroll -f \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" + `); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts new file mode 100644 index 000000000000..b91c4b60aa71 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PLATFORM_TYPE } from '../../../../hooks'; + +export function getInstallCommandForPlatform( + platform: PLATFORM_TYPE, + esHost: string, + serviceToken: string, + policyId?: string, + fleetServerHost?: string, + isProductionDeployment?: boolean +) { + let commandArguments = ''; + + if (isProductionDeployment && fleetServerHost) { + commandArguments += `--url=${fleetServerHost} \\\n`; + } + + commandArguments += ` -f \\\n --fleet-server-es=${esHost}`; + commandArguments += ` \\\n --fleet-server-service-token=${serviceToken}`; + if (policyId) { + commandArguments += ` \\\n --fleet-server-policy=${policyId}`; + } + + if (isProductionDeployment) { + commandArguments += ` \\\n --certificate-authorities=`; + commandArguments += ` \\\n --fleet-server-es-ca=`; + commandArguments += ` \\\n --fleet-server-cert=`; + commandArguments += ` \\\n --fleet-server-cert-key=`; + } + + switch (platform) { + case 'linux-mac': + return `sudo ./elastic-agent install ${commandArguments}`; + case 'windows': + return `.\\elastic-agent.exe install ${commandArguments}`; + case 'rpm-deb': + return `sudo elastic-agent enroll ${commandArguments}`; + default: + return ''; + } +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.test.tsx deleted file mode 100644 index e4c5840fd5f6..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 { getInstallCommandForPlatform } from './fleet_server_requirement_page'; - -describe('getInstallCommandForPlatform', () => { - describe('without policy id', () => { - it('should return the correct command if the the policyId is not set for linux-mac', () => { - const res = getInstallCommandForPlatform( - 'linux-mac', - 'http://elasticsearch:9200', - 'service-token-1' - ); - - expect(res).toMatchInlineSnapshot( - `"sudo ./elastic-agent install -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1"` - ); - }); - - it('should return the correct command if the the policyId is not set for windows', () => { - const res = getInstallCommandForPlatform( - 'windows', - 'http://elasticsearch:9200', - 'service-token-1' - ); - - expect(res).toMatchInlineSnapshot( - `".\\\\elastic-agent.exe install -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1"` - ); - }); - - it('should return the correct command if the the policyId is not set for rpm-deb', () => { - const res = getInstallCommandForPlatform( - 'rpm-deb', - 'http://elasticsearch:9200', - 'service-token-1' - ); - - expect(res).toMatchInlineSnapshot( - `"sudo elastic-agent enroll -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1"` - ); - }); - }); - - describe('with policy id', () => { - it('should return the correct command if the the policyId is set for linux-mac', () => { - const res = getInstallCommandForPlatform( - 'linux-mac', - 'http://elasticsearch:9200', - 'service-token-1', - 'policy-1' - ); - - expect(res).toMatchInlineSnapshot( - `"sudo ./elastic-agent install -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1 --fleet-server-policy=policy-1"` - ); - }); - - it('should return the correct command if the the policyId is set for windows', () => { - const res = getInstallCommandForPlatform( - 'windows', - 'http://elasticsearch:9200', - 'service-token-1', - 'policy-1' - ); - - expect(res).toMatchInlineSnapshot( - `".\\\\elastic-agent.exe install -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1 --fleet-server-policy=policy-1"` - ); - }); - - it('should return the correct command if the the policyId is set for rpm-deb', () => { - const res = getInstallCommandForPlatform( - 'rpm-deb', - 'http://elasticsearch:9200', - 'service-token-1', - 'policy-1' - ); - - expect(res).toMatchInlineSnapshot( - `"sudo elastic-agent enroll -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1 --fleet-server-policy=policy-1"` - ); - }); - }); - - it('should return nothing for an invalid platform', () => { - const res = getInstallCommandForPlatform( - 'rpm-deb', - 'http://elasticsearch:9200', - 'service-token-1' - ); - - expect(res).toMatchInlineSnapshot( - `"sudo elastic-agent enroll -f --fleet-server-es=http://elasticsearch:9200 --fleet-server-service-token=service-token-1"` - ); - }); -}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx index 9eac6b7370ce..c79263093abe 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx @@ -5,398 +5,25 @@ * 2.0. */ -import React, { useState, useMemo, useCallback } from 'react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPanel, - EuiSpacer, - EuiText, - EuiLink, - EuiEmptyPrompt, - EuiSteps, - EuiCodeBlock, - EuiCallOut, - EuiSelect, -} from '@elastic/eui'; -import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { DownloadStep } from '../../../components'; -import { - useStartServices, - useGetOutputs, - sendGenerateServiceToken, - usePlatform, - PLATFORM_OPTIONS, -} from '../../../hooks'; -import type { PLATFORM_TYPE } from '../../../hooks'; +import { useStartServices } from '../../../hooks'; + +import { CloudInstructions, OnPremInstructions } from './components'; const FlexItemWithMinWidth = styled(EuiFlexItem)` min-width: 0px; max-width: 100%; `; -export const ContentWrapper = styled(EuiFlexGroup)` +const ContentWrapper = styled(EuiFlexGroup)` height: 100%; margin: 0 auto; max-width: 800px; `; -// Otherwise the copy button is over the text -const CommandCode = styled.pre({ - overflow: 'scroll', -}); - -export const ServiceTokenStep = ({ - serviceToken, - getServiceToken, - isLoadingServiceToken, -}: { - serviceToken?: string; - getServiceToken: () => void; - isLoadingServiceToken: boolean; -}): EuiStepProps => { - return { - title: i18n.translate('xpack.fleet.fleetServerSetup.stepGenerateServiceTokenTitle', { - defaultMessage: 'Generate a service token', - }), - children: ( - <> - - - - - {!serviceToken ? ( - - - { - getServiceToken(); - }} - > - - - - - ) : ( - <> - - - - - - - - - - - - - {serviceToken} - - - - - )} - - ), - }; -}; - -export const FleetServerCommandStep = ({ - serviceToken, - installCommand, - platform, - setPlatform, -}: { - serviceToken?: string; - installCommand: string; - platform: string; - setPlatform: (platform: PLATFORM_TYPE) => void; -}): EuiStepProps => { - const { docLinks } = useStartServices(); - - return { - title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', { - defaultMessage: 'Start Fleet Server', - }), - status: !serviceToken ? 'disabled' : undefined, - children: serviceToken ? ( - <> - - - - - ), - }} - /> - - - - - - } - options={PLATFORM_OPTIONS} - value={platform} - onChange={(e) => setPlatform(e.target.value as PLATFORM_TYPE)} - aria-label={i18n.translate('xpack.fleet.fleetServerSetup.platformSelectAriaLabel', { - defaultMessage: 'Platform', - })} - /> - - - {installCommand} - - - - - - - ), - }} - /> - - - ) : null, - }; -}; - -export function getInstallCommandForPlatform( - platform: PLATFORM_TYPE, - esHost: string, - serviceToken: string, - policyId?: string -) { - const commandArguments = `-f --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}${ - policyId ? ` --fleet-server-policy=${policyId}` : '' - }`; - - switch (platform) { - case 'linux-mac': - return `sudo ./elastic-agent install ${commandArguments}`; - case 'windows': - return `.\\elastic-agent.exe install ${commandArguments}`; - case 'rpm-deb': - return `sudo elastic-agent enroll ${commandArguments}`; - default: - return ''; - } -} - -export const useFleetServerInstructions = (policyId?: string) => { - const outputsRequest = useGetOutputs(); - const { notifications } = useStartServices(); - const [serviceToken, setServiceToken] = useState(); - const [isLoadingServiceToken, setIsLoadingServiceToken] = useState(false); - const { platform, setPlatform } = usePlatform(); - - const output = outputsRequest.data?.items?.[0]; - const esHost = output?.hosts?.[0]; - - const installCommand = useMemo((): string => { - if (!serviceToken || !esHost) { - return ''; - } - - return getInstallCommandForPlatform(platform, esHost, serviceToken, policyId); - }, [serviceToken, esHost, platform, policyId]); - - const getServiceToken = useCallback(async () => { - setIsLoadingServiceToken(true); - try { - const { data } = await sendGenerateServiceToken(); - if (data?.value) { - setServiceToken(data?.value); - } - } catch (err) { - notifications.toasts.addError(err, { - title: i18n.translate('xpack.fleet.fleetServerSetup.errorGeneratingTokenTitleText', { - defaultMessage: 'Error generating token', - }), - }); - } - - setIsLoadingServiceToken(false); - }, [notifications]); - - return { - serviceToken, - getServiceToken, - isLoadingServiceToken, - installCommand, - platform, - setPlatform, - }; -}; - -const OnPremInstructions: React.FC = () => { - const { - serviceToken, - getServiceToken, - isLoadingServiceToken, - installCommand, - platform, - setPlatform, - } = useFleetServerInstructions(); - const { docLinks } = useStartServices(); - - return ( - - - -

- -

- - - - - ), - }} - /> -
- - -
- ); -}; - -const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => { - const { docLinks } = useStartServices(); - - return ( - - - - - } - body={ - - - - ), - }} - /> - } - actions={ - <> - - - - - } - /> - - ); -}; - export const FleetServerRequirementPage = () => { const startService = useStartServices(); const deploymentUrl = startService.cloud?.deploymentUrl; @@ -411,23 +38,7 @@ export const FleetServerRequirementPage = () => { )} - - - - - - - - - - - - - ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/index.tsx index 9e6505ede491..9993014f55cd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/index.tsx @@ -6,9 +6,4 @@ */ export { MissingESRequirementsPage } from './es_requirements_page'; -export { - FleetServerRequirementPage, - ServiceTokenStep, - FleetServerCommandStep, - useFleetServerInstructions, -} from './fleet_server_requirement_page'; +export { FleetServerRequirementPage } from './fleet_server_requirement_page'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx index 79b19b443cca..52a4c9d17648 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/index.tsx @@ -5,18 +5,20 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPortal } from '@elastic/eui'; import { FLEET_ROUTING_PATHS } from '../../constants'; -import { Loading, Error } from '../../components'; +import { Loading, Error, AgentEnrollmentFlyout } from '../../components'; import { useConfig, useFleetStatus, useBreadcrumbs, useCapabilities, useGetSettings, + useGetAgentPolicies, } from '../../hooks'; import { DefaultLayout, WithoutHeaderLayout } from '../../layouts'; @@ -26,18 +28,26 @@ import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; import { FleetServerUpgradeModal } from './components/fleet_server_upgrade_modal'; -const REFRESH_INTERVAL_MS = 30000; - export const AgentsApp: React.FunctionComponent = () => { useBreadcrumbs('agent_list'); const { agents } = useConfig(); const capabilities = useCapabilities(); + const agentPoliciesRequest = useGetAgentPolicies({ + page: 1, + perPage: 1000, + }); + + const agentPolicies = useMemo(() => agentPoliciesRequest.data?.items || [], [ + agentPoliciesRequest.data, + ]); + const fleetStatus = useFleetStatus(); const settings = useGetSettings(); + const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const [fleetServerModalVisible, setFleetServerModalVisible] = useState(false); const onCloseFleetServerModal = useCallback(() => { setFleetServerModalVisible(false); @@ -50,25 +60,6 @@ export const AgentsApp: React.FunctionComponent = () => { } }, [settings.data]); - useEffect(() => { - if ( - !agents.enabled || - fleetStatus.isLoading || - !fleetStatus.missingRequirements || - !fleetStatus.missingRequirements.includes('fleet_server') - ) { - return; - } - - const interval = setInterval(() => { - fleetStatus.refresh(); - }, REFRESH_INTERVAL_MS); - - return () => { - clearInterval(interval); - }; - }, [fleetStatus, agents.enabled]); - if (!agents.enabled) return null; if (!fleetStatus.missingRequirements && fleetStatus.isLoading) { return ; @@ -105,6 +96,27 @@ export const AgentsApp: React.FunctionComponent = () => { return ; } + const rightColumn = hasOnlyFleetServerMissingRequirement ? ( + <> + {isEnrollmentFlyoutOpen && ( + + setIsEnrollmentFlyoutOpen(false)} + /> + + )} + + + setIsEnrollmentFlyoutOpen(true)}> + + + + + + ) : undefined; + return ( @@ -112,7 +124,7 @@ export const AgentsApp: React.FunctionComponent = () => { - + {fleetServerModalVisible && ( )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts index fcf107856649..d16be0d8b97e 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts @@ -14,6 +14,19 @@ jest.mock('../../hooks/use_request', () => { }; }); +jest.mock( + '../../applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions', + () => { + const module = jest.requireActual( + '../../applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions' + ); + return { + ...module, + useFleetServerInstructions: jest.fn(), + }; + } +); + jest.mock( '../../applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page', () => { @@ -23,7 +36,6 @@ jest.mock( return { ...module, FleetServerRequirementPage: jest.fn(), - useFleetServerInstructions: jest.fn(), }; } ); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx index 65118044e98c..f6d62d45a4e5 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -19,7 +19,7 @@ import type { AgentPolicy } from '../../../common'; import { useGetSettings, sendGetFleetStatus } from '../../hooks/use_request'; import { FleetStatusProvider, ConfigContext } from '../../hooks'; -import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page'; +import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page/components'; import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps'; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 58362d85e2fb..9e4bdf386ef4 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -29,8 +29,11 @@ import { StandaloneInstructions } from './standalone_instructions'; import { MissingFleetServerHostCallout } from './missing_fleet_server_host_callout'; import type { BaseProps } from './types'; +type FlyoutMode = 'managed' | 'standalone'; + export interface Props extends BaseProps { onClose: () => void; + defaultMode?: FlyoutMode; } export * from './agent_policy_selection'; @@ -43,8 +46,9 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ agentPolicy, agentPolicies, viewDataStepContent, + defaultMode = 'managed', }) => { - const [mode, setMode] = useState<'managed' | 'standalone'>('managed'); + const [mode, setMode] = useState(defaultMode); const { modal } = useUrlModal(); const [lastModal, setLastModal] = useState(modal); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index efae8db377f7..c739725b7973 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -15,11 +15,13 @@ import { useGetOneEnrollmentAPIKey, useGetSettings, useLink, useFleetStatus } fr import { ManualInstructions } from '../../components/enrollment_instructions'; import { - FleetServerRequirementPage, + deploymentModeStep, ServiceTokenStep, FleetServerCommandStep, useFleetServerInstructions, -} from '../../applications/fleet/sections/agents/agent_requirements_page'; + addFleetServerHostStep, +} from '../../applications/fleet/sections/agents/agent_requirements_page/components'; +import { FleetServerRequirementPage } from '../../applications/fleet/sections/agents/agent_requirements_page'; import { DownloadStep, @@ -69,7 +71,7 @@ export const ManagedInstructions = React.memo( const settings = useGetSettings(); const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); - const steps = useMemo(() => { + const fleetServerSteps = useMemo(() => { const { serviceToken, getServiceToken, @@ -77,7 +79,20 @@ export const ManagedInstructions = React.memo( installCommand, platform, setPlatform, + deploymentMode, + setDeploymentMode, + addFleetServerHost, } = fleetServerInstructions; + + return [ + deploymentModeStep({ deploymentMode, setDeploymentMode }), + addFleetServerHostStep({ addFleetServerHost }), + ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }), + FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }), + ]; + }, [fleetServerInstructions]); + + const steps = useMemo(() => { const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; const baseSteps: EuiContainedStepProps[] = [ DownloadStep(), @@ -91,12 +106,7 @@ export const ManagedInstructions = React.memo( : AgentEnrollmentKeySelectionStep({ agentPolicy, selectedApiKeyId, setSelectedAPIKeyId }), ]; if (isFleetServerPolicySelected) { - baseSteps.push( - ...[ - ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }), - FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }), - ] - ); + baseSteps.push(...fleetServerSteps); } else { baseSteps.push({ title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { @@ -115,13 +125,14 @@ export const ManagedInstructions = React.memo( return baseSteps; }, [ agentPolicy, - agentPolicies, selectedApiKeyId, + setSelectedAPIKeyId, + viewDataStepContent, + agentPolicies, apiKey.data, + fleetServerSteps, isFleetServerPolicySelected, settings.data?.item?.fleet_server_hosts, - fleetServerInstructions, - viewDataStepContent, ]); return ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index 8b12994473e3..f77cba754e90 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -27,7 +27,7 @@ export const DownloadStep = () => { diff --git a/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx b/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx index 24ecc458d6bc..c8965f8be015 100644 --- a/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useContext, useEffect } from 'react'; +import React, { useState, useContext, useEffect, useCallback } from 'react'; import type { GetFleetStatusResponse } from '../types'; @@ -33,30 +33,35 @@ export const FleetStatusProvider: React.FC = ({ children }) => { isLoading: false, isReady: false, }); - async function sendGetStatus() { - try { - setState((s) => ({ ...s, isLoading: true })); - const res = await sendGetFleetStatus(); - if (res.error) { - throw res.error; - } + const sendGetStatus = useCallback( + async function sendGetStatus() { + try { + setState((s) => ({ ...s, isLoading: true })); + const res = await sendGetFleetStatus(); + if (res.error) { + throw res.error; + } - setState((s) => ({ - ...s, - isLoading: false, - isReady: res.data?.isReady ?? false, - missingRequirements: res.data?.missing_requirements, - })); - } catch (error) { - setState((s) => ({ ...s, isLoading: false, error })); - } - } + setState((s) => ({ + ...s, + isLoading: false, + isReady: res.data?.isReady ?? false, + missingRequirements: res.data?.missing_requirements, + })); + } catch (error) { + setState((s) => ({ ...s, isLoading: false, error })); + } + }, + [setState] + ); useEffect(() => { sendGetStatus(); - }, []); + }, [sendGetStatus]); + + const refresh = useCallback(() => sendGetStatus(), [sendGetStatus]); return ( - sendGetStatus() }}> + {children} );