[Uptime] Move uptime to new solution nav (#100905)

* Expose options to customize the route matching

* Add more comments

* move uptime to new solution nav

* push

* update test

* add an extra breadcrumb

Co-authored-by: Felix Stürmer <stuermer@weltenwort.de>
This commit is contained in:
Shahzad 2021-06-03 15:19:35 +02:00 committed by GitHub
parent c30dc1e08f
commit 2ea4d5713c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 470 additions and 555 deletions

View file

@ -8,7 +8,6 @@
"optionalPlugins": [
"data",
"home",
"observability",
"ml",
"fleet"
],
@ -18,7 +17,8 @@
"features",
"licensing",
"triggersActionsUi",
"usageCollection"
"usageCollection",
"observability"
],
"server": true,
"ui": true,
@ -31,4 +31,4 @@
"data",
"ml"
]
}
}

View file

@ -12,6 +12,8 @@ import {
PluginInitializerContext,
AppMountParameters,
} from 'kibana/public';
import { of } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public';
import {
FeatureCatalogueCategory,
@ -28,7 +30,11 @@ import {
} from '../../../../../src/plugins/data/public';
import { alertTypeInitializers } from '../lib/alert_types';
import { FleetStart } from '../../../fleet/public';
import { FetchDataParams, ObservabilityPublicSetup } from '../../../observability/public';
import {
FetchDataParams,
ObservabilityPublicSetup,
ObservabilityPublicStart,
} from '../../../observability/public';
import { PLUGIN } from '../../common/constants/plugin';
import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public';
import {
@ -48,6 +54,7 @@ export interface ClientPluginsStart {
data: DataPublicPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
fleet?: FleetStart;
observability: ObservabilityPublicStart;
}
export interface UptimePluginServices extends Partial<CoreStart> {
@ -83,21 +90,46 @@ export class UptimePlugin
return UptimeDataHelper(coreStart);
};
if (plugins.observability) {
plugins.observability.dashboard.register({
appName: 'synthetics',
hasData: async () => {
const dataHelper = await getUptimeDataHelper();
const status = await dataHelper.indexStatus();
return { hasData: status.docCount > 0, indices: status.indices };
},
fetchData: async (params: FetchDataParams) => {
const dataHelper = await getUptimeDataHelper();
return await dataHelper.overviewData(params);
},
});
}
plugins.observability.dashboard.register({
appName: 'synthetics',
hasData: async () => {
const dataHelper = await getUptimeDataHelper();
const status = await dataHelper.indexStatus();
return { hasData: status.docCount > 0, indices: status.indices };
},
fetchData: async (params: FetchDataParams) => {
const dataHelper = await getUptimeDataHelper();
return await dataHelper.overviewData(params);
},
});
plugins.observability.navigation.registerSections(
of([
{
label: 'Uptime',
sortKey: 200,
entries: [
{
label: i18n.translate('xpack.uptime.overview.heading', {
defaultMessage: 'Monitoring overview',
}),
app: 'uptime',
path: '/',
matchFullPath: true,
ignoreTrailingSlash: true,
},
{
label: i18n.translate('xpack.uptime.certificatesPage.heading', {
defaultMessage: 'TLS Certificates',
}),
app: 'uptime',
path: '/certificates',
matchFullPath: true,
},
],
},
])
);
core.application.register({
id: PLUGIN.ID,
euiIconType: 'logoObservability',

View file

@ -122,6 +122,7 @@ const Application = (props: UptimeAppProps) => {
storage,
data: startPlugins.data,
triggersActionsUi: startPlugins.triggersActionsUi,
observability: startPlugins.observability,
}}
>
<Router history={appMountParameters.history}>

View file

@ -0,0 +1,25 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n/react';
import { useSelector } from 'react-redux';
import { certificatesSelector } from '../../state/certificates/certificates';
export const CertificateTitle = () => {
const { data: certificates } = useSelector(certificatesSelector);
return (
<FormattedMessage
id="xpack.uptime.certificates.heading"
defaultMessage="TLS Certificates ({total})"
values={{
total: <span data-test-subj="uptimeCertTotal">{certificates?.total ?? 0}</span>,
}}
/>
);
};

View file

@ -49,7 +49,7 @@ describe('ActionMenuContent', () => {
// this href value is mocked, so it doesn't correspond to the real link
// that Kibana core services will provide
expect(addDataAnchor.getAttribute('href')).toBe('/app/uptime');
expect(addDataAnchor.getAttribute('href')).toBe('/home#/tutorial/uptimeMonitors');
expect(getByText('Add data'));
});
});

View file

@ -1,69 +0,0 @@
/*
* 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 moment from 'moment';
import { PageHeader } from './page_header';
import { Ping } from '../../../../common/runtime_types';
import { renderWithRouter } from '../../../lib';
import { mockReduxHooks } from '../../../lib/helper/test_helpers';
describe('PageHeader', () => {
const monitorName = 'sample monitor';
const defaultMonitorId = 'always-down';
const defaultMonitorStatus: Ping = {
docId: 'few213kl',
timestamp: moment(new Date()).subtract(15, 'm').toString(),
monitor: {
duration: {
us: 1234567,
},
id: defaultMonitorId,
status: 'up',
type: 'http',
name: monitorName,
},
url: {
full: 'https://www.elastic.co/',
},
};
beforeEach(() => {
mockReduxHooks(defaultMonitorStatus);
});
it('does not render dynamic elements by default', () => {
const component = renderWithRouter(<PageHeader />);
expect(component.find('[data-test-subj="superDatePickerShowDatesButton"]').length).toBe(0);
expect(component.find('[data-test-subj="certificatesRefreshButton"]').length).toBe(0);
expect(component.find('[data-test-subj="monitorTitle"]').length).toBe(0);
expect(component.find('[data-test-subj="uptimeTabs"]').length).toBe(0);
});
it('shallow renders with the date picker', () => {
const component = renderWithRouter(<PageHeader showDatePicker />);
expect(component.find('[data-test-subj="superDatePickerShowDatesButton"]').length).toBe(1);
});
it('shallow renders with certificate refresh button', () => {
const component = renderWithRouter(<PageHeader showCertificateRefreshBtn />);
expect(component.find('[data-test-subj="certificatesRefreshButton"]').length).toBe(1);
});
it('renders monitor title when showMonitorTitle', () => {
const component = renderWithRouter(<PageHeader showMonitorTitle />);
expect(component.find('[data-test-subj="monitorTitle"]').length).toBe(1);
expect(component.find('h1').text()).toBe(monitorName);
});
it('renders tabs when showTabs is true', () => {
const component = renderWithRouter(<PageHeader showTabs />);
expect(component.find('[data-test-subj="uptimeTabs"]').length).toBe(1);
});
});

View file

@ -1,64 +0,0 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import { UptimeDatePicker } from '../uptime_date_picker';
import { SyntheticsCallout } from '../../overview/synthetics_callout';
import { PageTabs } from './page_tabs';
import { CertRefreshBtn } from '../../certificates/cert_refresh_btn';
import { MonitorPageTitle } from '../../monitor/monitor_title';
export interface Props {
showCertificateRefreshBtn?: boolean;
showDatePicker?: boolean;
showMonitorTitle?: boolean;
showTabs?: boolean;
}
const StyledPicker = styled(EuiFlexItem)`
&&& {
@media only screen and (max-width: 1024px) and (min-width: 868px) {
.euiSuperDatePicker__flexWrapper {
width: 500px;
}
}
@media only screen and (max-width: 880px) {
flex-grow: 1;
.euiSuperDatePicker__flexWrapper {
width: calc(100% + 8px);
}
}
}
`;
export const PageHeader = ({
showCertificateRefreshBtn = false,
showDatePicker = false,
showMonitorTitle = false,
showTabs = false,
}: Props) => {
return (
<>
<SyntheticsCallout />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" wrap responsive={false}>
<EuiFlexItem>
{showMonitorTitle && <MonitorPageTitle />}
{showTabs && <PageTabs />}
</EuiFlexItem>
{showCertificateRefreshBtn && <CertRefreshBtn />}
{showDatePicker && (
<StyledPicker grow={false} style={{ flexBasis: 485 }}>
<UptimeDatePicker />
</StyledPicker>
)}
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
);
};

View file

@ -48,38 +48,6 @@ describe('MonitorTitle component', () => {
},
};
const defaultTCPMonitorStatus: Ping = {
docId: 'few213kl',
timestamp: moment(new Date()).subtract(15, 'm').toString(),
monitor: {
duration: {
us: 1234567,
},
id: 'tcp',
status: 'up',
type: 'tcp',
},
url: {
full: 'https://www.elastic.co/',
},
};
const defaultICMPMonitorStatus: Ping = {
docId: 'few213kl',
timestamp: moment(new Date()).subtract(15, 'm').toString(),
monitor: {
duration: {
us: 1234567,
},
id: 'icmp',
status: 'up',
type: 'icmp',
},
url: {
full: 'https://www.elastic.co/',
},
};
const defaultBrowserMonitorStatus: Ping = {
docId: 'few213kl',
timestamp: moment(new Date()).subtract(15, 'm').toString(),
@ -145,37 +113,4 @@ describe('MonitorTitle component', () => {
expect(betaLink.href).toBe('https://www.elastic.co/what-is/synthetic-monitoring');
expect(screen.getByText('Browser (BETA)')).toBeInTheDocument();
});
it('does not render beta disclaimer for http', () => {
render(<MonitorPageTitle />, {
state: { monitorStatus: { status: defaultMonitorStatus, loading: false } },
});
expect(screen.getByText('HTTP ping')).toBeInTheDocument();
expect(screen.queryByText(/BETA/)).not.toBeInTheDocument();
expect(
screen.queryByRole('link', { name: 'See more External link (opens in a new tab or window)' })
).not.toBeInTheDocument();
});
it('does not render beta disclaimer for tcp', () => {
render(<MonitorPageTitle />, {
state: { monitorStatus: { status: defaultTCPMonitorStatus, loading: false } },
});
expect(screen.getByText('TCP ping')).toBeInTheDocument();
expect(screen.queryByText(/BETA/)).not.toBeInTheDocument();
expect(
screen.queryByRole('link', { name: 'See more External link (opens in a new tab or window)' })
).not.toBeInTheDocument();
});
it('renders badge and does not render beta disclaimer for icmp', () => {
render(<MonitorPageTitle />, {
state: { monitorStatus: { status: defaultICMPMonitorStatus, loading: false } },
});
expect(screen.getByText('ICMP ping')).toBeInTheDocument();
expect(screen.queryByText(/BETA/)).not.toBeInTheDocument();
expect(
screen.queryByRole('link', { name: 'See more External link (opens in a new tab or window)' })
).not.toBeInTheDocument();
});
});

View file

@ -5,7 +5,15 @@
* 2.0.
*/
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiLink } from '@elastic/eui';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiLink,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { useSelector } from 'react-redux';
@ -95,26 +103,26 @@ export const MonitorPageTitle: React.FC = () => {
<EuiSpacer size="s" />
<EuiFlexGroup wrap={false} gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
{type && (
{isBrowser && type && (
<EuiBadge color="hollow">
{renderMonitorType(type)}{' '}
{isBrowser && (
<FormattedMessage
id="xpack.uptime.monitorDetails.title.disclaimer.description"
defaultMessage="(BETA)"
/>
)}
<FormattedMessage
id="xpack.uptime.monitorDetails.title.disclaimer.description"
defaultMessage="(BETA)"
/>
</EuiBadge>
)}
</EuiFlexItem>
{isBrowser && (
<EuiFlexItem grow={false}>
<EuiLink href="https://www.elastic.co/what-is/synthetic-monitoring" target="_blank">
<FormattedMessage
id="xpack.uptime.monitorDetails.title.disclaimer.link"
defaultMessage="See more"
/>
</EuiLink>
<EuiText>
<EuiLink href="https://www.elastic.co/what-is/synthetic-monitoring" target="_blank">
<FormattedMessage
id="xpack.uptime.monitorDetails.title.disclaimer.link"
defaultMessage="See more"
/>
</EuiLink>
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>

View file

@ -1,144 +0,0 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiButtonEmpty,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import moment from 'moment';
import { WaterfallChartContainer } from './waterfall/waterfall_chart_container';
export const PREVIOUS_CHECK_BUTTON_TEXT = i18n.translate(
'xpack.uptime.synthetics.stepDetail.previousCheckButtonText',
{
defaultMessage: 'Previous check',
}
);
export const NEXT_CHECK_BUTTON_TEXT = i18n.translate(
'xpack.uptime.synthetics.stepDetail.nextCheckButtonText',
{
defaultMessage: 'Next check',
}
);
interface Props {
checkGroup: string;
stepName?: string;
stepIndex: number;
totalSteps: number;
hasPreviousStep: boolean;
hasNextStep: boolean;
handlePreviousStep: () => void;
handleNextStep: () => void;
handleNextRun: () => void;
handlePreviousRun: () => void;
previousCheckGroup?: string;
nextCheckGroup?: string;
checkTimestamp?: string;
dateFormat: string;
}
export const StepDetail: React.FC<Props> = ({
dateFormat,
stepName,
checkGroup,
stepIndex,
totalSteps,
hasPreviousStep,
hasNextStep,
handlePreviousStep,
handleNextStep,
handlePreviousRun,
handleNextRun,
previousCheckGroup,
nextCheckGroup,
checkTimestamp,
}) => {
return (
<>
<EuiFlexGroup justifyContent="spaceBetween" responsive={false} wrap>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h1>{stepName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handlePreviousStep}
disabled={!hasPreviousStep}
iconType="arrowLeft"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<FormattedMessage
id="xpack.uptime.synthetics.stepDetail.totalSteps"
defaultMessage="Step {stepIndex} of {totalSteps}"
values={{
stepIndex,
totalSteps,
}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handleNextStep}
disabled={!hasNextStep}
iconType="arrowRight"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handlePreviousRun}
disabled={!previousCheckGroup}
iconType="arrowLeft"
aria-label={PREVIOUS_CHECK_BUTTON_TEXT}
>
{PREVIOUS_CHECK_BUTTON_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{moment(checkTimestamp).format(dateFormat)}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handleNextRun}
disabled={!nextCheckGroup}
iconType="arrowRight"
iconSide="right"
aria-label={NEXT_CHECK_BUTTON_TEXT}
>
{NEXT_CHECK_BUTTON_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<WaterfallChartContainer checkGroup={checkGroup} stepIndex={stepIndex} />
</>
);
};

View file

@ -13,8 +13,12 @@ import { useHistory } from 'react-router-dom';
import { getJourneySteps } from '../../../../state/actions/journey';
import { journeySelector } from '../../../../state/selectors';
import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public';
import { StepDetail } from './step_detail';
import { useMonitorBreadcrumb } from './use_monitor_breadcrumb';
import { ClientPluginsStart } from '../../../../apps/plugin';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { StepPageTitle } from './step_page_title';
import { StepPageNavigation } from './step_page_nav';
import { WaterfallChartContainer } from './waterfall/waterfall_chart_container';
export const NO_STEP_DATA = i18n.translate('xpack.uptime.synthetics.stepDetail.noData', {
defaultMessage: 'No data could be found for this step',
@ -66,8 +70,40 @@ export const StepDetailContainer: React.FC<Props> = ({ checkGroup, stepIndex })
history.push(`/journey/${journey?.details?.previous?.checkGroup}/step/1`);
}, [history, journey?.details?.previous?.checkGroup]);
const {
services: { observability },
} = useKibana<ClientPluginsStart>();
const PageTemplateComponent = observability.navigation.PageTemplate;
return (
<>
<PageTemplateComponent
pageHeader={{
pageTitle:
journey && activeStep ? (
<StepPageTitle
stepName={activeStep.synthetics?.step?.name}
stepIndex={stepIndex}
totalSteps={journey.steps.length}
hasPreviousStep={hasPreviousStep}
hasNextStep={hasNextStep}
handlePreviousStep={handlePreviousStep}
handleNextStep={handleNextStep}
/>
) : null,
rightSideItems: journey
? [
<StepPageNavigation
dateFormat={dateFormat}
handleNextRun={handleNextRun}
handlePreviousRun={handlePreviousRun}
nextCheckGroup={journey.details?.next?.checkGroup}
previousCheckGroup={journey.details?.previous?.checkGroup}
checkTimestamp={journey.details?.timestamp}
/>,
]
: [],
}}
>
<EuiPanel>
{(!journey || journey.loading) && (
<EuiFlexGroup justifyContent="center">
@ -86,24 +122,9 @@ export const StepDetailContainer: React.FC<Props> = ({ checkGroup, stepIndex })
</EuiFlexGroup>
)}
{journey && activeStep && !journey.loading && (
<StepDetail
checkGroup={checkGroup}
stepName={activeStep.synthetics?.step?.name}
stepIndex={stepIndex}
totalSteps={journey.steps.length}
hasPreviousStep={hasPreviousStep}
hasNextStep={hasNextStep}
handlePreviousStep={handlePreviousStep}
handleNextStep={handleNextStep}
handleNextRun={handleNextRun}
handlePreviousRun={handlePreviousRun}
previousCheckGroup={journey.details?.previous?.checkGroup}
nextCheckGroup={journey.details?.next?.checkGroup}
checkTimestamp={journey.details?.timestamp}
dateFormat={dateFormat}
/>
<WaterfallChartContainer checkGroup={checkGroup} stepIndex={stepIndex} />
)}
</EuiPanel>
</>
</PageTemplateComponent>
);
};

View file

@ -0,0 +1,71 @@
/*
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
export const PREVIOUS_CHECK_BUTTON_TEXT = i18n.translate(
'xpack.uptime.synthetics.stepDetail.previousCheckButtonText',
{
defaultMessage: 'Previous check',
}
);
export const NEXT_CHECK_BUTTON_TEXT = i18n.translate(
'xpack.uptime.synthetics.stepDetail.nextCheckButtonText',
{
defaultMessage: 'Next check',
}
);
interface Props {
previousCheckGroup?: string;
dateFormat: string;
checkTimestamp?: string;
nextCheckGroup?: string;
handlePreviousRun: () => void;
handleNextRun: () => void;
}
export const StepPageNavigation = ({
previousCheckGroup,
dateFormat,
handleNextRun,
handlePreviousRun,
checkTimestamp,
nextCheckGroup,
}: Props) => {
return (
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handlePreviousRun}
disabled={!previousCheckGroup}
iconType="arrowLeft"
aria-label={PREVIOUS_CHECK_BUTTON_TEXT}
>
{PREVIOUS_CHECK_BUTTON_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{moment(checkTimestamp).format(dateFormat)}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handleNextRun}
disabled={!nextCheckGroup}
iconType="arrowRight"
iconSide="right"
aria-label={NEXT_CHECK_BUTTON_TEXT}
>
{NEXT_CHECK_BUTTON_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,69 @@
/*
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
interface Props {
stepName?: string;
stepIndex: number;
totalSteps: number;
hasPreviousStep: boolean;
hasNextStep: boolean;
handlePreviousStep: () => void;
handleNextStep: () => void;
}
export const StepPageTitle = ({
stepName,
stepIndex,
totalSteps,
handleNextStep,
handlePreviousStep,
hasNextStep,
hasPreviousStep,
}: Props) => {
return (
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h1>{stepName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handlePreviousStep}
disabled={!hasPreviousStep}
iconType="arrowLeft"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<FormattedMessage
id="xpack.uptime.synthetics.stepDetail.totalSteps"
defaultMessage="Step {stepIndex} of {totalSteps}"
values={{
stepIndex,
totalSteps,
}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handleNextStep}
disabled={!hasNextStep}
iconType="arrowRight"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -63,6 +63,10 @@ describe('useMonitorBreadcrumbs', () => {
expect(getBreadcrumbs()).toMatchInlineSnapshot(`
Array [
Object {
"href": "",
"text": "Observability",
},
Object {
"href": "/app/uptime",
"onClick": [Function],
@ -129,6 +133,10 @@ describe('useMonitorBreadcrumbs', () => {
expect(getBreadcrumbs()).toMatchInlineSnapshot(`
Array [
Object {
"href": "",
"text": "Observability",
},
Object {
"href": "/app/uptime",
"onClick": [Function],

View file

@ -19,14 +19,8 @@ describe('useBreadcrumbs', () => {
const [getBreadcrumbs, core] = mockCore();
const expectedCrumbs: ChromeBreadcrumb[] = [
{
text: 'Crumb: ',
href: 'http://href.example.net',
},
{
text: 'Crumb II: Son of Crumb',
href: 'http://href2.example.net',
},
{ text: 'Crumb: ', href: 'http://href.example.net' },
{ text: 'Crumb II: Son of Crumb', href: 'http://href2.example.net' },
];
const Component = () => {
@ -46,7 +40,9 @@ describe('useBreadcrumbs', () => {
const urlParams: UptimeUrlParams = getSupportedUrlParams({});
expect(JSON.stringify(getBreadcrumbs())).toEqual(
JSON.stringify([makeBaseBreadcrumb('/app/uptime', urlParams)].concat(expectedCrumbs))
JSON.stringify(
makeBaseBreadcrumb('/app/uptime', '/app/observability', urlParams).concat(expectedCrumbs)
)
);
});
});
@ -58,7 +54,7 @@ const mockCore: () => [() => ChromeBreadcrumb[], any] = () => {
};
const core = {
application: {
getUrlForApp: () => '/app/uptime',
getUrlForApp: (app: string) => (app === 'uptime' ? '/app/uptime' : '/app/observability'),
navigateToUrl: jest.fn(),
},
chrome: {

View file

@ -36,34 +36,52 @@ function handleBreadcrumbClick(
}));
}
export const makeBaseBreadcrumb = (href: string, params?: UptimeUrlParams): EuiBreadcrumb => {
export const makeBaseBreadcrumb = (
uptimePath: string,
observabilityPath: string,
params?: UptimeUrlParams
): [EuiBreadcrumb, EuiBreadcrumb] => {
if (params) {
const crumbParams: Partial<UptimeUrlParams> = { ...params };
delete crumbParams.statusFilter;
const query = stringifyUrlParams(crumbParams, true);
href += query === EMPTY_QUERY ? '' : query;
uptimePath += query === EMPTY_QUERY ? '' : query;
}
return {
text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', {
defaultMessage: 'Uptime',
}),
href,
};
return [
{
text: i18n.translate('xpack.uptime.breadcrumbs.observabilityText', {
defaultMessage: 'Observability',
}),
href: observabilityPath,
},
{
text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', {
defaultMessage: 'Uptime',
}),
href: uptimePath,
},
];
};
export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => {
const params = useUrlParams()[0]();
const kibana = useKibana();
const setBreadcrumbs = kibana.services.chrome?.setBreadcrumbs;
const appPath = kibana.services.application?.getUrlForApp(PLUGIN.ID) ?? '';
const uptimePath = kibana.services.application?.getUrlForApp(PLUGIN.ID) ?? '';
const observabilityPath =
kibana.services.application?.getUrlForApp('observability-overview') ?? '';
const navigate = kibana.services.application?.navigateToUrl;
useEffect(() => {
if (setBreadcrumbs) {
setBreadcrumbs(
handleBreadcrumbClick([makeBaseBreadcrumb(appPath, params)].concat(extraCrumbs), navigate)
handleBreadcrumbClick(
makeBaseBreadcrumb(uptimePath, observabilityPath, params).concat(extraCrumbs),
navigate
)
);
}
}, [appPath, extraCrumbs, navigate, params, setBreadcrumbs]);
}, [uptimePath, observabilityPath, extraCrumbs, navigate, params, setBreadcrumbs]);
};

View file

@ -79,6 +79,12 @@ const createMockStore = () => {
};
};
const mockAppUrls: Record<string, string> = {
uptime: '/app/uptime',
observability: '/app/observability',
'/home#/tutorial/uptimeMonitors': '/home#/tutorial/uptimeMonitors',
};
/* default mock core */
const defaultCore = coreMock.createStart();
const mockCore: () => Partial<CoreStart> = () => {
@ -86,7 +92,7 @@ const mockCore: () => Partial<CoreStart> = () => {
...defaultCore,
application: {
...defaultCore.application,
getUrlForApp: () => '/app/uptime',
getUrlForApp: (app: string) => mockAppUrls[app],
navigateToUrl: jest.fn(),
capabilities: {
...defaultCore.application.capabilities,

View file

@ -5,15 +5,14 @@
* 2.0.
*/
import { useDispatch, useSelector } from 'react-redux';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { EuiSpacer } from '@elastic/eui';
import React, { useContext, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { useTrackPageview } from '../../../observability/public';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { getDynamicSettings } from '../state/actions/dynamic_settings';
import { UptimeRefreshContext } from '../contexts';
import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates';
import { getCertificatesAction } from '../state/certificates/certificates';
import { CertificateList, CertificateSearch, CertSort } from '../components/certificates';
const DEFAULT_PAGE_SIZE = 10;
@ -58,22 +57,8 @@ export const CertificatesPage: React.FC = () => {
);
}, [dispatch, page, search, sort.direction, sort.field, lastRefresh]);
const { data: certificates } = useSelector(certificatesSelector);
return (
<EuiPanel>
<EuiTitle>
<h1 className="eui-textNoWrap">
<FormattedMessage
id="xpack.uptime.certificates.heading"
defaultMessage="TLS Certificates ({total})"
values={{
total: <span data-test-subj="uptimeCertTotal">{certificates?.total ?? 0}</span>,
}}
/>
</h1>
</EuiTitle>
<>
<EuiSpacer size="m" />
<CertificateSearch setSearch={setSearch} />
<EuiSpacer size="m" />
@ -86,6 +71,6 @@ export const CertificatesPage: React.FC = () => {
}}
sort={sort}
/>
</EuiPanel>
</>
);
};

View file

@ -15,6 +15,7 @@ import { MonitorList } from '../components/overview/monitor_list/monitor_list_co
import { EmptyState, FilterGroup } from '../components/overview';
import { StatusPanel } from '../components/overview/status_panel';
import { QueryBar } from '../components/overview/query_bar/query_bar';
import { MONITORING_OVERVIEW_LABEL } from '../routes';
const EuiFlexItemStyled = styled(EuiFlexItem)`
&& {
@ -32,7 +33,7 @@ export const OverviewPageComponent = () => {
useTrackPageview({ app: 'uptime', path: 'overview' });
useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 });
useBreadcrumbs([]); // No extra breadcrumbs on overview
useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview
return (
<EmptyState>

View file

@ -148,73 +148,71 @@ export const SettingsPage: React.FC = () => {
);
return (
<>
<EuiPanel style={{ maxWidth: 1000, margin: 'auto' }}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>{cannotEditNotice}</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<div id="settings-form">
<EuiForm>
<IndicesForm
loading={dss.loading}
onChange={onChangeFormField}
formFields={formFields}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<AlertDefaultsForm
loading={dss.loading}
formFields={formFields}
onChange={onChangeFormField}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<CertificateExpirationForm
loading={dss.loading}
onChange={onChangeFormField}
formFields={formFields}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<EuiPanel style={{ maxWidth: 1000, margin: 'auto' }}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>{cannotEditNotice}</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<div id="settings-form">
<EuiForm>
<IndicesForm
loading={dss.loading}
onChange={onChangeFormField}
formFields={formFields}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<AlertDefaultsForm
loading={dss.loading}
formFields={formFields}
onChange={onChangeFormField}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<CertificateExpirationForm
loading={dss.loading}
onChange={onChangeFormField}
formFields={formFields}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="discardSettingsButton"
isDisabled={!isFormDirty || isFormDisabled}
onClick={() => {
resetForm();
}}
>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.discardSettingsButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="apply-settings-button"
onClick={onApply}
color="primary"
isDisabled={!isFormDirty || !isFormValid || isFormDisabled}
fill
>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.applySettingsButtonLabel"
defaultMessage="Apply changes"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="discardSettingsButton"
isDisabled={!isFormDirty || isFormDisabled}
onClick={() => {
resetForm();
}}
>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.discardSettingsButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="apply-settings-button"
onClick={onApply}
color="primary"
isDisabled={!isFormDirty || !isFormValid || isFormDisabled}
fill
>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.applySettingsButtonLabel"
defaultMessage="Apply changes"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { useTrackPageview } from '../../../../observability/public';
import { useInitApp } from '../../hooks/use_init_app';
import { StepsList } from '../../components/synthetics/check_steps/steps_list';
@ -14,6 +14,7 @@ import { useCheckSteps } from '../../components/synthetics/check_steps/use_check
import { ChecksNavigation } from './checks_navigation';
import { useMonitorBreadcrumb } from '../../components/monitor/synthetics/step_detail/use_monitor_breadcrumb';
import { EmptyJourney } from '../../components/synthetics/empty_journey';
import { ClientPluginsStart } from '../../apps/plugin';
export const SyntheticsCheckSteps: React.FC = () => {
useInitApp();
@ -24,21 +25,22 @@ export const SyntheticsCheckSteps: React.FC = () => {
useMonitorBreadcrumb({ details, activeStep: details?.journey });
const {
services: { observability },
} = useKibana<ClientPluginsStart>();
const PageTemplateComponent = observability.navigation.PageTemplate;
return (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle>
<h1>{details?.journey?.monitor.name || details?.journey?.monitor.id}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{details && <ChecksNavigation timestamp={details.timestamp} details={details} />}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<PageTemplateComponent
pageHeader={{
pageTitle: details?.journey?.monitor.name || details?.journey?.monitor.id,
rightSideItems: [
details ? <ChecksNavigation timestamp={details.timestamp} details={details} /> : null,
],
}}
>
<StepsList data={steps} loading={loading} error={error} />
{(!steps || steps.length === 0) && !loading && <EmptyJourney checkGroup={checkGroup} />}
</>
</PageTemplateComponent>
);
};

View file

@ -6,8 +6,9 @@
*/
import React, { FC, useEffect } from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { Props as PageHeaderProps, PageHeader } from './components/common/header/page_header';
import { Route, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
CERTIFICATES_ROUTE,
MONITOR_ROUTE,
@ -21,6 +22,13 @@ import { CertificatesPage } from './pages/certificates';
import { UptimePage, useUptimeTelemetry } from './hooks';
import { OverviewPageComponent } from './pages/overview';
import { SyntheticsCheckSteps } from './pages/synthetics/synthetics_checks';
import { ClientPluginsStart } from './apps/plugin';
import { MonitorPageTitle } from './components/monitor/monitor_title';
import { UptimeDatePicker } from './components/common/uptime_date_picker';
import { useKibana } from '../../../../src/plugins/kibana_react/public';
import { CertRefreshBtn } from './components/certificates/cert_refresh_btn';
import { CertificateTitle } from './components/certificates/certificate_title';
import { SyntheticsCallout } from './components/overview/synthetics_callout';
interface RouteProps {
path: string;
@ -28,11 +36,15 @@ interface RouteProps {
dataTestSubj: string;
title: string;
telemetryId: UptimePage;
headerProps?: PageHeaderProps;
pageHeader?: { pageTitle: string | JSX.Element; rightSideItems?: JSX.Element[] };
}
const baseTitle = 'Uptime - Kibana';
export const MONITORING_OVERVIEW_LABEL = i18n.translate('xpack.uptime.overview.heading', {
defaultMessage: 'Monitoring overview',
});
const Routes: RouteProps[] = [
{
title: `Monitor | ${baseTitle}`,
@ -40,9 +52,9 @@ const Routes: RouteProps[] = [
component: MonitorPage,
dataTestSubj: 'uptimeMonitorPage',
telemetryId: UptimePage.Monitor,
headerProps: {
showDatePicker: true,
showMonitorTitle: true,
pageHeader: {
pageTitle: <MonitorPageTitle />,
rightSideItems: [<UptimeDatePicker />],
},
},
{
@ -51,8 +63,10 @@ const Routes: RouteProps[] = [
component: SettingsPage,
dataTestSubj: 'uptimeSettingsPage',
telemetryId: UptimePage.Settings,
headerProps: {
showTabs: true,
pageHeader: {
pageTitle: (
<FormattedMessage id="xpack.uptime.settings.heading" defaultMessage="Uptime settings" />
),
},
},
{
@ -61,9 +75,9 @@ const Routes: RouteProps[] = [
component: CertificatesPage,
dataTestSubj: 'uptimeCertificatesPage',
telemetryId: UptimePage.Certificates,
headerProps: {
showCertificateRefreshBtn: true,
showTabs: true,
pageHeader: {
pageTitle: <CertificateTitle />,
rightSideItems: [<CertRefreshBtn />],
},
},
{
@ -86,9 +100,9 @@ const Routes: RouteProps[] = [
component: OverviewPageComponent,
dataTestSubj: 'uptimeOverviewPage',
telemetryId: UptimePage.Overview,
headerProps: {
showDatePicker: true,
showTabs: true,
pageHeader: {
pageTitle: MONITORING_OVERVIEW_LABEL,
rightSideItems: [<UptimeDatePicker />],
},
},
];
@ -106,31 +120,31 @@ const RouteInit: React.FC<Pick<RouteProps, 'path' | 'title' | 'telemetryId'>> =
};
export const PageRouter: FC = () => {
const {
services: { observability },
} = useKibana<ClientPluginsStart>();
const PageTemplateComponent = observability.navigation.PageTemplate;
return (
<>
{/* Independent page header route that matches all paths and passes appropriate header props */}
{/* Prevents the header from being remounted on route changes */}
<Route
path={[...Routes.map((route) => route.path)]}
exact={true}
render={({ match }: RouteComponentProps) => {
const routeProps: RouteProps | undefined = Routes.find(
(route: RouteProps) => route?.path === match?.path
);
return routeProps?.headerProps && <PageHeader {...routeProps?.headerProps} />;
}}
/>
<Switch>
{Routes.map(({ title, path, component: RouteComponent, dataTestSubj, telemetryId }) => (
<Switch>
{Routes.map(
({ title, path, component: RouteComponent, dataTestSubj, telemetryId, pageHeader }) => (
<Route path={path} key={telemetryId} exact={true}>
<div data-test-subj={dataTestSubj}>
<SyntheticsCallout />
<RouteInit title={title} path={path} telemetryId={telemetryId} />
<RouteComponent />
{pageHeader ? (
<PageTemplateComponent pageHeader={pageHeader}>
<RouteComponent />
</PageTemplateComponent>
) : (
<RouteComponent />
)}
</div>
</Route>
))}
<Route component={NotFoundPage} />
</Switch>
</>
)
)}
<Route component={NotFoundPage} />
</Switch>
);
};

View file

@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const find = getService('find');
const PageObjects = getPageObjects(['common', 'timePicker', 'header']);
@ -27,7 +28,7 @@ export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderCo
return {
async hasViewCertButton() {
return retry.tryForTime(15000, async () => {
await testSubjects.existOrFail('uptimeCertificatesLink');
await find.existsByCssSelector('[href="/app/uptime/certificates"]');
});
},
async certificateExists(cert: { certId: string; monitorId: string }) {

View file

@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const find = getService('find');
const PageObjects = getPageObjects(['common', 'timePicker', 'header']);
const goToUptimeRoot = async () => {
@ -70,8 +71,8 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
goToCertificates: async () => {
if (!(await testSubjects.exists('uptimeCertificatesPage', { timeout: 0 }))) {
return retry.try(async () => {
if (await testSubjects.exists('uptimeCertificatesLink', { timeout: 0 })) {
await testSubjects.click('uptimeCertificatesLink', 10000);
if (await find.existsByCssSelector('[href="/app/uptime/certificates"]', 0)) {
await find.clickByCssSelector('[href="/app/uptime/certificates"]');
}
await testSubjects.existOrFail('uptimeCertificatesPage');
});