[Uptime][Monitor Management] Use push flyout to show Test Run Results (#125017) (uptime/issues/445)

* Make run-once test results appear in a push flyout. Fixup tooltip. Fixup action buttons order.

* Wrapping Monitor Fields form rows when Test Run flyout is open.

* Only show step duration trend if it's an already saved monitor. Stop showing "Failed to run steps" until Test Run steps are done loading.

uptime/issues/445

Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co>
This commit is contained in:
Abdul Wahab Zahid 2022-02-15 16:33:43 +01:00 committed by GitHub
parent c2a010367d
commit dde4d6e9da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 361 additions and 146 deletions

View file

@ -13,10 +13,10 @@ import {
EuiFieldText,
EuiCheckbox,
EuiFormRow,
EuiDescribedFormGroup,
EuiSpacer,
} from '@elastic/eui';
import { ComboBox } from '../combo_box';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { useBrowserAdvancedFieldsContext, useBrowserSimpleFieldsContext } from '../contexts';
@ -28,9 +28,10 @@ import { ThrottlingFields } from './throttling_fields';
interface Props {
validate: Validation;
children?: React.ReactNode;
minColumnWidth?: string;
}
export const BrowserAdvancedFields = memo<Props>(({ validate, children }) => {
export const BrowserAdvancedFields = memo<Props>(({ validate, children, minColumnWidth }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const { fields: simpleFields } = useBrowserSimpleFieldsContext();
@ -49,7 +50,8 @@ export const BrowserAdvancedFields = memo<Props>(({ validate, children }) => {
>
<EuiSpacer size="m" />
{simpleFields[ConfigKey.SOURCE_ZIP_URL] && (
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -115,9 +117,10 @@ export const BrowserAdvancedFields = memo<Props>(({ validate, children }) => {
data-test-subj="syntheticsBrowserJourneyFiltersTags"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
)}
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -211,9 +214,9 @@ export const BrowserAdvancedFields = memo<Props>(({ validate, children }) => {
data-test-subj="syntheticsBrowserSyntheticsArgs"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
<ThrottlingFields validate={validate} />
<ThrottlingFields validate={validate} minColumnWidth={minColumnWidth} />
{children}
</EuiAccordion>
);

View file

@ -7,14 +7,8 @@
import React, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiDescribedFormGroup,
EuiSwitch,
EuiSpacer,
EuiFormRow,
EuiFieldNumber,
EuiText,
} from '@elastic/eui';
import { EuiSwitch, EuiSpacer, EuiFormRow, EuiFieldNumber, EuiText } from '@elastic/eui';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { OptionalLabel } from '../optional_label';
import { useBrowserAdvancedFieldsContext } from '../contexts';
@ -22,6 +16,7 @@ import { Validation, ConfigKey } from '../types';
interface Props {
validate: Validation;
minColumnWidth?: string;
}
type ThrottlingConfigs =
@ -30,7 +25,7 @@ type ThrottlingConfigs =
| ConfigKey.UPLOAD_SPEED
| ConfigKey.LATENCY;
export const ThrottlingFields = memo<Props>(({ validate }) => {
export const ThrottlingFields = memo<Props>(({ validate, minColumnWidth }) => {
const { fields, setFields } = useBrowserAdvancedFieldsContext();
const handleInputChange = useCallback(
@ -148,7 +143,8 @@ export const ThrottlingFields = memo<Props>(({ validate }) => {
) : null;
return (
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -183,6 +179,6 @@ export const ThrottlingFields = memo<Props>(({ validate }) => {
}
/>
{throttlingInputs}
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
);
});

View file

@ -9,6 +9,7 @@ import React from 'react';
import styled from 'styled-components';
import { EuiPanel } from '@elastic/eui';
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
import { CodeEditor as MonacoCodeEditor } from '../../../../../../src/plugins/kibana_react/public';
import { MonacoEditorLangId } from './types';
@ -28,7 +29,11 @@ interface Props {
export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value }: Props) => {
return (
<CodeEditorContainer borderRadius="none" hasShadow={false} hasBorder={true}>
<div id={`${id}-editor`} aria-label={ariaLabel} data-test-subj="codeEditorContainer">
<MonacoCodeContainer
id={`${id}-editor`}
aria-label={ariaLabel}
data-test-subj="codeEditorContainer"
>
<MonacoCodeEditor
languageId={languageId}
width="100%"
@ -41,7 +46,13 @@ export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value }: Props
isCopyable={true}
allowFullScreen={true}
/>
</div>
</MonacoCodeContainer>
</CodeEditorContainer>
);
};
const MonacoCodeContainer = euiStyled.div`
& > .kibanaCodeEditor {
z-index: 0;
}
`;

View file

@ -0,0 +1,23 @@
/*
* 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 { EuiDescribedFormGroup } from '@elastic/eui';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
/**
* EuiForm group doesn't expose props to control the flex wrapping on flex groups defining form rows.
* This override allows to define a minimum column width to which the Described Form's flex rows should wrap.
*/
export const DescribedFormGroupWithWrap = euiStyled(EuiDescribedFormGroup)<{
minColumnWidth?: string;
}>`
> .euiFlexGroup {
${({ minColumnWidth }) => (minColumnWidth ? `flex-wrap: wrap;` : '')}
> .euiFlexItem {
${({ minColumnWidth }) => (minColumnWidth ? `min-width: ${minColumnWidth};` : '')}
}
}
`;

View file

@ -14,11 +14,11 @@ import {
EuiFormRow,
EuiSelect,
EuiSpacer,
EuiDescribedFormGroup,
EuiSwitch,
EuiCallOut,
EuiLink,
} from '@elastic/eui';
import { DescribedFormGroupWithWrap } from './common/described_form_group_with_wrap';
import { ConfigKey, DataStream, Validation } from './types';
import { usePolicyConfigContext } from './contexts';
import { TLSFields } from './tls_fields';
@ -36,6 +36,7 @@ interface Props {
dataStreams?: DataStream[];
children?: React.ReactNode;
appendAdvancedFields?: React.ReactNode;
minColumnWidth?: string;
}
const dataStreamToString = [
@ -54,7 +55,7 @@ const dataStreamToString = [
];
export const CustomFields = memo<Props>(
({ validate, dataStreams = [], children, appendAdvancedFields }) => {
({ validate, dataStreams = [], children, appendAdvancedFields, minColumnWidth }) => {
const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } =
usePolicyConfigContext();
@ -86,7 +87,8 @@ export const CustomFields = memo<Props>(
return (
<EuiForm component="form">
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -165,9 +167,10 @@ export const CustomFields = memo<Props>(
{renderSimpleFields(monitorType)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
{(isHTTP || isTCP) && (
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -197,15 +200,23 @@ export const CustomFields = memo<Props>(
onChange={(event) => setIsTLSEnabled(event.target.checked)}
/>
<TLSFields />
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
)}
<EuiSpacer size="m" />
{isHTTP && (
<HTTPAdvancedFields validate={validate}>{appendAdvancedFields}</HTTPAdvancedFields>
<HTTPAdvancedFields validate={validate} minColumnWidth={minColumnWidth}>
{appendAdvancedFields}
</HTTPAdvancedFields>
)}
{isTCP && (
<TCPAdvancedFields minColumnWidth={minColumnWidth}>
{appendAdvancedFields}
</TCPAdvancedFields>
)}
{isTCP && <TCPAdvancedFields>{appendAdvancedFields}</TCPAdvancedFields>}
{isBrowser && (
<BrowserAdvancedFields validate={validate}>{appendAdvancedFields}</BrowserAdvancedFields>
<BrowserAdvancedFields validate={validate} minColumnWidth={minColumnWidth}>
{appendAdvancedFields}
</BrowserAdvancedFields>
)}
{isICMP && <ICMPAdvancedFields>{appendAdvancedFields}</ICMPAdvancedFields>}
</EuiForm>

View file

@ -14,11 +14,11 @@ import {
EuiFieldText,
EuiFormRow,
EuiSelect,
EuiDescribedFormGroup,
EuiCheckbox,
EuiSpacer,
EuiFieldPassword,
} from '@elastic/eui';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { useHTTPAdvancedFieldsContext } from '../contexts';
@ -33,9 +33,10 @@ import { ComboBox } from '../combo_box';
interface Props {
validate: Validation;
children?: React.ReactNode;
minColumnWidth?: string;
}
export const HTTPAdvancedFields = memo<Props>(({ validate, children }) => {
export const HTTPAdvancedFields = memo<Props>(({ validate, children, minColumnWidth }) => {
const { fields, setFields } = useHTTPAdvancedFieldsContext();
const handleInputChange = useCallback(
({ value, configKey }: { value: unknown; configKey: ConfigKey }) => {
@ -56,7 +57,8 @@ export const HTTPAdvancedFields = memo<Props>(({ validate, children }) => {
data-test-subj="syntheticsHTTPAdvancedFieldsAccordion"
>
<EuiSpacer size="xl" />
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -248,9 +250,10 @@ export const HTTPAdvancedFields = memo<Props>(({ validate, children }) => {
)}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
<EuiSpacer size="xl" />
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -316,8 +319,9 @@ export const HTTPAdvancedFields = memo<Props>(({ validate, children }) => {
)}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
</DescribedFormGroupWithWrap>
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -461,7 +465,7 @@ export const HTTPAdvancedFields = memo<Props>(({ validate, children }) => {
data-test-subj="syntheticsResponseBodyCheckNegative"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
{children}
</EuiAccordion>
);

View file

@ -7,14 +7,8 @@
import React, { memo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiAccordion,
EuiCheckbox,
EuiFormRow,
EuiDescribedFormGroup,
EuiFieldText,
EuiSpacer,
} from '@elastic/eui';
import { EuiAccordion, EuiCheckbox, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui';
import { DescribedFormGroupWithWrap } from '../common/described_form_group_with_wrap';
import { useTCPAdvancedFieldsContext } from '../contexts';
@ -24,9 +18,10 @@ import { OptionalLabel } from '../optional_label';
interface Props {
children?: React.ReactNode;
minColumnWidth?: string;
}
export const TCPAdvancedFields = memo<Props>(({ children }) => {
export const TCPAdvancedFields = memo<Props>(({ children, minColumnWidth }) => {
const { fields, setFields } = useTCPAdvancedFieldsContext();
const handleInputChange = useCallback(
@ -43,7 +38,8 @@ export const TCPAdvancedFields = memo<Props>(({ children }) => {
data-test-subj="syntheticsTCPAdvancedFieldsAccordion"
>
<EuiSpacer size="m" />
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -134,8 +130,9 @@ export const TCPAdvancedFields = memo<Props>(({ children }) => {
data-test-subj="syntheticsTCPRequestSendCheck"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
</DescribedFormGroupWithWrap>
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -179,7 +176,7 @@ export const TCPAdvancedFields = memo<Props>(({ children }) => {
data-test-subj="syntheticsTCPResponseReceiveCheck"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
{children}
</EuiAccordion>
);

View file

@ -35,7 +35,7 @@ describe('<ActionBar />', () => {
});
it('only calls setMonitor when valid and after submission', () => {
render(<ActionBar monitor={monitor} isValid={true} />);
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={true} />);
act(() => {
userEvent.click(screen.getByText('Save monitor'));
@ -45,7 +45,7 @@ describe('<ActionBar />', () => {
});
it('does not call setMonitor until submission', () => {
render(<ActionBar monitor={monitor} isValid={true} />);
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={true} />);
expect(setMonitor).not.toBeCalled();
@ -57,7 +57,7 @@ describe('<ActionBar />', () => {
});
it('does not call setMonitor if invalid', () => {
render(<ActionBar monitor={monitor} isValid={false} />);
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={false} />);
expect(setMonitor).not.toBeCalled();
@ -69,7 +69,7 @@ describe('<ActionBar />', () => {
});
it('disables button and displays help text when form is invalid after first submission', async () => {
render(<ActionBar monitor={monitor} isValid={false} />);
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={false} />);
expect(
screen.queryByText('Your monitor has errors. Please fix them before saving.')
@ -90,7 +90,9 @@ describe('<ActionBar />', () => {
it('calls option onSave when saving monitor', () => {
const onSave = jest.fn();
render(<ActionBar monitor={monitor} isValid={false} onSave={onSave} />);
render(
<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={false} onSave={onSave} />
);
act(() => {
userEvent.click(screen.getByText('Save monitor'));

View file

@ -13,7 +13,7 @@ import {
EuiButton,
EuiButtonEmpty,
EuiText,
EuiToolTip,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -37,11 +37,19 @@ export interface ActionBarProps {
monitor: SyntheticsMonitor;
isValid: boolean;
testRun?: TestRun;
isTestRunInProgress: boolean;
onSave?: () => void;
onTestNow?: () => void;
}
export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: ActionBarProps) => {
export const ActionBar = ({
monitor,
isValid,
onSave,
onTestNow,
testRun,
isTestRunInProgress,
}: ActionBarProps) => {
const { monitorId } = useParams<{ monitorId: string }>();
const { basePath } = useContext(UptimeSettingsContext);
const { locations } = useSelector(monitorManagementListSelector);
@ -49,6 +57,7 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isSuccessful, setIsSuccessful] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean | undefined>(undefined);
const { data, status } = useFetcher(() => {
if (!isSaving || !isValid) {
@ -94,7 +103,7 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti
});
setIsSuccessful(true);
} else if (hasErrors && !loading) {
Object.values(data).forEach((location) => {
Object.values(data!).forEach((location) => {
const { status: responseStatus, reason } = location.error || {};
kibanaService.toasts.addWarning({
title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', {
@ -144,35 +153,51 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{onTestNow && (
<EuiFlexItem grow={false} style={{ marginRight: 20 }}>
<EuiToolTip content={TEST_NOW_DESCRIPTION}>
<EuiButton
fill
size="s"
color="success"
iconType="play"
onClick={() => onTestNow()}
disabled={!isValid}
data-test-subj={'monitorTestNowRunBtn'}
>
{testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL}
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="ghost"
size="s"
iconType="cross"
href={`${basePath}/app/uptime/${MONITOR_MANAGEMENT_ROUTE}`}
>
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
{onTestNow && (
<EuiFlexItem grow={false}>
{/* Popover is used instead of EuiTooltip until the resolution of https://github.com/elastic/eui/issues/5604 */}
<EuiPopover
repositionOnScroll={true}
initialFocus={false}
button={
<EuiButton
css={{ width: '100%' }}
fill
size="s"
color="success"
iconType="play"
disabled={!isValid || isTestRunInProgress}
data-test-subj={'monitorTestNowRunBtn'}
onClick={() => onTestNow()}
onMouseEnter={() => {
setIsPopoverOpen(true);
}}
onMouseLeave={() => {
setIsPopoverOpen(false);
}}
>
{testRun ? RE_RUN_TEST_LABEL : RUN_TEST_LABEL}
</EuiButton>
}
isOpen={isPopoverOpen}
>
<EuiText style={{ width: 260, outline: 'none' }}>
<p>{TEST_NOW_DESCRIPTION}</p>
</EuiText>
</EuiPopover>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
color="primary"

View file

@ -59,7 +59,9 @@ describe('<ActionBar /> Service Errors', () => {
status: FETCH_STATUS.SUCCESS,
refetch: () => {},
});
render(<ActionBar monitor={monitor} isValid={true} />, { state: mockLocationsState });
render(<ActionBar monitor={monitor} isTestRunInProgress={false} isValid={true} />, {
state: mockLocationsState,
});
userEvent.click(screen.getByText('Save monitor'));
await waitFor(() => {

View file

@ -88,7 +88,7 @@ export const EditMonitorConfig = ({ monitor }: Props) => {
browserDefaultValues={fullDefaultConfig[DataStream.BROWSER]}
tlsDefaultValues={defaultTLSConfig}
>
<MonitorConfig />
<MonitorConfig isEdit={true} />
</SyntheticsProviders>
);
};

View file

@ -6,17 +6,19 @@
*/
import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiSpacer, EuiDescribedFormGroup, EuiLink, EuiFieldText } from '@elastic/eui';
import type { Validation } from '../../../../common/types/index';
import { ConfigKey } from '../../../../common/runtime_types/monitor_management';
import { EuiFormRow, EuiSpacer, EuiLink, EuiFieldText } from '@elastic/eui';
import type { Validation } from '../../../../common/types';
import { ConfigKey } from '../../../../common/runtime_types';
import { DescribedFormGroupWithWrap } from '../../fleet_package/common/described_form_group_with_wrap';
import { usePolicyConfigContext } from '../../fleet_package/contexts';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
interface Props {
validate: Validation;
minColumnWidth?: string;
}
export const MonitorManagementAdvancedFields = memo<Props>(({ validate }) => {
export const MonitorManagementAdvancedFields = memo<Props>(({ validate, minColumnWidth }) => {
const { namespace, setNamespace } = usePolicyConfigContext();
const namespaceErrorMsg = validate[ConfigKey.NAMESPACE]?.({
@ -26,7 +28,8 @@ export const MonitorManagementAdvancedFields = memo<Props>(({ validate }) => {
const { services } = useKibana();
return (
<EuiDescribedFormGroup
<DescribedFormGroupWithWrap
minColumnWidth={minColumnWidth}
title={
<h4>
<FormattedMessage
@ -83,6 +86,6 @@ export const MonitorManagementAdvancedFields = memo<Props>(({ validate }) => {
name="namespace"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</DescribedFormGroupWithWrap>
);
});

View file

@ -5,9 +5,17 @@
* 2.0.
*/
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { EuiResizableContainer } from '@elastic/eui';
import {
EuiFlyoutBody,
EuiFlyoutHeader,
EuiFlyout,
EuiSpacer,
EuiFlyoutFooter,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { v4 as uuidv4 } from 'uuid';
import { defaultConfig, usePolicyConfigContext } from '../../fleet_package/contexts';
@ -19,7 +27,7 @@ import { MonitorFields } from './monitor_fields';
import { TestNowMode, TestRun } from '../test_now_mode/test_now_mode';
import { MonitorFields as MonitorFieldsType } from '../../../../common/runtime_types';
export const MonitorConfig = () => {
export const MonitorConfig = ({ isEdit = false }: { isEdit: boolean }) => {
const { monitorType } = usePolicyConfigContext();
/* raw policy config compatible with the UI. Save this to saved objects */
@ -37,46 +45,70 @@ export const MonitorConfig = () => {
});
const [testRun, setTestRun] = useState<TestRun>();
const [isTestRunInProgress, setIsTestRunInProgress] = useState<boolean>(false);
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
const onTestNow = () => {
const handleTestNow = () => {
if (config) {
setTestRun({ id: uuidv4(), monitor: config as MonitorFieldsType });
setIsTestRunInProgress(true);
setIsFlyoutOpen(true);
}
};
const handleTestDone = useCallback(() => {
setIsTestRunInProgress(false);
}, [setIsTestRunInProgress]);
const handleFlyoutClose = useCallback(() => {
handleTestDone();
setIsFlyoutOpen(false);
}, [handleTestDone, setIsFlyoutOpen]);
const flyout = isFlyoutOpen && config && (
<EuiFlyout
type="push"
size="m"
paddingSize="m"
maxWidth="44%"
aria-labelledby={TEST_RESULT}
onClose={handleFlyoutClose}
>
<EuiFlyoutHeader>
<EuiSpacer size="xl" />
</EuiFlyoutHeader>
<EuiFlyoutBody>
<TestNowMode testRun={testRun} isMonitorSaved={isEdit} onDone={handleTestDone} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty iconType="cross" onClick={handleFlyoutClose} flush="left">
{CLOSE_LABEL}
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyout>
);
return (
<>
<EuiResizableContainer>
{(EuiResizablePanel, EuiResizableButton) => (
<>
<EuiResizablePanel
initialSize={55}
minSize="30%"
mode={[
'collapsible',
{
position: 'top',
},
]}
>
<MonitorFields />
</EuiResizablePanel>
<MonitorFields />
<EuiResizableButton />
<EuiResizablePanel initialSize={45} minSize="200px" mode="main">
{config && <TestNowMode testRun={testRun} />}
</EuiResizablePanel>
</>
)}
</EuiResizableContainer>
{flyout}
<ActionBarPortal
monitor={policyConfig[monitorType]}
isValid={isValid}
onTestNow={onTestNow}
onTestNow={handleTestNow}
testRun={testRun}
isTestRunInProgress={isTestRunInProgress}
/>
</>
);
};
const TEST_RESULT = i18n.translate('xpack.uptime.monitorManagement.testResult', {
defaultMessage: 'Test result',
});
const CLOSE_LABEL = i18n.translate('xpack.uptime.monitorManagement.closeButtonLabel', {
defaultMessage: 'Close',
});

View file

@ -15,14 +15,22 @@ import { validate } from '../validation';
import { MonitorNameAndLocation } from './monitor_name_location';
import { MonitorManagementAdvancedFields } from './monitor_advanced_fields';
const MIN_COLUMN_WRAP_WIDTH = '360px';
export const MonitorFields = () => {
const { monitorType } = usePolicyConfigContext();
return (
<EuiForm id="syntheticsServiceCreateMonitorForm" component="form">
<CustomFields
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
validate={validate[monitorType]}
dataStreams={[DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER]}
appendAdvancedFields={<MonitorManagementAdvancedFields validate={validate[monitorType]} />}
appendAdvancedFields={
<MonitorManagementAdvancedFields
validate={validate[monitorType]}
minColumnWidth={MIN_COLUMN_WRAP_WIDTH}
/>
}
>
<MonitorNameAndLocation validate={validate[monitorType]} />
</CustomFields>

View file

@ -43,7 +43,7 @@ export const MonitorNameAndLocation = ({ validate }: Props) => {
defaultMessage="Monitor name"
/>
}
fullWidth={true}
fullWidth={false}
isInvalid={isNameInvalid || nameAlreadyExists}
error={
nameAlreadyExists ? (

View file

@ -14,8 +14,16 @@ import { BrowserTestRunResult } from './browser_test_results';
import { fireEvent } from '@testing-library/dom';
describe('BrowserTestRunResult', function () {
const onDone = jest.fn();
let testId: string;
beforeEach(() => {
testId = 'test-id';
jest.resetAllMocks();
});
it('should render properly', async function () {
render(<BrowserTestRunResult monitorId={'test-id'} />);
render(<BrowserTestRunResult monitorId={testId} isMonitorSaved={true} onDone={onDone} />);
expect(await screen.findByText('Test result')).toBeInTheDocument();
expect(await screen.findByText('0 steps completed')).toBeInTheDocument();
const dataApi = (kibanaService.core as any).data.search;
@ -28,7 +36,7 @@ describe('BrowserTestRunResult', function () {
query: {
bool: {
filter: [
{ term: { config_id: 'test-id' } },
{ term: { config_id: testId } },
{
terms: {
'synthetics.type': ['heartbeat/summary', 'journey/start'],
@ -52,12 +60,13 @@ describe('BrowserTestRunResult', function () {
data,
stepListData: { steps: [stepEndDoc._source] } as any,
loading: false,
stepsLoading: false,
journeyStarted: true,
summaryDoc: summaryDoc._source,
stepEnds: [stepEndDoc._source],
});
render(<BrowserTestRunResult monitorId={'test-id'} />);
render(<BrowserTestRunResult monitorId={testId} isMonitorSaved={true} onDone={onDone} />);
expect(await screen.findByText('Test result')).toBeInTheDocument();
@ -69,6 +78,9 @@ describe('BrowserTestRunResult', function () {
expect(await screen.findByText('Go to https://www.elastic.co/')).toBeInTheDocument();
expect(await screen.findByText('21.8 seconds')).toBeInTheDocument();
// Calls onDone on completion
expect(onDone).toHaveBeenCalled();
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { useEffect } from 'react';
import * as React from 'react';
import { EuiAccordion, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -16,13 +17,21 @@ import { TestResultHeader } from '../test_result_header';
interface Props {
monitorId: string;
isMonitorSaved: boolean;
onDone: () => void;
}
export const BrowserTestRunResult = ({ monitorId }: Props) => {
const { data, loading, stepEnds, journeyStarted, summaryDoc, stepListData } =
export const BrowserTestRunResult = ({ monitorId, isMonitorSaved, onDone }: Props) => {
const { data, loading, stepsLoading, stepEnds, journeyStarted, summaryDoc, stepListData } =
useBrowserRunOnceMonitors({
configId: monitorId,
});
useEffect(() => {
if (Boolean(summaryDoc)) {
onDone();
}
}, [summaryDoc, onDone]);
const hits = data?.hits.hits;
const doc = hits?.[0]?._source as JourneyStep;
@ -50,6 +59,10 @@ export const BrowserTestRunResult = ({ monitorId }: Props) => {
</div>
);
const isStepsLoading =
journeyStarted && stepEnds.length === 0 && (!summaryDoc || (summaryDoc && stepsLoading));
const isStepsLoadingFailed = summaryDoc && stepEnds.length === 0 && !isStepsLoading;
return (
<AccordionWrapper
id={monitorId}
@ -59,13 +72,16 @@ export const BrowserTestRunResult = ({ monitorId }: Props) => {
buttonContent={buttonContent}
paddingSize="s"
data-test-subj="expandResults"
initialIsOpen={true}
>
{summaryDoc && stepEnds.length === 0 && <EuiText color="danger">{FAILED_TO_RUN}</EuiText>}
{!summaryDoc && journeyStarted && stepEnds.length === 0 && <EuiText>{LOADING_STEPS}</EuiText>}
{isStepsLoading && <EuiText>{LOADING_STEPS}</EuiText>}
{isStepsLoadingFailed && <EuiText color="danger">{FAILED_TO_RUN}</EuiText>}
{stepEnds.length > 0 && stepListData?.steps && (
<StepsList
data={stepListData.steps}
compactView={true}
showStepDurationTrend={isMonitorSaved}
loading={Boolean(loading)}
error={undefined}
/>

View file

@ -34,6 +34,7 @@ describe('useBrowserRunOnceMonitors', function () {
data: undefined,
journeyStarted: false,
loading: true,
stepsLoading: true,
stepEnds: [],
stepListData: undefined,
summaryDoc: undefined,

View file

@ -86,7 +86,7 @@ export const useBrowserRunOnceMonitors = ({
const { data, loading } = useBrowserEsResults({ configId, testRunId, lastRefresh });
const { data: stepListData } = useFetcher(() => {
const { data: stepListData, loading: stepsLoading } = useFetcher(() => {
if (checkGroupId && !skipDetails) {
return fetchJourneySteps({
checkGroup: checkGroupId,
@ -122,6 +122,7 @@ export const useBrowserRunOnceMonitors = ({
data,
stepEnds,
loading,
stepsLoading,
stepListData,
summaryDoc: summary,
journeyStarted: Boolean(checkGroupId),

View file

@ -14,8 +14,16 @@ import * as runOnceHooks from './use_simple_run_once_monitors';
import { Ping } from '../../../../../common/runtime_types';
describe('SimpleTestResults', function () {
const onDone = jest.fn();
let testId: string;
beforeEach(() => {
testId = 'test-id';
jest.resetAllMocks();
});
it('should render properly', async function () {
render(<SimpleTestResults monitorId={'test-id'} />);
render(<SimpleTestResults monitorId={testId} onDone={onDone} />);
expect(await screen.findByText('Test result')).toBeInTheDocument();
const dataApi = (kibanaService.core as any).data.search;
@ -26,7 +34,7 @@ describe('SimpleTestResults', function () {
body: {
query: {
bool: {
filter: [{ term: { config_id: 'test-id' } }, { exists: { field: 'summary' } }],
filter: [{ term: { config_id: testId } }, { exists: { field: 'summary' } }],
},
},
sort: [{ '@timestamp': 'desc' }],
@ -51,7 +59,7 @@ describe('SimpleTestResults', function () {
loading: false,
});
render(<SimpleTestResults monitorId={'test-id'} />);
render(<SimpleTestResults monitorId={'test-id'} onDone={onDone} />);
expect(await screen.findByText('Test result')).toBeInTheDocument();
@ -61,6 +69,9 @@ describe('SimpleTestResults', function () {
expect(await screen.findByText('Checked Jan 12, 2022 11:54:27 AM')).toBeInTheDocument();
expect(await screen.findByText('Took 191 ms')).toBeInTheDocument();
// Calls onDone on completion
expect(onDone).toHaveBeenCalled();
screen.debug();
});
});

View file

@ -12,16 +12,18 @@ import { TestResultHeader } from '../test_result_header';
interface Props {
monitorId: string;
onDone: () => void;
}
export function SimpleTestResults({ monitorId }: Props) {
export function SimpleTestResults({ monitorId, onDone }: Props) {
const [summaryDocs, setSummaryDocs] = useState<Ping[]>([]);
const { summaryDoc, loading } = useSimpleRunOnceMonitors({ configId: monitorId });
useEffect(() => {
if (summaryDoc) {
setSummaryDocs((prevState) => [summaryDoc, ...prevState]);
onDone();
}
}, [summaryDoc]);
}, [summaryDoc, onDone]);
return (
<>

View file

@ -13,9 +13,19 @@ import { kibanaService } from '../../../state/kibana_service';
import { MonitorFields } from '../../../../common/runtime_types';
describe('TestNowMode', function () {
const onDone = jest.fn();
afterEach(() => {
jest.resetAllMocks();
});
it('should render properly', async function () {
render(
<TestNowMode testRun={{ id: 'test-run', monitor: { type: 'browser' } as MonitorFields }} />
<TestNowMode
testRun={{ id: 'test-run', monitor: { type: 'browser' } as MonitorFields }}
isMonitorSaved={false}
onDone={onDone}
/>
);
expect(await screen.findByText('Test result')).toBeInTheDocument();
expect(await screen.findByText('PENDING')).toBeInTheDocument();

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
@ -26,13 +26,25 @@ export interface TestRun {
monitor: MonitorFields;
}
export function TestNowMode({ testRun }: { testRun?: TestRun }) {
export function TestNowMode({
testRun,
isMonitorSaved,
onDone,
}: {
testRun?: TestRun;
isMonitorSaved: boolean;
onDone: () => void;
}) {
const [serviceError, setServiceError] = useState<null | Error>(null);
const { data, loading: isPushing } = useFetcher(() => {
if (testRun) {
return runOnceMonitor({
monitor: testRun.monitor,
id: testRun.id,
});
})
.then(() => setServiceError(null))
.catch((error) => setServiceError(error));
}
return new Promise((resolve) => resolve(null));
}, [testRun]);
@ -49,7 +61,13 @@ export function TestNowMode({ testRun }: { testRun?: TestRun }) {
const errors = (data as { errors?: Array<{ error: Error }> })?.errors;
const hasErrors = errors && errors?.length > 0;
const hasErrors = serviceError || (errors && errors?.length > 0);
useEffect(() => {
if (!isPushing && (!testRun || hasErrors)) {
onDone();
}
}, [testRun, hasErrors, isPushing, onDone]);
if (!testRun) {
return null;
@ -68,7 +86,12 @@ export function TestNowMode({ testRun }: { testRun?: TestRun }) {
{testRun && !hasErrors && !isPushing && (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem key={testRun.id}>
<TestRunResult monitorId={testRun.id} monitor={testRun.monitor} />
<TestRunResult
monitorId={testRun.id}
monitor={testRun.monitor}
isMonitorSaved={isMonitorSaved}
onDone={onDone}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}

View file

@ -13,11 +13,13 @@ import { SimpleTestResults } from './simple/simple_test_results';
interface Props {
monitorId: string;
monitor: SyntheticsMonitor;
isMonitorSaved: boolean;
onDone: () => void;
}
export const TestRunResult = ({ monitorId, monitor }: Props) => {
export const TestRunResult = ({ monitorId, monitor, isMonitorSaved, onDone }: Props) => {
return monitor.type === 'browser' ? (
<BrowserTestRunResult monitorId={monitorId} />
<BrowserTestRunResult monitorId={monitorId} isMonitorSaved={isMonitorSaved} onDone={onDone} />
) : (
<SimpleTestResults monitorId={monitorId} />
<SimpleTestResults monitorId={monitorId} onDone={onDone} />
);
};

View file

@ -8,7 +8,7 @@
import type { MouseEvent } from 'react';
import * as React from 'react';
import { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
import { EuiButtonEmpty, EuiPopover, EuiText } from '@elastic/eui';
import { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { JourneyStep } from '../../../../common/runtime_types';
@ -16,6 +16,7 @@ import { StepFieldTrend } from './step_field_trend';
import { microToSec } from '../../../lib/formatting';
interface Props {
showStepDurationTrend?: boolean;
compactView?: boolean;
step: JourneyStep;
durationPopoverOpenIndex: number | null;
@ -26,8 +27,20 @@ export const StepDuration = ({
step,
durationPopoverOpenIndex,
setDurationPopoverOpenIndex,
showStepDurationTrend = true,
compactView = false,
}: Props) => {
const stepDurationText = useMemo(
() =>
i18n.translate('xpack.uptime.synthetics.step.duration', {
defaultMessage: '{value} seconds',
values: {
value: microToSec(step.synthetics.step?.duration.us!, 1),
},
}),
[step.synthetics.step?.duration.us]
);
const component = useMemo(
() => (
<StepFieldTrend
@ -43,17 +56,16 @@ export const StepDuration = ({
return <span>--</span>;
}
if (!showStepDurationTrend) {
return <EuiText>{stepDurationText}</EuiText>;
}
const button = (
<EuiButtonEmpty
onMouseEnter={() => setDurationPopoverOpenIndex(step.synthetics.step?.index ?? null)}
iconType={compactView ? undefined : 'visArea'}
>
{i18n.translate('xpack.uptime.synthetics.step.duration', {
defaultMessage: '{value} seconds',
values: {
value: microToSec(step.synthetics.step?.duration.us!, 1),
},
})}
{stepDurationText}
</EuiButtonEmpty>
);

View file

@ -37,6 +37,7 @@ interface Props {
error?: Error;
loading: boolean;
compactView?: boolean;
showStepDurationTrend?: boolean;
}
interface StepStatusCount {
@ -85,7 +86,13 @@ function reduceStepStatus(prev: StepStatusCount, cur: JourneyStep): StepStatusCo
return prev;
}
export const StepsList = ({ data, error, loading, compactView = false }: Props) => {
export const StepsList = ({
data,
error,
loading,
showStepDurationTrend = true,
compactView = false,
}: Props) => {
const steps: JourneyStep[] = data.filter(isStepEnd);
const { expandedRows, toggleExpand } = useExpandedRow({ steps, allSteps: data, loading });
@ -140,6 +147,7 @@ export const StepsList = ({ data, error, loading, compactView = false }: Props)
step={item}
durationPopoverOpenIndex={durationPopoverOpenIndex}
setDurationPopoverOpenIndex={setDurationPopoverOpenIndex}
showStepDurationTrend={showStepDurationTrend}
compactView={compactView}
/>
);

View file

@ -37,7 +37,7 @@ export const AddMonitorPage: React.FC = () => {
allowedScheduleUnits: [ScheduleUnit.MINUTES],
}}
>
<MonitorConfig />
<MonitorConfig isEdit={false} />
</SyntheticsProviders>
</Loader>
);