Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive setup mode #106881

Merged
merged 45 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
caa9ad3
Interactive setup mode
thomheymann Jul 14, 2021
34672fc
remove first attempt
thomheymann Jul 27, 2021
09d1f95
Added suggestions from code review
thomheymann Jul 28, 2021
4177cef
Verify CA before writing config
thomheymann Jul 30, 2021
185660d
fix i18n path
thomheymann Jul 30, 2021
4e7b041
updated plugin list
thomheymann Jul 30, 2021
5223dfa
Updated page bundle limits
thomheymann Jul 30, 2021
decec27
added manual configuration
thomheymann Aug 2, 2021
f2c42d4
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 2, 2021
56b068b
fetch certificate chain
thomheymann Aug 3, 2021
44d5432
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 4, 2021
309dd38
Fix race condition when calling multiple form methods
thomheymann Aug 4, 2021
da3daf9
Fix i18n errors
thomheymann Aug 4, 2021
44229d4
added types and refactored slightly
thomheymann Aug 9, 2021
7a3c2d9
welcome page styling
thomheymann Aug 9, 2021
21ffd73
Remove holdsetup config option
thomheymann Aug 9, 2021
8837a12
typo
thomheymann Aug 9, 2021
312c287
fix build errors
thomheymann Aug 10, 2021
b0d35b1
Merge branch 'master' into interactive-setup
thomheymann Aug 10, 2021
ba00e1e
Updated manual configuration form
thomheymann Aug 11, 2021
3867ce6
Remove issuer link
thomheymann Aug 11, 2021
9703fe0
Add tests for decode enrollment token
thomheymann Aug 11, 2021
a76873d
Removed unused class names
thomheymann Aug 11, 2021
96a282d
fix issue where credentials got inherited from base config
thomheymann Aug 12, 2021
d0adcab
Added tooltips and text overflow
thomheymann Aug 12, 2021
dbc2cd1
styling fixes
thomheymann Aug 12, 2021
eabab62
refactored text truncate
thomheymann Aug 16, 2021
180a0ab
Added unit tests
thomheymann Aug 17, 2021
0ebb990
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 17, 2021
6ee2096
added suggestions from code review
thomheymann Aug 23, 2021
1fae7fc
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 24, 2021
a732fbb
Fixed typo and tests
thomheymann Aug 24, 2021
1038b0c
Styling fixes
thomheymann Aug 24, 2021
57d46be
Fix i18n errors
thomheymann Aug 24, 2021
bf97edc
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 24, 2021
b7b0837
Added suggestions from code review
thomheymann Aug 25, 2021
a67e2de
Added route tests
thomheymann Aug 25, 2021
f0f2aa2
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 25, 2021
a14c140
Explicit type exports
thomheymann Aug 25, 2021
d14ea4e
Fix server url
thomheymann Aug 25, 2021
993c11c
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 27, 2021
bbb89d2
Added unit tests
thomheymann Aug 29, 2021
a45adb7
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 29, 2021
0f25dc6
Added product not supported scenario
thomheymann Aug 30, 2021
8e504ca
Merge branch 'master' of github.com:elastic/kibana into interactive-s…
thomheymann Aug 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ pageLoadAssetSize:
expressionImage: 19288
expressionMetric: 22238
expressionShape: 34008
interactiveSetup: 18532
interactiveSetup: 70000
expressionTagcloud: 27505
expressions: 239290
securitySolution: 231753
2 changes: 1 addition & 1 deletion src/plugins/interactive_setup/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
* Side Public License, v 1.
*/

export type { InteractiveSetupViewState, EnrollmentToken } from './types';
export type { InteractiveSetupViewState, EnrollmentToken, Certificate, PingResult } from './types';
export { ElasticsearchConnectionStatus } from './elasticsearch_connection_status';
23 changes: 23 additions & 0 deletions src/plugins/interactive_setup/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Side Public License, v 1.
*/

import type { PeerCertificate } from 'tls';

import type { ElasticsearchConnectionStatus } from './elasticsearch_connection_status';

/**
Expand Down Expand Up @@ -43,3 +45,24 @@ export interface EnrollmentToken {
*/
key: string;
}

export interface Certificate {
issuer: Partial<PeerCertificate['issuer']>;
valid_from: PeerCertificate['valid_from'];
valid_to: PeerCertificate['valid_to'];
subject: Partial<PeerCertificate['subject']>;
fingerprint256: PeerCertificate['fingerprint256'];
raw: string;
}

