Skip to content

Commit

Permalink
Agent enrollment flyout (#48173)
Browse files Browse the repository at this point in the history
* Fix agent table empty prompt

* First pass at agent enrollment flyout with placeholders

* Move enrollment commands into separate file and enable templated vars

* Use default double braces for templating
  • Loading branch information
jen-huang authored Oct 16, 2019
1 parent fb77f20 commit 1a4375c
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
EuiSpacer,
EuiText,
EuiFilterGroup,
EuiFilterButton,
} from '@elastic/eui';
import { FrontendLibs } from '../../../lib/types';
import {
ShellEnrollmentInstructions,
ContainerEnrollmentInstructions,
ToolsEnrollmentInstructions,
} from './enrollment_instructions';

interface RouterProps {
libs: FrontendLibs;
onClose: () => void;
}

export const AgentEnrollmentFlyout: React.SFC<RouterProps> = ({ libs, onClose }) => {
const [quickInstallType, setQuickInstallType] = useState<'shell' | 'container' | 'tools'>(
'shell'
);

const renderHeader = () => (
<EuiFlyoutHeader hasBorder aria-labelledby="FleetAgentEnrollmentFlyoutTitle">
<EuiTitle size="m">
<h2 id="FleetAgentEnrollmentFlyoutTitle">
<FormattedMessage
id="xpack.fleet.agentEnrollment.flyoutTitle"
defaultMessage="Install agent"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.agentEnrollment.flyoutDescription"
defaultMessage="Enroll a new agent into Fleet."
/>
</p>
</EuiText>
</EuiFlyoutHeader>
);

const renderInstructions = () => (
<Fragment>
<EuiText>
<h5>
<FormattedMessage
id="xpack.fleet.agentEnrollment.quickInstallTitle"
defaultMessage="Quick installation"
/>
</h5>
</EuiText>
<EuiSpacer size="s" />
<EuiFilterGroup>
<EuiFilterButton
hasActiveFilters={quickInstallType === 'shell'}
onClick={() => setQuickInstallType('shell')}
>
<FormattedMessage
id="xpack.fleet.agentEnrollment.shellInstallButtonText"
defaultMessage="Shell"
/>
</EuiFilterButton>
<EuiFilterButton
hasActiveFilters={quickInstallType === 'container'}
onClick={() => setQuickInstallType('container')}
>
<FormattedMessage
id="xpack.fleet.agentEnrollment.containerInstallButtonText"
defaultMessage="Container"
/>
</EuiFilterButton>
<EuiFilterButton
hasActiveFilters={quickInstallType === 'tools'}
onClick={() => setQuickInstallType('tools')}
>
<FormattedMessage
id="xpack.fleet.agentEnrollment.toolsInstallButtonText"
defaultMessage="Tools"
/>
</EuiFilterButton>
</EuiFilterGroup>
<EuiSpacer size="m" />
{quickInstallType === 'shell' ? (
<ShellEnrollmentInstructions
kibanaUrl={`${window.location.origin}${libs.framework.info.basePath}`}
/>
) : null}
{quickInstallType === 'container' ? <ContainerEnrollmentInstructions /> : null}
{quickInstallType === 'tools' ? <ToolsEnrollmentInstructions /> : null}
</Fragment>
);

const renderBody = () => <EuiFlyoutBody>{renderInstructions()}</EuiFlyoutBody>;

const renderFooter = () => (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onClose}>
Continue
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);

