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

[Alerting UI] Don't wait for health check before showing Create Alert flyout #80996

Merged
merged 11 commits into from
Oct 26, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { HealthCheck } from './health_check';

import { act } from 'react-dom/test-utils';
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
import { HealthContextProvider } from '../context/health_context';

const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' };

Expand All @@ -20,9 +21,11 @@ describe('health check', () => {
http.get.mockImplementationOnce(() => new Promise(() => {}));

const { queryByText, container } = render(
<HealthCheck http={http} docLinks={docLinks}>
<p>{'shouldnt render'}</p>
</HealthCheck>
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks}>
<p>{'shouldnt render'}</p>
</HealthCheck>
</HealthContextProvider>
);
await act(async () => {
// wait for useEffect to run
Expand All @@ -32,13 +35,33 @@ describe('health check', () => {
expect(queryByText('shouldnt render')).not.toBeInTheDocument();
});

it('renders children immediately if waitForCheck is false', async () => {
http.get.mockImplementationOnce(() => new Promise(() => {}));

const { queryByText, container } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={false}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
);
await act(async () => {
// wait for useEffect to run
});

expect(container.getElementsByClassName('euiLoadingSpinner').length).toBe(0);
expect(queryByText('should render')).toBeInTheDocument();
});

it('renders children if keys are enabled', async () => {
http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true });

const { queryByText } = render(
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
);
await act(async () => {
// wait for useEffect to run
Expand All @@ -53,9 +76,11 @@ describe('health check', () => {
}));

const { queryAllByText } = render(
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
);
await act(async () => {
// wait for useEffect to run
Expand All @@ -81,9 +106,11 @@ describe('health check', () => {
}));

const { queryByText, queryByRole } = render(
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
);
await act(async () => {
// wait for useEffect to run
Expand All @@ -108,9 +135,11 @@ describe('health check', () => {
}));

const { queryByText } = render(
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
);
await act(async () => {
// wait for useEffect to run
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,39 @@ import { EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { AlertingFrameworkHealth } from '../../types';
import { health } from '../lib/alert_api';
import './health_check.scss';
import { useHealthContext } from '../context/health_context';

interface Props {
docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>;
http: HttpSetup;
inFlyout?: boolean;
waitForCheck?: boolean;
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
}

export const HealthCheck: React.FunctionComponent<Props> = ({
docLinks,
http,
children,
inFlyout = false,
waitForCheck = true,
}) => {
const { setLoadingHealthCheck } = useHealthContext();
const [alertingHealth, setAlertingHealth] = React.useState<Option<AlertingFrameworkHealth>>(none);

React.useEffect(() => {
(async function () {
setLoadingHealthCheck(true);
setAlertingHealth(some(await health({ http })));
setLoadingHealthCheck(false);
})();
}, [http]);
}, [http, setLoadingHealthCheck]);

const className = inFlyout ? 'alertingFlyoutHealthCheck' : 'alertingHealthCheck';

return pipe(
alertingHealth,
fold(
() => <EuiLoadingSpinner size="m" />,
() => (waitForCheck ? <EuiLoadingSpinner size="m" /> : <Fragment>{children}</Fragment>),
(healthCheck) => {
return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? (
<Fragment>{children}</Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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, { createContext, useCallback, useContext, useMemo, useState } from 'react';

export interface HealthContextValue {
loadingHealthCheck: boolean;
setLoadingHealthCheck: (loading: boolean) => void;
}

const defaultHealthContext: HealthContextValue = {
loadingHealthCheck: false,
setLoadingHealthCheck: (loading: boolean) => {
throw new Error(
'setLoadingHealthCheck was not initialized, set it when you invoke the context'
);
},
};

const HealthContext = createContext<HealthContextValue>(defaultHealthContext);

export const HealthContextProvider = ({ children }: { children: React.ReactNode }) => {
const [loading, setLoading] = useState<boolean>(false);

const setLoadingHealthCheck = useCallback((isLoading: boolean) => {
setLoading(isLoading);
}, []);

const value = useMemo(() => {
return { loadingHealthCheck: loading, setLoadingHealthCheck };
}, [loading, setLoadingHealthCheck]);

return <HealthContext.Provider value={value}>{children}</HealthContext.Provider>;
};

export const useHealthContext = () => {
const ctx = useContext(HealthContext);
if (!ctx) {
throw new Error('HealthContext has not been set.');
}
return ctx;
};
17 changes: 11 additions & 6 deletions x-pack/plugins/triggers_actions_ui/public/application/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ActionsConnectorsList } from './sections/actions_connectors_list/compon
import { AlertsList } from './sections/alerts_list/components/alerts_list';
import { PLUGIN } from './constants/plugin';
import { HealthCheck } from './components/health_check';
import { HealthContextProvider } from './context/health_context';

interface MatchParams {
section: Section;
Expand Down Expand Up @@ -139,19 +140,23 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
exact
path={routeToConnectors}
component={() => (
<HealthCheck docLinks={docLinks} http={http}>
<ActionsConnectorsList />
</HealthCheck>
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http}>
<ActionsConnectorsList />
</HealthCheck>
</HealthContextProvider>
)}
/>
)}
<Route
exact
path={routeToAlerts}
component={() => (
<HealthCheck docLinks={docLinks} http={http}>
<AlertsList />
</HealthCheck>
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http} inFlyout={true}>
<AlertsList />
</HealthCheck>
</HealthContextProvider>
)}
/>
</Switch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import {
EuiTitle,
EuiFlyoutHeader,
EuiFlyout,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
EuiFlyoutBody,
EuiPortal,
EuiBetaBadge,
Expand All @@ -29,6 +24,8 @@ import { HealthCheck } from '../../components/health_check';
import { PLUGIN } from '../../constants/plugin';
import { ConfirmAlertSave } from './confirm_alert_save';
import { hasShowActionsCapability } from '../../lib/capabilities';
import AlertAddFooter from './alert_add_footer';
import { HealthContextProvider } from '../../context/health_context';

interface AlertAddProps {
consumer: string;
Expand Down Expand Up @@ -183,54 +180,37 @@ export const AlertAdd = ({
</h3>
</EuiTitle>
</EuiFlyoutHeader>
<HealthCheck docLinks={docLinks} http={http} inFlyout={true}>
<EuiFlyoutBody>
<AlertForm
alert={alert}
dispatch={dispatch}
errors={errors}
canChangeTrigger={canChangeTrigger}
operation={i18n.translate('xpack.triggersActionsUI.sections.alertAdd.operationName', {
defaultMessage: 'create',
})}
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http} inFlyout={true} waitForCheck={false}>
<EuiFlyoutBody>
<AlertForm
alert={alert}
dispatch={dispatch}
errors={errors}
canChangeTrigger={canChangeTrigger}
operation={i18n.translate(
'xpack.triggersActionsUI.sections.alertAdd.operationName',
{
defaultMessage: 'create',
}
)}
/>
</EuiFlyoutBody>
<AlertAddFooter
isSaving={isSaving}
hasErrors={hasErrors || hasActionErrors}
onSave={async () => {
setIsSaving(true);
if (shouldConfirmSave) {
setIsConfirmAlertSaveModalOpen(true);
} else {
await saveAlertAndCloseFlyout();
}
}}
onCancel={closeFlyout}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelSaveAlertButton" onClick={closeFlyout}>
{i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="secondary"
data-test-subj="saveAlertButton"
type="submit"
iconType="check"
isDisabled={hasErrors || hasActionErrors}
isLoading={isSaving}
onClick={async () => {
setIsSaving(true);
if (shouldConfirmSave) {
setIsConfirmAlertSaveModalOpen(true);
} else {
await saveAlertAndCloseFlyout();
}
}}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</HealthCheck>
</HealthCheck>
</HealthContextProvider>
{isConfirmAlertSaveModalOpen && (
<ConfirmAlertSave
onConfirm={async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useHealthContext } from '../../context/health_context';

interface AlertAddFooterProps {
isSaving: boolean;
hasErrors: boolean;
onSave: () => void;
onCancel: () => void;
}

export const AlertAddFooter = ({ isSaving, hasErrors, onSave, onCancel }: AlertAddFooterProps) => {
const { loadingHealthCheck } = useHealthContext();

return (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelSaveAlertButton" onClick={onCancel}>
{i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', {
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="secondary"
data-test-subj="saveAlertButton"
type="submit"
iconType="check"
isDisabled={hasErrors || loadingHealthCheck}
isLoading={isSaving}
onClick={onSave}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel"
ymao1 marked this conversation as resolved.
Show resolved Hide resolved
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);
};

// eslint-disable-next-line import/no-default-export
export { AlertAddFooter as default };
Loading