export interface PingResult {
/**
* Indicates whether the cluster requires authentication.
*/
authRequired: boolean;

/**
* Full certificate chain of cluster at requested address. Only present if cluster uses HTTPS.
*/
certificateChain?: Certificate[];
}
26 changes: 26 additions & 0 deletions src/plugins/interactive_setup/public/app.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.interactiveSetup {
@include kibanaFullScreenGraphics;
}

.interactiveSetup__header {
position: relative;
z-index: 10;
padding: $euiSizeXL;
}

.interactiveSetup__logo {
@include kibanaCircleLogo;
@include euiBottomShadowMedium;

margin-bottom: $euiSizeXL;
}

.interactiveSetup__content {
position: relative;
z-index: 10;
margin: auto;
margin-bottom: $euiSizeXL;
max-width: map-get($euiBreakpoints, 's') - $euiSizeXL;
padding-left: $euiSizeXL;
padding-right: $euiSizeXL;
}
thomheymann marked this conversation as resolved.
Show resolved Hide resolved
84 changes: 69 additions & 15 deletions src/plugins/interactive_setup/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,76 @@
* Side Public License, v 1.
*/

import { EuiPageTemplate, EuiPanel, EuiText } from '@elastic/eui';
import React from 'react';
import './app.scss';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: not for this PR, just so that I don't forget about these two points we should do for alpha2:

  • Redirect to original URL after successful configuration
  • Automatically redirect (with a warning or something) to original URL if we detect that Kibana can connect to ES while user is in interactive setup UI


import { EuiIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React, { useState } from 'react';

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

import { ClusterAddressForm } from './cluster_address_form';
import type { ClusterConfigurationFormProps } from './cluster_configuration_form';
import { ClusterConfigurationForm } from './cluster_configuration_form';
import { EnrollmentTokenForm } from './enrollment_token_form';
import { ProgressIndicator } from './progress_indicator';

export const App: FunctionComponent = () => {
const [page, setPage] = useState<'token' | 'manual' | 'success'>('token');
const [cluster, setCluster] = useState<
Omit<ClusterConfigurationFormProps, 'onCancel' | 'onSuccess'>
>();

export const App = () => {
return (
<EuiPageTemplate
restrictWidth={false}
template="empty"
pageHeader={{
iconType: 'logoElastic',
pageTitle: 'Welcome to Elastic',
}}
>
<EuiPanel>
<EuiText>Kibana server is not ready yet.</EuiText>
</EuiPanel>
</EuiPageTemplate>
<div className="interactiveSetup">
<header className="interactiveSetup__header eui-textCenter">
<EuiSpacer size="xxl" />
<span className="interactiveSetup__logo">
<EuiIcon type="logoElastic" size="xxl" />
</span>
<EuiTitle size="m">
<h1>
<FormattedMessage
id="interactiveSetup.app.pageTitle"
defaultMessage="Configure Elastic to get started"
/>
</h1>
</EuiTitle>
<EuiSpacer size="xl" />
</header>
<div className="interactiveSetup__content">
<EuiPanel paddingSize="l">
<div hidden={page !== 'token'}>
thomheymann marked this conversation as resolved.
Show resolved Hide resolved
<EnrollmentTokenForm
onCancel={() => setPage('manual')}
onSuccess={() => setPage('success')}
/>
</div>
<div hidden={page !== 'manual'}>
{cluster ? (
<ClusterConfigurationForm
onCancel={() => setCluster(undefined)}
onSuccess={() => setPage('success')}
{...cluster}
/>
) : (
<ClusterAddressForm
onCancel={() => setPage('token')}
onSuccess={(result, values) =>
setCluster({
host: values.host,
authRequired: result.authRequired,
certificateChain: result.certificateChain,
})
}
/>
)}
</div>
{page === 'success' && (
<ProgressIndicator onSuccess={() => window.location.replace(window.location.href)} />
)}
</EuiPanel>
</div>
</div>
);
};
70 changes: 70 additions & 0 deletions src/plugins/interactive_setup/public/cluster_address_form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';

import { coreMock } from 'src/core/public/mocks';

import { ClusterAddressForm } from './cluster_address_form';
import { Providers } from './plugin';

jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
azasypkin marked this conversation as resolved.
Show resolved Hide resolved

describe('ClusterAddressForm', () => {
jest.setTimeout(20_000);

it('calls enrollment API when submitting form', async () => {
const coreStart = coreMock.createStart();
coreStart.http.post.mockResolvedValue({});

const onSuccess = jest.fn();

const { findByRole, findByLabelText } = render(
<Providers http={coreStart.http}>
<ClusterAddressForm onSuccess={onSuccess} />
</Providers>
);
fireEvent.change(await findByLabelText('Address'), {
target: { value: 'https://localhost' },
});
fireEvent.click(await findByRole('button', { name: 'Check address', hidden: true }));

await waitFor(() => {
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/interactive_setup/ping', {
body: JSON.stringify({
host: 'https://localhost:9200',
}),
});
expect(onSuccess).toHaveBeenCalled();
});
});

it('validates form', async () => {
const coreStart = coreMock.createStart();
const onSuccess = jest.fn();

const { findAllByText, findByRole, findByLabelText } = render(
<Providers http={coreStart.http}>
<ClusterAddressForm onSuccess={onSuccess} />
</Providers>
);

fireEvent.change(await findByLabelText('Address'), {
target: { value: 'localhost' },
});

fireEvent.click(await findByRole('button', { name: 'Check address', hidden: true }));

await findAllByText(/Enter a valid address including protocol/i);
thomheymann marked this conversation as resolved.
Show resolved Hide resolved

expect(coreStart.http.post).not.toHaveBeenCalled();
});
});
146 changes: 146 additions & 0 deletions src/plugins/interactive_setup/public/cluster_address_form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { IHttpFetchError } from 'kibana/public';

