Skip to content

Commit

Permalink
[Fleet] Add redesigned Fleet Server flyout (#127786)
Browse files Browse the repository at this point in the history
* WIP new fleet server flyout

* WIP finish up quick start tab

* Refactor quick start steps into separate files:

* Initial refactor of existing fleet server instructions

* Move quick start form return value to explicit type

* Flesh out fleet server commands

* Fix translation error

* Migrate on prem instructions component over to new file structure

* Makes quick start tab actually create policy

* Fix type errors

* Fix missing hooks + update snapshots

* Fix paths in mocks

* Fix translations

* WIP test fixes

* Implement enabled/disabled state for new steps

* Fix cypress tests

* Force re-render to get last test passing

* Fix failing tests

* Fix import errors after conflicts

* Fix snapshot tests

* Use id instead of full policy for policy selector

* Replace Fleet Server instructions w/ Advanced Tab contents

* First pass at integrating add agent/fleet server flyouts

* Test commit

* Fix imports

* Fix imports

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Enforce https-only fleet server URL's + improve errors

* Fix failing tests

* Fix fleet server command in quick start

* Show success state in Quick start when policy exists + use first fleet server host if it exists

* Set initial service token value when Fleet Server policy ID is initiall set

* Generate service token instead of enrollment token

* Fix fleet server flyout opening from unhealthy callout

* Revert service token change + use EuiComboBox for fleet server host

* Fix checks + use custom option text

* Move fleet server host combobox to component

* Use new combobox in advanced tab

* Fix translations

* Fix unused import

* Don't recreate quick start policy if it already exists

* Actually use quick start policy fields 🙃

* Fix policy check

* Update x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/components/fleet_server_host_combobox.tsx

Co-authored-by: Mark Hopkin <[email protected]>

* Update x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/install_fleet_server.tsx

Co-authored-by: Mark Hopkin <[email protected]>

* Update x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/set_deployment_mode.tsx

Co-authored-by: Mark Hopkin <[email protected]>

* Fix formatting issue

* Clean up fleet server settings variable declaration per PR review

Co-authored-by: Kibana Machine <[email protected]>
Co-authored-by: Mark Hopkin <[email protected]>
  • Loading branch information
3 people authored Apr 26, 2022
1 parent ac8df39 commit b655b48
Show file tree
Hide file tree
Showing 61 changed files with 2,262 additions and 1,137 deletions.
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/.storybook/context/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export const getCloud = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => {
const cloud: CloudSetup = {
isCloudEnabled,
baseUrl: 'https://base.url',
cloudId: 'cloud-id',
cloudId: isCloudEnabled ? 'cloud-id' : undefined,
cname: 'found.io',
deploymentUrl: 'https://deployment.url',
deploymentUrl: isCloudEnabled ? 'https://deployment.url' : undefined,
organizationUrl: 'https://organization.url',
profileUrl: 'https://profile.url',
snapshotsUrl: 'https://snapshots.url',
Expand Down
56 changes: 53 additions & 3 deletions x-pack/plugins/fleet/.storybook/context/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ export const getHttp = (basepath = BASE_PATH) => {
serverBasePath: basepath,
},
get: (async (path: string, options: HttpFetchOptions) => {
action('get')(path, options);
action('get')(path, JSON.stringify(options));
// TODO: all of this needs revision, as it's far too clunky... but it works for now,
// with the few paths we're supporting.
if (path === '/api/fleet/agents/setup') {
if (!isReady) {
isReady = true;
return { isReady: false, missing_requirements: ['api_keys', 'fleet_server'] };
}
return { isInitialized: true, nonFatalErrors: [] };
return { isReady: true, isInitialized: true, nonFatalErrors: [], missing_requirements: [] };
}

if (path === '/api/fleet/epm/categories') {
Expand Down Expand Up @@ -79,9 +79,59 @@ export const getHttp = (basepath = BASE_PATH) => {
return { success: true };
}

action(path)('KP: UNSUPPORTED ROUTE');
if (path.match('/api/fleet/agent_policies')) {
return { items: [] };
}

if (path.match('/api/fleet/settings')) {
return { item: { fleet_server_hosts: [] } };
}

if (path.match('/api/fleet/outputs')) {
return {
items: [{ name: 'Default Output', is_default: true, hosts: ['https://test.es:9200'] }],
};
}

action(path)(`UNSUPPORTED ROUTE: GET ${path}`);
return {};
}) as HttpHandler,
post: (async (path: string, options: HttpFetchOptions) => {
action('post')(path, JSON.stringify(options));

if (path.match('/api/fleet/settings')) {
return { items: [] };
}

if (path.match('/api/fleet/service_tokens')) {
return {
name: 'test-token',
value: 'test-token-value',
};
}

if (path.match('/api/fleet/agent_policies')) {
return {
item: {
id: 'test-policy',
name: 'Test Policy',
namespace: 'default',
description: 'Test Policy',
monitoring_enabled: ['metrics'],
data_output_id: 'test-output',
monitoring_output_id: 'test-output',
status: 'active',
packagePolicies: ['test-package-policy'],
updated_on: new Date(),
updated_by: 'elastic',
revision: 0,
agents: 0,
},
};
}

action(path)(`UNSUPPORTED ROUTE: POST ${path}`);
}) as HttpHandler,
} as unknown as HttpStart;

return http;
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/.storybook/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
chrome: getChrome(),
cloud: {
...getCloud({ isCloudEnabled }),
CloudContextProvider: () => <></>,
CloudContextProvider: ({ children }) => <>{children}</>,
},
customIntegrations: {
ContextProvider: getStorybookContextProvider(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ describe('Edit settings', () => {

it('should update Fleet server hosts', () => {
cy.getBySel('editHostsBtn').click();
cy.get('[placeholder="Specify host URL"').type('http://localhost:8220');
cy.get('[placeholder="Specify host URL"').type('https://localhost:8220');

cy.intercept('/api/fleet/settings', {
item: { id: 'fleet-default-settings', fleet_server_hosts: ['http://localhost:8220'] },
item: { id: 'fleet-default-settings', fleet_server_hosts: ['https://localhost:8220'] },
});
cy.intercept('PUT', '/api/fleet/settings', {
fleet_server_hosts: ['http://localhost:8220'],
fleet_server_hosts: ['https://localhost:8220'],
}).as('updateSettings');

cy.getBySel('saveApplySettingsBtn').click();
cy.getBySel(CONFIRM_MODAL_BTN).click();

cy.wait('@updateSettings').then((interception) => {
expect(interception.request.body.fleet_server_hosts[0]).to.equal('http://localhost:8220');
expect(interception.request.body.fleet_server_hosts[0]).to.equal('https://localhost:8220');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ describe('Fleet startup', () => {
});

it('should create Fleet Server policy', () => {
cy.getBySel('fleetServerFlyoutTab-advanced').click();
cy.getBySel('createFleetServerPolicyBtn').click();

// verify policy is created and has fleet server and system package
verifyPolicy('Fleet Server policy 1', ['Fleet Server', 'System']);

navigateToTab(AGENTS_TAB);
cy.getBySel('fleetServerFlyoutTab-advanced').click();

// verify create button changed to dropdown
cy.getBySel('agentPolicyDropdown');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 { EuiSteps } from '@elastic/eui';
import React from 'react';

import { useAdvancedForm } from './hooks';

import {
getAddFleetServerHostStep,
getSelectAgentPolicyStep,
getGenerateServiceTokenStep,
getSetDeploymentModeStep,
getInstallFleetServerStep,
getConfirmFleetServerConnectionStep,
} from './steps';

export const AdvancedTab: React.FunctionComponent = () => {
const {
eligibleFleetServerPolicies,
refreshEligibleFleetServerPolicies,
fleetServerPolicyId,
setFleetServerPolicyId,
isFleetServerReady,
serviceToken,
isLoadingServiceToken,
generateServiceToken,
fleetServerHostForm,
deploymentMode,
setDeploymentMode,
} = useAdvancedForm();

const steps = [
getSelectAgentPolicyStep({
policyId: fleetServerPolicyId,
setPolicyId: setFleetServerPolicyId,
eligibleFleetServerPolicies,
refreshEligibleFleetServerPolicies,
}),
getSetDeploymentModeStep({
deploymentMode,
setDeploymentMode,
disabled: !Boolean(fleetServerPolicyId),
}),
getAddFleetServerHostStep({ fleetServerHostForm, disabled: !Boolean(fleetServerPolicyId) }),
getGenerateServiceTokenStep({
serviceToken,
generateServiceToken,
isLoadingServiceToken,
disabled: !Boolean(fleetServerHostForm.isFleetServerHostSubmitted),
}),
getInstallFleetServerStep({
isFleetServerReady,
serviceToken,
fleetServerHost: fleetServerHostForm.fleetServerHost,
fleetServerPolicyId,
disabled: !Boolean(serviceToken),
}),
getConfirmFleetServerConnectionStep({ isFleetServerReady, disabled: !Boolean(serviceToken) }),
];

return <EuiSteps steps={steps} className="eui-textLeft" />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 } from 'react';
import type { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';

interface Props {
fleetServerHost: string | undefined;
fleetServerHostSettings: string[];
isDisabled: boolean;
isInvalid: boolean;
onFleetServerHostChange: (host: string) => void;
}

export const FleetServerHostComboBox: React.FunctionComponent<Props> = ({
fleetServerHost,
fleetServerHostSettings,
isDisabled = false,
isInvalid = false,
onFleetServerHostChange,
}) => {
// Track options that are created inline
const [createdOptions, setCreatedOptions] = useState<string[]>([]);

const options = [...createdOptions, ...fleetServerHostSettings].map((option) => ({
label: option,
value: option,
}));

const handleChange = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
const host = selectedOptions[0].value ?? '';
onFleetServerHostChange(host);
};

const handleCreateOption = (option: string) => {
setCreatedOptions([...createdOptions, option]);
onFleetServerHostChange(option);
};

return (
<EuiComboBox<string>
fullWidth
isClearable={false}
singleSelection={{ asPlainText: true }}
placeholder="https://fleet-server-host.com:8220"
options={options}
customOptionText={i18n.translate(
'xpack.fleet.fleetServerSetup.addFleetServerHostCustomOptionText',
{
defaultMessage: 'Add {searchValuePlaceholder} as a new Fleet Server host',
values: { searchValuePlaceholder: '{searchValue}' },
}
)}
selectedOptions={fleetServerHost ? [{ label: fleetServerHost, value: fleetServerHost }] : []}
prepend={
<EuiText>
<FormattedMessage
id="xpack.fleet.fleetServerSetup.addFleetServerHostInputLabel"
defaultMessage="Fleet Server host"
/>
</EuiText>
}
noSuggestions={fleetServerHostSettings.length === 0}
data-test-subj="fleetServerHostInput"
isDisabled={isDisabled}
isInvalid={isInvalid}
onChange={handleChange}
onCreateOption={handleCreateOption}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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 * from './fleet_server_host_combobox';
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { EuiButton } from '@elastic/eui';

import { FleetServerFlyout as FleetServerFlyoutComponent } from '.';

export const FleetServerFlyout = () => {
const [isOpen, setIsOpen] = React.useState(false);

return (
<div style={{ width: 900 }}>
<EuiButton size="m" fill color="primary" onClick={() => setIsOpen(true)}>
Show flyout
</EuiButton>
{isOpen && <FleetServerFlyoutComponent onClose={() => setIsOpen(false)} />}
</div>
);
};

FleetServerFlyout.args = {
isCloudEnabled: false,
};

export default {
component: FleetServerFlyout,
title: 'Sections/Fleet/Agents/Fleet Server Instructions/In Flyout',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.
*/

// These hooks are tightly coupled to each tab of the Fleet server instructions component, and provide
// all necessary data to drive those UI's
export * from './use_advanced_form';
export * from './use_quick_start_form';

// These are individual hooks for one-off consumption. These are typically composed in the hooks above,
// but exported here to support individual usage.
export * from './use_wait_for_fleet_server';
export * from './use_select_fleet_server_policy';
export * from './use_service_token';
export * from './use_fleet_server_host';
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 { useState } from 'react';

import type { DeploymentMode } from '../steps';

import { useFleetServerHost } from './use_fleet_server_host';
import { useSelectFleetServerPolicy } from './use_select_fleet_server_policy';
import { useServiceToken } from './use_service_token';
import { useWaitForFleetServer } from './use_wait_for_fleet_server';

/**
* Provides all data/state required for the "advanced" tab in the Fleet Server instructions/flyout
*/
export const useAdvancedForm = (defaultAgentPolicyId?: string) => {
const {
eligibleFleetServerPolicies,
refreshEligibleFleetServerPolicies,
fleetServerPolicyId,
setFleetServerPolicyId,
} = useSelectFleetServerPolicy(defaultAgentPolicyId);
const { isFleetServerReady } = useWaitForFleetServer();
const { serviceToken, isLoadingServiceToken, generateServiceToken } = useServiceToken();
const fleetServerHostForm = useFleetServerHost();

const [deploymentMode, setDeploymentMode] = useState<DeploymentMode>('quickstart');

return {
eligibleFleetServerPolicies,
refreshEligibleFleetServerPolicies,
fleetServerPolicyId,
setFleetServerPolicyId,
isFleetServerReady,
serviceToken,
isLoadingServiceToken,
generateServiceToken,
fleetServerHostForm,
deploymentMode,
setDeploymentMode,
};
};
Loading

0 comments on commit b655b48

Please sign in to comment.