return (
<EuiFlyout onClose={onClose} size="m" maxWidth={650}>
{renderHeader()}
{renderBody()}
{renderFooter()}
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';

export const ContainerEnrollmentInstructions: React.SFC = () => {
return <div>Container instructions</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiSteps, EuiText, EuiCodeBlock } from '@elastic/eui';
import { Writer } from 'mustache';

export { ShellEnrollmentInstructions } from './shell';
export { ContainerEnrollmentInstructions } from './container';
export { ToolsEnrollmentInstructions } from './tools';

export type ManualEnrollmentInstructions = Array<{
title: string;
textPre?: string;
commands?: string;
commandsLang?: 'bash' | 'yaml';
}>;

export const ManualEnrollmentSteps: React.SFC<{ instructions: ManualEnrollmentInstructions }> = ({
instructions,
}) => (
<EuiSteps
steps={instructions.map(({ title, textPre, commands, commandsLang }) => ({
title,
children: (
<EuiText size="s">
{textPre ? <p>{textPre}</p> : null}
{commands ? (
// TODO: Increase overflowHeight when https://github.com/elastic/eui/issues/2435 is fixed
// or be smarter with setting this number before release
<EuiCodeBlock overflowHeight={150} language={commandsLang || 'bash'} isCopyable>
{replaceTemplateStrings(commands.trim())}
</EuiCodeBlock>
) : null}
</EuiText>
),
}))}
/>
);

// Setup for replacing template variables in install instructions
const mustacheWriter = new Writer();

// do not html escape output
// @ts-ignore
mustacheWriter.escapedValue = function escapedValue(token, context) {
const value = context.lookup(token[1]);
if (value != null) {
return value;
}
};

// Configure available variable values
export function replaceTemplateStrings(text: string = '') {
const variables = {
config: {
enrollmentToken: 'sometesttoken',
},
};
mustacheWriter.parse(text);
return mustacheWriter.render(text, variables, () => {});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, Fragment } from 'react';
import {
EuiFieldText,
EuiCopy,
EuiButtonEmpty,
EuiPopover,
EuiSpacer,
EuiContextMenuPanel,
EuiContextMenuItem,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { ManualEnrollmentInstructions, ManualEnrollmentSteps } from '../';
import * as MAC_COMMANDS from './mac_commands';

// No need for i18n as these are platform names
const PLATFORMS = {
macos: 'macOS',
windows: 'Windows',
linux: 'Linux',
};

// Manual instructions based on platform
const PLATFORM_INSTRUCTIONS: {
[key: string]: ManualEnrollmentInstructions;
} = {
macos: [
{
title: i18n.translate('xpack.fleet.agentEnrollment.typeShell.manualInstall.stepOneTitle', {
defaultMessage: 'Download and install Elastic Agent',
}),
textPre: 'Lorem ipsum instructions here.',
commands: MAC_COMMANDS.INSTALL,
},
{
title: i18n.translate('xpack.fleet.agentEnrollment.typeShell.manualInstall.stepTwoTitle', {
defaultMessage: 'Edit the configuration',
}),
textPre: 'Modify the configuration file to set the connection information:',
commands: MAC_COMMANDS.CONFIG,
commandsLang: 'yaml',
},
{
title: i18n.translate('xpack.fleet.agentEnrollment.typeShell.manualInstall.stepThreeTitle', {
defaultMessage: 'Start the agent',
}),
commands: MAC_COMMANDS.START,
},
],
};

interface Props {
kibanaUrl: string;
}

export const ShellEnrollmentInstructions: React.SFC<Props> = ({ kibanaUrl }) => {
// Platform state
const [currentPlatform, setCurrentPlatform] = useState<keyof typeof PLATFORMS>('macos');
const [isPlatformOptionsOpen, setIsPlatformOptionsOpen] = useState<boolean>(false);
const [isManualInstallationOpen, setIsManualInstallationOpen] = useState<boolean>(false);

// Build quick installation command
const quickInstallInstructions = `curl ${kibanaUrl}/api/fleet/install/${currentPlatform} | bash`;

return (
<Fragment>
<EuiFieldText
readOnly
value={quickInstallInstructions}
fullWidth
prepend={
<EuiPopover
button={
<EuiButtonEmpty
size="xs"
iconType="arrowDown"
iconSide="right"
onClick={() => setIsPlatformOptionsOpen(true)}
>
{PLATFORMS[currentPlatform]}
</EuiButtonEmpty>
}
isOpen={isPlatformOptionsOpen}
closePopover={() => setIsPlatformOptionsOpen(false)}
>
<EuiContextMenuPanel
items={Object.entries(PLATFORMS).map(([platform, name]) => (
<EuiContextMenuItem
key={platform}
onClick={() => {
setCurrentPlatform(platform as typeof currentPlatform);
setIsPlatformOptionsOpen(false);
}}
>
{name}
</EuiContextMenuItem>
))}
/>
</EuiPopover>
}
append={
<EuiCopy textToCopy={quickInstallInstructions}>
{copy => (
<EuiButtonEmpty onClick={copy} color="primary" size="s">
<FormattedMessage
id="xpack.fleet.agentEnrollment.copyInstructionsButtonText"
defaultMessage="copy"
/>
</EuiButtonEmpty>
)}
</EuiCopy>
}
/>

<EuiSpacer size="m" />

<EuiButtonEmpty
onClick={() => setIsManualInstallationOpen(!isManualInstallationOpen)}
iconType={isManualInstallationOpen ? 'arrowUp' : 'arrowDown'}
iconSide="right"
size="xs"
flush="left"
>
<FormattedMessage
id="xpack.fleet.agentEnrollment.manualInstructionsToggleLinkText"
defaultMessage="Manual installation"
/>
</EuiButtonEmpty>

{isManualInstallationOpen ? (
<Fragment>
<EuiSpacer size="m" />
<ManualEnrollmentSteps
instructions={PLATFORM_INSTRUCTIONS[currentPlatform] || PLATFORM_INSTRUCTIONS.macos}
/>
</Fragment>
) : null}
</Fragment>
);
};
Loading

0 comments on commit 1a4375c

Please sign in to comment.