[Fleet] Better onboarding experience for Fleet Server on premise (#103550)

This commit is contained in:
Nicolas Chaulet 2021-06-29 14:28:37 -04:00 committed by GitHub
parent 5d24b23182
commit 770aa79121
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1268 additions and 563 deletions

View file

@ -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<Props> = ({ section, children }) => {
export const DefaultLayout: React.FunctionComponent<Props> = ({
section,
children,
rightColumn,
}) => {
const { getHref } = useLink();
const { agents } = useConfig();
return (
<WithHeaderLayout
leftColumn={<DefaultPageTitle />}
rightColumn={rightColumn}
tabs={[
{
name: (

View file

@ -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 (
<>
<EuiPanel
paddingSize="none"
grow={false}
hasShadow={false}
hasBorder={true}
className="eui-textCenter"
>
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.cloudSetupTitle"
defaultMessage="Enable APM & Fleet"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.fleet.fleetServerSetup.cloudSetupText"
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}"
values={{
link: (
<EuiLink
href={docLinks.links.fleet.fleetServerAddFleetServer}
target="_blank"
external
>
<FormattedMessage
id="xpack.fleet.settings.userGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
}
actions={
<>
<EuiButton
iconSide="right"
iconType="popout"
fill
isLoading={false}
type="submit"
href={`${deploymentUrl}/edit`}
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.cloudDeploymentLink"
defaultMessage="Edit deployment"
/>
</EuiButton>
</>
}
/>
</EuiPanel>
<EuiSpacer size="m" />
<EuiFlexItem>
<EuiFlexGroup justifyContent="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.fleet.fleetServerSetup.waitingText"
defaultMessage="Waiting for a Fleet Server to connect..."
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</>
);
};

View file

@ -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 && (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.generateServiceTokenDescription"
defaultMessage="A service token grants Fleet Server permissions to write to Elasticsearch."
/>
</EuiText>
<EuiSpacer size="m" />
{!serviceToken ? (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={isLoadingServiceToken}
isDisabled={isLoadingServiceToken}
onClick={() => {
getServiceToken();
}}
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.generateServiceTokenButton"
defaultMessage="Generate service token"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiCallOut
iconType="check"
size="s"
color="success"
title={
<FormattedMessage
id="xpack.fleet.fleetServerSetup.saveServiceTokenDescription"
defaultMessage="Save your service token information. This will be shown only once."
/>
}
/>
<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<strong>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.serviceTokenLabel"
defaultMessage="Service token"
/>
</strong>
</EuiFlexItem>
<FlexItemWithMinWidth>
<EuiCodeBlock paddingSize="m" isCopyable>
<CommandCode>{serviceToken}</CommandCode>
</EuiCodeBlock>
</FlexItemWithMinWidth>
</EuiFlexGroup>
</>
)}
</>
),
};
};
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 ? (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.installAgentDescription"
defaultMessage="From the agent directory, copy and run the appropriate quick start command to start an Elastic Agent as a Fleet Server using the generated token and a self-signed certificate. See the {userGuideLink} for instructions on using your own certificates for production deployment. All commands require administrator privileges."
values={{
userGuideLink: (
<EuiLink
href={docLinks.links.fleet.fleetServerAddFleetServer}
external
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSelect
prepend={
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.platformSelectLabel"
defaultMessage="Platform"
/>
</EuiText>
}
options={PLATFORM_OPTIONS}
value={platform}
onChange={(e) => setPlatform(e.target.value as PLATFORM_TYPE)}
aria-label={i18n.translate('xpack.fleet.fleetServerSetup.platformSelectAriaLabel', {
defaultMessage: 'Platform',
})}
/>
<EuiSpacer size="s" />
<EuiCodeBlock
fontSize="m"
isCopyable={true}
paddingSize="m"
language="console"
whiteSpace="pre-wrap"
>
<CommandCode>{installCommand}</CommandCode>
</EuiCodeBlock>
<EuiSpacer size="s" />
<EuiText>
<FormattedMessage
id="xpack.fleet.enrollmentInstructions.troubleshootingText"
defaultMessage="If you are having trouble connecting, see our {link}."
values={{
link: (
<EuiLink target="_blank" external href={docLinks.links.fleet.troubleshooting}>
<FormattedMessage
id="xpack.fleet.enrollmentInstructions.troubleshootingLink"
defaultMessage="troubleshooting guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
</>
) : null,
};
};
export const useFleetServerInstructions = (policyId?: string) => {
const outputsRequest = useGetOutputs();
const { notifications } = useStartServices();
const [serviceToken, setServiceToken] = useState<string>();
const [isLoadingServiceToken, setIsLoadingServiceToken] = useState<boolean>(false);
const { platform, setPlatform } = usePlatform();
const [deploymentMode, setDeploymentMode] = useState<DeploymentMode>('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<HTMLSelectElement>) => {
setPolicyId(e.target.value);
},
[setPolicyId]
);
return {
title: i18n.translate('xpack.fleet.fleetServerSetup.stepSelectAgentPolicyTitle', {
defaultMessage: 'Select an Agent policy',
}),
status: undefined,
children: (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.selectAgentPolicyDescriptionText"
defaultMessage="Agent policies allow you to configure and mange your agents remotely. We recommend using the “Default Fleet Server policy” which includes the necessary configuration to run a Fleet Server."
/>
</EuiText>
<EuiSpacer size="m" />
<EuiSelect
prepend={
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.agentPolicySelectLabel"
defaultMessage="Agent policy"
/>
</EuiText>
}
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<void>;
}): EuiStepProps => {
return {
title: i18n.translate('xpack.fleet.fleetServerSetup.addFleetServerHostStepTitle', {
defaultMessage: 'Add your Fleet Server host',
}),
status: undefined,
children: <AddFleetServerHostStepContent addFleetServerHost={addFleetServerHost} />,
};
};
export const AddFleetServerHostStepContent = ({
addFleetServerHost,
}: {
addFleetServerHost: (v: string) => Promise<void>;
}) => {
const [calloutHost, setCalloutHost] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [fleetServerHost, setFleetServerHost] = useState('');
const [error, setError] = useState<undefined | string>();
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<HTMLInputElement>) => {
setFleetServerHost(e.target.value);
if (error) {
validate(e.target.value);
}
},
[error, validate, setFleetServerHost]
);
return (
<EuiForm onSubmit={onSubmit}>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.addFleetServerHostStepDescription"
defaultMessage="Specify the URL your agents will use to connect to Fleet Server. This should match the public IP address or domain of the host where Fleet Server will run. By default, Fleet Server uses port {port}."
values={{ port: <EuiCode>8220</EuiCode> }}
/>
</EuiText>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFieldText
fullWidth
placeholder={'http://127.0.0.1:8220'}
value={calloutHost}
isInvalid={!!error}
onChange={onChange}
prepend={
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.addFleetServerHostInputLabel"
defaultMessage="Fleet Server host"
/>
</EuiText>
}
/>
{error && <EuiFormErrorText>{error}</EuiFormErrorText>}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton isLoading={isLoading} onClick={onSubmit}>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.addFleetServerHostButton"
defaultMessage="Add host"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{calloutHost && (
<>
<EuiSpacer size="m" />
<EuiCallOut
iconType="check"
size="s"
color="success"
title={
<FormattedMessage
id="xpack.fleet.fleetServerSetup.addFleetServerHostSuccessTitle"
defaultMessage="Added Fleet Server host"
/>
}
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.addFleetServerHostSuccessText"
defaultMessage="Added {host}. You can edit your Fleet Server hosts in {fleetSettingsLink}."
values={{
host: calloutHost,
fleetSettingsLink: (
<EuiLink href={getModalHref('settings')}>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.fleetSettingsLink"
defaultMessage="Fleet Settings"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
</>
)}
</EuiForm>
);
};
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: (
<DeploymentModeStepContent
deploymentMode={deploymentMode}
setDeploymentMode={setDeploymentMode}
/>
),
};
};
const DeploymentModeStepContent = ({
deploymentMode,
setDeploymentMode,
}: {
deploymentMode: DeploymentMode;
setDeploymentMode: (v: DeploymentMode) => void;
}) => {
const onChangeCallback = useCallback(
(v: string) => {
if (v === 'production' || v === 'quickstart') {
setDeploymentMode(v);
}
},
[setDeploymentMode]
);
return (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.stepDeploymentModeDescriptionText"
defaultMessage="Fleet uses Transport Layer Security (TLS) to encrypt traffic between Elastic Agents and other components in the Elastic Stack. Choose a deployment mode to determine how you wish to handle certificates. Your selection will affect the Fleet Server set up command shown in a later step."
/>
</EuiText>
<EuiSpacer size="m" />
<EuiRadioGroup
options={[
{
id: 'quickstart',
label: (
<FormattedMessage
id="xpack.fleet.fleetServerSetup.deploymentModeQuickStartOption"
defaultMessage="{quickStart} Fleet Server will generate a self-signed certificate. Subsequent agents must be enrolled using the --insecure flag. Not recommended for production use cases."
values={{
quickStart: (
<strong>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.quickStartText"
defaultMessage="Quick start"
/>
</strong>
),
}}
/>
),
},
{
id: 'production',
label: (
<FormattedMessage
id="xpack.fleet.fleetServerSetup.deploymentModeProductionOption"
defaultMessage="{production} Provide your own certificates. This option will require agents to specify a cert key when enrolling with Fleet"
values={{
production: (
<strong>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.productionText"
defaultMessage="Production"
/>
</strong>
),
}}
/>
),
},
]}
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: (
<>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.stepFleetServerCompleteDescription"
defaultMessage="You can now enroll agents with Fleet."
/>
<EuiSpacer size="m" />
<EuiButton fill onClick={onContinueClick}>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.continueButton"
defaultMessage="Continue"
/>
</EuiButton>
</>
),
};
};
export const OnPremInstructions: React.FC = () => {
const { notifications } = useStartServices();
const [policyId, setPolicyId] = useState<string | undefined>();
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 (
<EuiPanel paddingSize="l" grow={false} hasShadow={false} hasBorder={true}>
<EuiSpacer size="s" />
<EuiText className="eui-textCenter">
<h2>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupTitle"
defaultMessage="Add a Fleet Server"
/>
</h2>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupText"
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. Follow the instructions below to set up a Fleet Server. For more information, see the {userGuideLink}."
values={{
userGuideLink: (
<EuiLink
href={docLinks.links.fleet.fleetServerAddFleetServer}
external
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSteps
className="eui-textLeft"
steps={[
DownloadStep(),
AgentPolicySelectionStep({ policyId, setPolicyId }),
deploymentModeStep({ deploymentMode, setDeploymentMode }),
addFleetServerHostStep({ addFleetServerHost }),
ServiceTokenStep({
disabled: deploymentMode === 'production' && !fleetServerHost,
serviceToken,
getServiceToken,
isLoadingServiceToken,
}),
FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }),
isWaitingForFleetServer
? WaitingForFleetServerStep({
status: !serviceToken ? 'disabled' : 'loading',
})
: CompleteStep(),
]}
/>
</EuiPanel>
);
};

View file

@ -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';

View file

@ -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=<PATH_TO_CA> \\\\
--fleet-server-es-ca=<PATH_TO_ES_CERT> \\\\
--fleet-server-cert=<PATH_TO_FLEET_SERVER_CERT> \\\\
--fleet-server-cert-key=<PATH_TO_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=<PATH_TO_CA> \\\\
--fleet-server-es-ca=<PATH_TO_ES_CERT> \\\\
--fleet-server-cert=<PATH_TO_FLEET_SERVER_CERT> \\\\
--fleet-server-cert-key=<PATH_TO_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=<PATH_TO_CA> \\\\
--fleet-server-es-ca=<PATH_TO_ES_CERT> \\\\
--fleet-server-cert=<PATH_TO_FLEET_SERVER_CERT> \\\\
--fleet-server-cert-key=<PATH_TO_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"
`);
});
});

View file

@ -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=<PATH_TO_CA>`;
commandArguments += ` \\\n --fleet-server-es-ca=<PATH_TO_ES_CERT>`;
commandArguments += ` \\\n --fleet-server-cert=<PATH_TO_FLEET_SERVER_CERT>`;
commandArguments += ` \\\n --fleet-server-cert-key=<PATH_TO_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 '';
}
}

View file

@ -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"`
);
});
});

View file

@ -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: (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.generateServiceTokenDescription"
defaultMessage="A service token grants Fleet Server permissions to write to Elasticsearch."
/>
</EuiText>
<EuiSpacer size="m" />
{!serviceToken ? (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={isLoadingServiceToken}
isDisabled={isLoadingServiceToken}
onClick={() => {
getServiceToken();
}}
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.generateServiceTokenButton"
defaultMessage="Generate service token"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiCallOut size="s">
<FormattedMessage
id="xpack.fleet.fleetServerSetup.saveServiceTokenDescription"
defaultMessage="Save your service token information. This will be shown only once."
/>
</EuiCallOut>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<strong>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.serviceTokenLabel"
defaultMessage="Service token"
/>
</strong>
</EuiFlexItem>
<FlexItemWithMinWidth>
<EuiCodeBlock paddingSize="m" isCopyable>
<CommandCode>{serviceToken}</CommandCode>
</EuiCodeBlock>
</FlexItemWithMinWidth>
</EuiFlexGroup>
</>
)}
</>
),
};
};
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 ? (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.installAgentDescription"
defaultMessage="From the agent directory, copy and run the appropriate quick start command to start an Elastic Agent as a Fleet Server using the generated token and a self-signed certificate. See the {userGuideLink} for instructions on using your own certificates for production deployment. All commands require administrator privileges."
values={{
userGuideLink: (
<EuiLink
href={docLinks.links.fleet.fleetServerAddFleetServer}
external
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSelect
prepend={
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.platformSelectLabel"
defaultMessage="Platform"
/>
</EuiText>
}
options={PLATFORM_OPTIONS}
value={platform}
onChange={(e) => setPlatform(e.target.value as PLATFORM_TYPE)}
aria-label={i18n.translate('xpack.fleet.fleetServerSetup.platformSelectAriaLabel', {
defaultMessage: 'Platform',
})}
/>
<EuiSpacer size="s" />
<EuiCodeBlock
fontSize="m"
isCopyable={true}
paddingSize="m"
language="console"
whiteSpace="pre"
>
<CommandCode>{installCommand}</CommandCode>
</EuiCodeBlock>
<EuiSpacer size="s" />
<EuiText>
<FormattedMessage
id="xpack.fleet.enrollmentInstructions.troubleshootingText"
defaultMessage="If you are having trouble connecting, see our {link}."
values={{
link: (
<EuiLink target="_blank" external href={docLinks.links.fleet.troubleshooting}>
<FormattedMessage
id="xpack.fleet.enrollmentInstructions.troubleshootingLink"
defaultMessage="troubleshooting guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
</>
) : 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<string>();
const [isLoadingServiceToken, setIsLoadingServiceToken] = useState<boolean>(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 (
<EuiPanel paddingSize="l" grow={false} hasShadow={false} hasBorder={true}>
<EuiSpacer size="s" />
<EuiText className="eui-textCenter">
<h2>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupTitle"
defaultMessage="Add a Fleet Server"
/>
</h2>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupText"
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the {userGuideLink} for more information."
values={{
userGuideLink: (
<EuiLink
href={docLinks.links.fleet.fleetServerAddFleetServer}
external
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.setupGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSteps
className="eui-textLeft"
steps={[
DownloadStep(),
ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }),
FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }),
]}
/>
</EuiPanel>
);
};
const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => {
const { docLinks } = useStartServices();
return (
<EuiPanel
paddingSize="none"
grow={false}
hasShadow={false}
hasBorder={true}
className="eui-textCenter"
>
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.cloudSetupTitle"
defaultMessage="Enable APM & Fleet"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.fleet.fleetServerSetup.cloudSetupText"
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}"
values={{
link: (
<EuiLink
href={docLinks.links.fleet.fleetServerAddFleetServer}
target="_blank"
external
>
<FormattedMessage
id="xpack.fleet.settings.userGuideLink"
defaultMessage="Fleet User Guide"
/>
</EuiLink>
),
}}
/>
}
actions={
<>
<EuiButton
iconSide="right"
iconType="popout"
fill
isLoading={false}
type="submit"
href={`${deploymentUrl}/edit`}
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.cloudDeploymentLink"
defaultMessage="Edit deployment"
/>
</EuiButton>
</>
}
/>
</EuiPanel>
);
};
export const FleetServerRequirementPage = () => {
const startService = useStartServices();
const deploymentUrl = startService.cloud?.deploymentUrl;
@ -411,23 +38,7 @@ export const FleetServerRequirementPage = () => {
<OnPremInstructions />
)}
</FlexItemWithMinWidth>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.fleet.fleetServerSetup.waitingText"
defaultMessage="Waiting for a Fleet Server to connect..."
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</ContentWrapper>
<EuiSpacer size="xxl" />
</>
);
};

View file

@ -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';

View file

@ -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 <Loading />;
@ -105,6 +96,27 @@ export const AgentsApp: React.FunctionComponent = () => {
return <NoAccessPage />;
}
const rightColumn = hasOnlyFleetServerMissingRequirement ? (
<>
{isEnrollmentFlyoutOpen && (
<EuiPortal>
<AgentEnrollmentFlyout
defaultMode="standalone"
agentPolicies={agentPolicies}
onClose={() => setIsEnrollmentFlyoutOpen(false)}
/>
</EuiPortal>
)}
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton fill iconType="plusInCircle" onClick={() => setIsEnrollmentFlyoutOpen(true)}>
<FormattedMessage id="xpack.fleet.addAgentButton" defaultMessage="Add Agent" />
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
) : undefined;
return (
<Router>
<Switch>
@ -112,7 +124,7 @@ export const AgentsApp: React.FunctionComponent = () => {
<AgentDetailsPage />
</Route>
<Route path={FLEET_ROUTING_PATHS.agents}>
<DefaultLayout section="agents">
<DefaultLayout section="agents" rightColumn={rightColumn}>
{fleetServerModalVisible && (
<FleetServerUpgradeModal onClose={onCloseFleetServerModal} />
)}

View file

@ -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(),
};
}
);

View file

@ -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';

View file

@ -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<Props> = ({
agentPolicy,
agentPolicies,
viewDataStepContent,
defaultMode = 'managed',
}) => {
const [mode, setMode] = useState<'managed' | 'standalone'>('managed');
const [mode, setMode] = useState<FlyoutMode>(defaultMode);
const { modal } = useUrlModal();
const [lastModal, setLastModal] = useState(modal);

View file

@ -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<Props>(
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<Props>(
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<Props>(
: 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<Props>(
return baseSteps;
}, [
agentPolicy,
agentPolicies,
selectedApiKeyId,
setSelectedAPIKeyId,
viewDataStepContent,
agentPolicies,
apiKey.data,
fleetServerSteps,
isFleetServerPolicySelected,
settings.data?.item?.fleet_server_hosts,
fleetServerInstructions,
viewDataStepContent,
]);
return (

View file

@ -27,7 +27,7 @@ export const DownloadStep = () => {
<EuiText>
<FormattedMessage
id="xpack.fleet.agentEnrollment.downloadDescription"
defaultMessage="You can download the agent binaries and their verification signatures from the Elastic Agent download page."
defaultMessage="Fleet Server runs on an Elastic Agent. You can download the Elastic Agent binaries and verification signatures from Elastics download page."
/>
</EuiText>
<EuiSpacer size="l" />

View file

@ -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 (
<FleetStatusContext.Provider value={{ ...state, refresh: () => sendGetStatus() }}>
<FleetStatusContext.Provider value={{ ...state, refresh }}>
{children}
</FleetStatusContext.Provider>
);