import type { PingResult } from '../common';
import type { ValidationErrors } from './use_form';
import { useForm } from './use_form';
import { useHttp } from './use_http';

export interface ClusterAddressFormValues {
host: string;
}

export interface ClusterAddressFormProps {
defaultValues?: ClusterAddressFormValues;
onCancel?(): void;
onSuccess?(result: PingResult, values: ClusterAddressFormValues): void;
}

export const ClusterAddressForm: FunctionComponent<ClusterAddressFormProps> = ({
defaultValues = {
host: 'https://localhost:9200',
},
onCancel,
onSuccess,
}) => {
const http = useHttp();

const [form, eventHandlers] = useForm({
defaultValues,
validate: async (values) => {
const errors: ValidationErrors<typeof values> = {};

if (!values.host) {
errors.host = i18n.translate('interactiveSetup.clusterAddressForm.hostRequiredError', {
defaultMessage: 'Enter an address.',
});
} else {
try {
const url = new URL(values.host);
if (!url.protocol || !url.hostname) {
throw new Error();
}
} catch (error) {
errors.host = i18n.translate('interactiveSetup.clusterAddressForm.hostInvalidError', {
defaultMessage: 'Enter a valid address including protocol.',
});
}
}

return errors;
},
onSubmit: async (values) => {
const url = new URL(values.host);
const host = `${url.protocol}//${url.hostname}:${url.port || 9200}`;

const result = await http.post<PingResult>('/internal/interactive_setup/ping', {
body: JSON.stringify({ host }),
});

onSuccess?.(result, { host });
},
});

return (
<EuiForm component="form" noValidate {...eventHandlers}>
{form.submitError && (
<>
<EuiCallOut
color="danger"
title={i18n.translate('interactiveSetup.clusterAddressForm.submitErrorTitle', {
defaultMessage: "Couldn't check address",
})}
>
{(form.submitError as IHttpFetchError).body?.message}
</EuiCallOut>
<EuiSpacer />
</>
)}

<EuiFormRow
label={i18n.translate('interactiveSetup.clusterAddressForm.hostLabel', {
defaultMessage: 'Address',
})}
error={form.errors.host}
isInvalid={form.touched.host && !!form.errors.host}
fullWidth
>
<EuiFieldText
name="host"
value={form.values.host}
isInvalid={form.touched.host && !!form.errors.host}
fullWidth
/>
</EuiFormRow>
<EuiSpacer />

<EuiFlexGroup responsive={false} justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty flush="right" iconType="arrowLeft" onClick={onCancel}>
<FormattedMessage
id="interactiveSetup.clusterAddressForm.cancelButton"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
type="submit"
isLoading={form.isSubmitting}
isDisabled={form.isSubmitted && form.isInvalid}
fill
>
<FormattedMessage
id="interactiveSetup.clusterAddressForm.submitButton"
defaultMessage="{isSubmitting, select, true{Checking address…} other{Check address}}"
values={{ isSubmitting: form.isSubmitting }}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
);
};
Loading