[Uptime] Support URL parameters (#35375) (#35650)

* WIP. Trying some things.

* Introduce useUrlParams hook to modify path.

* WIP code on setting/reading date range from url params.

* Create constants file for app defaults.

* Add functions for parsing supported URL parameters.

* Add index entry and update useUrlParams hook.

* Remove defaults and update application to store persisted state in the URL.

* More temp code.

* Support URL params and fix filters.

* Update Monitor page to accept URL parameters.

* Rename a test folder and add tests for new helper functions.

* Add functional test for filter query.

* Add missing prop to test component.

* Update breadcrumb functions to accept search parameters.

* Update app to support forwarding of search params in in-app links.

* Write snapshot for status bar test.

* Fix memory leak.
This commit is contained in:
Justin Kambic 2019-04-26 12:28:11 -04:00 committed by GitHub
parent 5d5bfced0c
commit a532edf7bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 883 additions and 466 deletions

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
export const CLIENT_DEFAULTS = {
// 60 seconds
AUTOREFRESH_INTERVAL: 60 * 1000,
// polling defaults to "on"
AUTOREFRESH_IS_PAUSED: false,
DATE_RANGE_START: 'now-15m',
DATE_RANGE_END: 'now',
SEARCH: '',
SELECTED_PING_LIST_STATUS: 'down',
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { CLIENT_DEFAULTS } from './client_defaults';
export { INDEX_NAMES } from './index_names';
export { PLUGIN } from './plugin';
export { QUERY } from './query';

View file

@ -11,16 +11,18 @@ export interface UMBreadcrumb {
href?: string;
}
export const overviewBreadcrumb: UMBreadcrumb = {
const makeOverviewBreadcrumb = (search?: string): UMBreadcrumb => ({
text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', {
defaultMessage: 'Uptime',
}),
href: '#/',
};
href: `#/${search ? search : ''}`,
});
export const getOverviewPageBreadcrumbs = (): UMBreadcrumb[] => [overviewBreadcrumb];
export const getOverviewPageBreadcrumbs = (search?: string): UMBreadcrumb[] => [
makeOverviewBreadcrumb(search),
];
export const getMonitorPageBreadcrumb = (name: string): UMBreadcrumb[] => [
overviewBreadcrumb,
export const getMonitorPageBreadcrumb = (name: string, search?: string): UMBreadcrumb[] => [
makeOverviewBreadcrumb(search),
{ text: name },
];

View file

@ -1,196 +1,200 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterBar component renders the component without errors 1`] = `
<EuiSearchBar
box={
Object {
"incremental": false,
<div
data-test-subj="xpack.uptime.filterBar"
>
<EuiSearchBar
box={
Object {
"incremental": false,
}
}
}
className="euiFlexGroup--gutterSmall"
filters={
Array [
Object {
"field": "monitor.status",
"items": Array [
Object {
"name": "Up",
"value": "up",
},
Object {
"name": "Down",
"value": "down",
},
],
"type": "field_value_toggle_group",
},
Object {
"field": "monitor.id",
"multiSelect": false,
"name": "ID",
"options": Array [
Object {
"value": "auto-tcp-0X81440A68E839814C",
"view": "auto-tcp-0X81440A68E839814C",
},
Object {
"value": "auto-http-0X3675F89EF0612091",
"view": "auto-http-0X3675F89EF0612091",
},
Object {
"value": "auto-http-0X970CBD2F2102BFA8",
"view": "auto-http-0X970CBD2F2102BFA8",
},
Object {
"value": "auto-http-0X131221E73F825974",
"view": "auto-http-0X131221E73F825974",
},
Object {
"value": "auto-http-0X9CB71300ABD5A2A8",
"view": "auto-http-0X9CB71300ABD5A2A8",
},
Object {
"value": "auto-http-0XD9AE729FC1C1E04A",
"view": "auto-http-0XD9AE729FC1C1E04A",
},
Object {
"value": "auto-http-0XDD2D4E60FD4A61C3",
"view": "auto-http-0XDD2D4E60FD4A61C3",
},
Object {
"value": "auto-http-0XA8096548ECEB85B7",
"view": "auto-http-0XA8096548ECEB85B7",
},
Object {
"value": "auto-http-0XC9CDA429418EDC2B",
"view": "auto-http-0XC9CDA429418EDC2B",
},
Object {
"value": "auto-http-0XE3B163481423197D",
"view": "auto-http-0XE3B163481423197D",
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
Object {
"field": "monitor.name",
"multiSelect": false,
"name": "Name",
"options": Array [],
"searchThreshold": 2,
"type": "field_value_selection",
},
Object {
"field": "url.full",
"multiSelect": false,
"name": "URL",
"options": Array [
Object {
"value": "tcp://localhost:9200",
"view": "tcp://localhost:9200",
},
Object {
"value": "http://localhost:12349/",
"view": "http://localhost:12349/",
},
Object {
"value": "http://www.google.com/",
"view": "http://www.google.com/",
},
Object {
"value": "https://www.google.com/",
"view": "https://www.google.com/",
},
Object {
"value": "https://www.github.com/",
"view": "https://www.github.com/",
},
Object {
"value": "http://www.reddit.com/",
"view": "http://www.reddit.com/",
},
Object {
"value": "https://www.elastic.co",
"view": "https://www.elastic.co",
},
Object {
"value": "http://www.example.com/",
"view": "http://www.example.com/",
},
Object {
"value": "https://www.wikipedia.org/",
"view": "https://www.wikipedia.org/",
},
Object {
"value": "https://news.google.com/",
"view": "https://news.google.com/",
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
Object {
"field": "url.port",
"multiSelect": false,
"name": "Port",
"options": Array [
Object {
"value": 9200,
"view": 9200,
},
Object {
"value": 12349,
"view": 12349,
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
Object {
"field": "monitor.type",
"multiSelect": false,
"name": "Scheme",
"options": Array [
Object {
"value": "tcp",
"view": "tcp",
},
Object {
"value": "http",
"view": "http",
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
]
}
onChange={[MockFunction]}
schema={
Object {
"fields": Object {
"monitor.host": Object {
"type": "string",
className="euiFlexGroup--gutterSmall"
filters={
Array [
Object {
"field": "monitor.status",
"items": Array [
Object {
"name": "Up",
"value": "up",
},
Object {
"name": "Down",
"value": "down",
},
],
"type": "field_value_toggle_group",
},
"monitor.id": Object {
"type": "string",
Object {
"field": "monitor.id",
"multiSelect": false,
"name": "ID",
"options": Array [
Object {
"value": "auto-tcp-0X81440A68E839814C",
"view": "auto-tcp-0X81440A68E839814C",
},
Object {
"value": "auto-http-0X3675F89EF0612091",
"view": "auto-http-0X3675F89EF0612091",
},
Object {
"value": "auto-http-0X970CBD2F2102BFA8",
"view": "auto-http-0X970CBD2F2102BFA8",
},
Object {
"value": "auto-http-0X131221E73F825974",
"view": "auto-http-0X131221E73F825974",
},
Object {
"value": "auto-http-0X9CB71300ABD5A2A8",
"view": "auto-http-0X9CB71300ABD5A2A8",
},
Object {
"value": "auto-http-0XD9AE729FC1C1E04A",
"view": "auto-http-0XD9AE729FC1C1E04A",
},
Object {
"value": "auto-http-0XDD2D4E60FD4A61C3",
"view": "auto-http-0XDD2D4E60FD4A61C3",
},
Object {
"value": "auto-http-0XA8096548ECEB85B7",
"view": "auto-http-0XA8096548ECEB85B7",
},
Object {
"value": "auto-http-0XC9CDA429418EDC2B",
"view": "auto-http-0XC9CDA429418EDC2B",
},
Object {
"value": "auto-http-0XE3B163481423197D",
"view": "auto-http-0XE3B163481423197D",
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
"monitor.ip": Object {
"type": "string",
Object {
"field": "monitor.name",
"multiSelect": false,
"name": "Name",
"options": Array [],
"searchThreshold": 2,
"type": "field_value_selection",
},
"monitor.scheme": Object {
"type": "string",
Object {
"field": "url.full",
"multiSelect": false,
"name": "URL",
"options": Array [
Object {
"value": "tcp://localhost:9200",
"view": "tcp://localhost:9200",
},
Object {
"value": "http://localhost:12349/",
"view": "http://localhost:12349/",
},
Object {
"value": "http://www.google.com/",
"view": "http://www.google.com/",
},
Object {
"value": "https://www.google.com/",
"view": "https://www.google.com/",
},
Object {
"value": "https://www.github.com/",
"view": "https://www.github.com/",
},
Object {
"value": "http://www.reddit.com/",
"view": "http://www.reddit.com/",
},
Object {
"value": "https://www.elastic.co",
"view": "https://www.elastic.co",
},
Object {
"value": "http://www.example.com/",
"view": "http://www.example.com/",
},
Object {
"value": "https://www.wikipedia.org/",
"view": "https://www.wikipedia.org/",
},
Object {
"value": "https://news.google.com/",
"view": "https://news.google.com/",
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
"monitor.status": Object {
"type": "string",
Object {
"field": "url.port",
"multiSelect": false,
"name": "Port",
"options": Array [
Object {
"value": 9200,
"view": 9200,
},
Object {
"value": 12349,
"view": 12349,
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
"url.port": Object {
"type": "number",
Object {
"field": "monitor.type",
"multiSelect": false,
"name": "Scheme",
"options": Array [
Object {
"value": "tcp",
"view": "tcp",
},
Object {
"value": "http",
"view": "http",
},
],
"searchThreshold": 2,
"type": "field_value_selection",
},
},
"strict": true,
]
}
}
/>
onChange={[MockFunction]}
schema={
Object {
"fields": Object {
"monitor.host": Object {
"type": "string",
},
"monitor.id": Object {
"type": "string",
},
"monitor.ip": Object {
"type": "string",
},
"monitor.scheme": Object {
"type": "string",
},
"monitor.status": Object {
"type": "string",
},
"url.port": Object {
"type": "number",
},
},
"strict": true,
}
}
/>
</div>
`;

View file

@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
<div
class="euiPanel euiPanel--paddingMedium"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
aria-label="Monitor status"
class="euiHealth"
style="line-height:inherit"
>
<div
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
class="euiIcon euiIcon--medium euiIcon--success"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<circle
cx="8"
cy="8"
id="dot-a"
r="4"
/>
</defs>
<use
href="#dot-a"
/>
</svg>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
Up
</div>
</div>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
aria-label="Monitor URL link"
class="euiLink euiLink--primary"
href="https://www.example.com/"
rel="noopener noreferrer"
target="_blank"
>
https://www.example.com/
</a>
</div>
</div>
<div
aria-label="Monitor duration in milliseconds"
class="euiFlexItem euiFlexItem--flexGrowZero"
>
1235ms
</div>
<div
aria-label="Time since last check"
class="euiFlexItem euiFlexItem--flexGrowZero"
>
15 minutes ago
</div>
</div>
</div>
`;

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
import React from 'react';
import { renderWithIntl } from 'test_utils/enzyme_helpers';
import { Ping } from '../../../../common/graphql/types';
@ -15,7 +16,9 @@ describe('MonitorStatusBar component', () => {
beforeEach(() => {
monitorStatus = [
{
timestamp: '1554820772000',
timestamp: moment(new Date())
.subtract(15, 'm')
.toString(),
monitor: {
duration: {
us: 1234567,
@ -33,6 +36,6 @@ describe('MonitorStatusBar component', () => {
const component = renderWithIntl(
<MonitorStatusBarComponent loading={false} data={{ monitorStatus }} monitorId="foo" />
);
expect(component.text()).toContain('1235ms');
expect(component).toMatchSnapshot();
});
});

View file

@ -193,6 +193,7 @@ describe('PingList component', () => {
data={{ allPings }}
onUpdateApp={jest.fn()}
onSelectedStatusUpdate={jest.fn()}
selectedOption="down"
/>
);
expect(component).toMatchSnapshot();

View file

@ -23,13 +23,17 @@ import { ErrorListItem, Ping } from '../../../common/graphql/types';
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order';
import { errorListQuery } from '../../queries';
interface ErrorListProps {
linkParameters?: string;
}
interface ErrorListQueryResult {
errorList?: ErrorListItem[];
}
type Props = UptimeGraphQLQueryProps<ErrorListQueryResult>;
type Props = UptimeGraphQLQueryProps<ErrorListQueryResult> & ErrorListProps;
export const ErrorListComponent = ({ data, loading }: Props) => (
export const ErrorListComponent = ({ data, linkParameters, loading }: Props) => (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h5>
@ -70,7 +74,7 @@ export const ErrorListComponent = ({ data, loading }: Props) => (
}),
render: (id: string, { name }: ErrorListItem) => (
<EuiLink>
<Link to={`/monitor/${id}`}>{name || id}</Link>
<Link to={`/monitor/${id}${linkParameters}`}>{name || id}</Link>
</EuiLink>
),
width: '25%',
@ -106,7 +110,7 @@ export const ErrorListComponent = ({ data, loading }: Props) => (
</EuiPanel>
);
export const ErrorList = withUptimeGraphQL<ErrorListQueryResult>(
export const ErrorList = withUptimeGraphQL<ErrorListQueryResult, ErrorListProps>(
ErrorListComponent,
errorListQuery
);

View file

@ -20,7 +20,7 @@ interface FilterBarQueryResult {
}
interface FilterBarProps {
currentQuery?: object;
currentQuery?: string;
updateQuery: UptimeSearchBarQueryChangeHandler;
}
@ -125,14 +125,16 @@ export const FilterBarComponent = ({ currentQuery, data, updateQuery }: Props) =
},
];
return (
<EuiSearchBar
box={{ incremental: false }}
className="euiFlexGroup--gutterSmall"
onChange={updateQuery}
filters={filters}
query={currentQuery}
schema={filterBarSearchSchema}
/>
<div data-test-subj="xpack.uptime.filterBar">
<EuiSearchBar
box={{ incremental: false }}
className="euiFlexGroup--gutterSmall"
onChange={updateQuery}
filters={filters}
query={currentQuery}
schema={filterBarSearchSchema}
/>
</div>
);
};

View file

@ -41,6 +41,7 @@ interface MonitorListQueryResult {
interface MonitorListProps {
dangerColor: string;
linkParameters?: string;
}
type Props = UptimeGraphQLQueryProps<MonitorListQueryResult> & MonitorListProps;
@ -52,7 +53,7 @@ const monitorListPagination = {
pageSizeOptions: [5, 10, 20, 50],
};
export const MonitorListComponent = ({ dangerColor, data, loading }: Props) => (
export const MonitorListComponent = ({ dangerColor, data, linkParameters, loading }: Props) => (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h5>
@ -98,7 +99,10 @@ export const MonitorListComponent = ({ dangerColor, data, loading }: Props) => (
}),
render: (id: string, monitor: LatestMonitor) => (
<EuiLink>
<Link data-test-subj={`monitor-page-link-${id}`} to={`/monitor/${id}`}>
<Link
data-test-subj={`monitor-page-link-${id}`}
to={`/monitor/${id}${linkParameters}`}
>
{monitor.ping && monitor.ping.monitor && monitor.ping.monitor.name
? monitor.ping.monitor.name
: id}

View file

@ -95,7 +95,7 @@ export const MonitorStatusBarComponent = ({ data, monitorId }: Props) => {
)}
grow={false}
>
{moment(parseInt(timestamp, 10)).fromNow()}
{moment(new Date(timestamp).valueOf()).fromNow()}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { get } from 'lodash';
import moment from 'moment';
import React, { Fragment, useEffect, useState } from 'react';
import React, { Fragment, useEffect } from 'react';
import { Ping, PingResults } from '../../../common/graphql/types';
import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../lib/helper';
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order';
@ -35,6 +35,7 @@ interface PingListQueryResult {
interface PingListProps {
onUpdateApp: () => void;
onSelectedStatusUpdate: (status: string | null) => void;
selectedOption: string;
}
type Props = UptimeGraphQLQueryProps<PingListQueryResult> & PingListProps;
@ -44,8 +45,9 @@ export const PingListComponent = ({
loading,
onSelectedStatusUpdate,
onUpdateApp,
selectedOption,
}: Props) => {
const [statusOptions] = useState<EuiComboBoxOptionProps[]>([
const statusOptions: EuiComboBoxOptionProps[] = [
{
label: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', {
defaultMessage: 'All',
@ -64,8 +66,7 @@ export const PingListComponent = ({
}),
value: 'down',
},
]);
const [selectedOption, setSelectedOption] = useState<EuiComboBoxOptionProps>(statusOptions[2]);
];
const columns = [
{
field: 'monitor.status',
@ -195,18 +196,17 @@ export const PingListComponent = ({
<EuiComboBox
isClearable={false}
singleSelection={{ asPlainText: true }}
selectedOptions={[selectedOption || statusOptions[2]]}
selectedOptions={[
statusOptions.find(({ value }) => value === selectedOption) || statusOptions[2],
]}
options={statusOptions}
aria-label={i18n.translate('xpack.uptime.pingList.statusLabel', {
defaultMessage: 'Status',
})}
onChange={(selectedOptions: EuiComboBoxOptionProps[]) => {
if (selectedOptions[0]) {
setSelectedOption(selectedOptions[0]);
}
if (typeof selectedOptions[0].value === 'string') {
// @ts-ignore it's definitely a string
onSelectedStatusUpdate(
// @ts-ignore it's definitely a string
selectedOptions[0].value !== '' ? selectedOptions[0].value : null
);
}

View file

@ -0,0 +1,57 @@
/*
* 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 { EuiSuperDatePicker } from '@elastic/eui';
import React from 'react';
import { useUrlParams } from '../../hooks';
// TODO: when EUI exports types for this, this should be replaced
interface SuperDateRangePickerRangeChangedEvent {
start: string;
end: string;
}
interface SuperDateRangePickerRefreshChangedEvent {
isPaused: boolean;
refreshInterval?: number;
}
interface Props {
history: any;
location: any;
refreshApp: () => void;
}
type UptimeDatePickerProps = Props;
export const UptimeDatePicker = (props: UptimeDatePickerProps) => {
const { history, location, refreshApp } = props;
const [
{ autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd },
updateUrl,
] = useUrlParams(history, location);
return (
<EuiSuperDatePicker
start={dateRangeStart}
end={dateRangeEnd}
isPaused={autorefreshIsPaused}
refreshInterval={autorefreshInterval}
onTimeChange={({ start, end }: SuperDateRangePickerRangeChangedEvent) => {
updateUrl({ dateRangeStart: start, dateRangeEnd: end });
refreshApp();
}}
// @ts-ignore onRefresh is not defined on EuiSuperDatePicker's type yet
onRefresh={refreshApp}
onRefreshChange={({ isPaused, refreshInterval }: SuperDateRangePickerRefreshChangedEvent) => {
updateUrl({
autorefreshInterval:
refreshInterval === undefined ? autorefreshInterval : refreshInterval,
autorefreshPaused: isPaused,
});
}}
/>
);
};

View file

@ -36,21 +36,41 @@ export function withUptimeGraphQL<T, P = {}>(WrappedComponent: any, query: any)
return withApollo((props: Props) => {
const { lastRefresh } = useContext(UptimeRefreshContext);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<T | undefined>(undefined);
const [errors, setErrors] = useState<GraphQLError[] | undefined>(undefined);
let updateState = (
loadingVal: boolean,
dataVal: T | undefined,
errorsVal: GraphQLError[] | undefined
) => {
setLoading(loadingVal);
setData(dataVal);
setErrors(errorsVal);
};
const { client, implementsCustomErrorState, variables } = props;
const fetch = () => {
setLoading(true);
client.query<T>({ fetchPolicy: 'network-only', query, variables }).then((result: any) => {
setData(result.data);
setLoading(result.loading);
setErrors(result.errors);
updateState(result.loading, result.data, result.errors);
});
};
useEffect(
() => {
fetch();
/**
* If the `then` handler in `fetch`'s promise is fired after
* this component has unmounted, it will try to set state on an
* unmounted component, which indicates a memory leak and will trigger
* React warnings.
*
* We counteract this side effect by providing a cleanup function that will
* reassign the update function to do nothing with the returned values.
*/
return () => {
updateState = () => {};
};
},
[variables, lastRefresh]
);

View file

@ -0,0 +1,37 @@
/*
* 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 { useUrlParams } from '../use_url_params';
describe('useUrlParams', () => {
it('returns the expected params and an update function', () => {
const history: any[] = [];
const location = { pathname: '/', search: '_g=()' };
const [params, updateFunction] = useUrlParams(history, location);
expect(params).toEqual({
autorefreshInterval: 60000,
autorefreshIsPaused: false,
dateRangeStart: 'now-15m',
dateRangeEnd: 'now',
search: '',
selectedPingStatus: 'down',
});
expect(updateFunction).toBeInstanceOf(Function);
});
it('returns an update URL function that pushes a new URL to the history object', () => {
const history: any[] = [];
const location = { pathname: '/', search: '_g=()' };
const [, updateFunction] = useUrlParams(history, location);
const nextPath = updateFunction({ search: 'monitor.id:foo status:down' });
expect(nextPath).toEqual('/?_g=()&search=monitor.id%3Afoo%20status%3Adown');
expect(history).toHaveLength(1);
expect(history[0]).toEqual({
pathname: '/',
search: '_g=()&search=monitor.id%3Afoo%20status%3Adown',
});
});
});

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { useUrlParams } from './use_url_params';

View file

@ -0,0 +1,36 @@
/*
* 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 qs from 'querystring';
import {
UptimeUrlParams,
getSupportedUrlParams,
} from '../lib/helper/url_params/get_supported_url_params';
interface Location {
pathname: string;
search: string;
}
export const useUrlParams = (
history: any,
location: Location
): [UptimeUrlParams, (updatedParams: any) => string] => {
const { pathname, search } = location;
const currentParams: any = qs.parse(search[0] === '?' ? search.slice(1) : search);
const updateUrl = (updatedParams: any) => {
const updatedSearch = qs.stringify({ ...currentParams, ...updatedParams });
history.push({
pathname,
search: updatedSearch,
});
return `${pathname}?${updatedSearch}`;
};
return [getSupportedUrlParams(currentParams), updateUrl];
};

View file

@ -9,7 +9,6 @@ import { unmountComponentAtNode } from 'react-dom';
import chrome from 'ui/chrome';
import { PLUGIN } from '../../../../common/constants';
import { UMBreadcrumb } from '../../../breadcrumbs';
import { UptimePersistedState } from '../../../uptime_app';
import { BootstrapUptimeApp, UMFrameworkAdapter } from '../../lib';
import { CreateGraphQLClient } from './framework_adapter_types';
import { renderUptimeKibanaGlobalHelp } from './kibana_global_help';
@ -18,25 +17,11 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
private uiRoutes: any;
private xsrfHeader: string;
private uriPath: string;
private defaultDateRangeStart: string;
private defaultDateRangeEnd: string;
private defaultAutorefreshInterval: number;
private defaultAutorefreshIsPaused: boolean;
constructor(
uiRoutes: any,
dateRangeStart?: string,
dateRangeEnd?: string,
autorefreshInterval?: number,
autorefreshIsPaused?: boolean
) {
constructor(uiRoutes: any) {
this.uiRoutes = uiRoutes;
this.xsrfHeader = chrome.getXsrfToken();
this.uriPath = `${chrome.getBasePath()}/api/uptime/graphql`;
this.defaultDateRangeStart = dateRangeStart || 'now-15m';
this.defaultDateRangeEnd = dateRangeEnd || 'now';
this.defaultAutorefreshInterval = autorefreshInterval || 60 * 1000;
this.defaultAutorefreshIsPaused = autorefreshIsPaused || true;
}
/**
@ -88,9 +73,6 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
// determine whether dark mode is enabled
const darkMode = config.get('theme:darkMode', false) || false;
// get current persisted state, if any
const persistedState = this.initializePersistedState();
/**
* We pass this global help setup as a prop to the app, because for
* localization it's necessary to have the provider mounted before
@ -103,13 +85,6 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
return () => ReactDOM.unmountComponentAtNode(element);
});
const {
autorefreshIsPaused,
autorefreshInterval,
dateRangeStart,
dateRangeEnd,
} = persistedState;
ReactDOM.render(
renderComponent({
basePath,
@ -118,11 +93,6 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
kibanaBreadcrumbs,
routerBasename,
client: graphQLClient,
initialAutorefreshIsPaused: autorefreshIsPaused,
initialAutorefreshInterval: autorefreshInterval,
initialDateRangeStart: dateRangeStart,
initialDateRangeEnd: dateRangeEnd,
persistState: this.updatePersistedState,
renderGlobalHelpControls,
}),
elem
@ -152,41 +122,4 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
unmountComponentAtNode(elem);
});
};
private initializePersistedState = (): UptimePersistedState => {
const uptimeConfigurationData = window.localStorage.getItem(PLUGIN.LOCAL_STORAGE_KEY);
const defaultState: UptimePersistedState = {
autorefreshIsPaused: this.defaultAutorefreshIsPaused,
autorefreshInterval: this.defaultAutorefreshInterval,
dateRangeStart: this.defaultDateRangeStart,
dateRangeEnd: this.defaultDateRangeEnd,
};
try {
if (uptimeConfigurationData) {
const parsed = JSON.parse(uptimeConfigurationData) || {};
const { dateRangeStart, dateRangeEnd } = parsed;
// TODO: this is defensive code to ensure we don't encounter problems
// when encountering older versions of the localStorage values.
// The old code has never been released, so users don't need it, and this
// code should be removed eventually.
if (
(dateRangeEnd && typeof dateRangeEnd === 'number') ||
(dateRangeStart && typeof dateRangeStart === 'number')
) {
this.updatePersistedState(defaultState);
return defaultState;
}
return parsed;
}
} catch (e) {
// TODO: this should result in a redirect to error page
throw e;
}
this.updatePersistedState(defaultState);
return defaultState;
};
private updatePersistedState = (state: UptimePersistedState) => {
window.localStorage.setItem(PLUGIN.LOCAL_STORAGE_KEY, JSON.stringify(state));
};
}

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;
* you may not use this file except in compliance with the Elastic License.
*/
import { stringifyUrlParams } from '../stringify_url_params';
describe('stringifyUrlParams', () => {
it('creates expected string value', () => {
const result = stringifyUrlParams({
autorefreshInterval: 50000,
autorefreshIsPaused: false,
dateRangeStart: 'now-15m',
dateRangeEnd: 'now',
search: 'monitor.id: foo',
selectedPingStatus: 'down',
});
expect(result).toEqual(
'?autorefreshInterval=50000&autorefreshIsPaused=false&dateRangeStart=now-15m&dateRangeEnd=now&search=monitor.id%3A%20foo&selectedPingStatus=down'
);
});
});

View file

@ -0,0 +1,10 @@
/*
* 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 qs from 'querystring';
import { UptimeUrlParams } from './url_params/get_supported_url_params';
export const stringifyUrlParams = (params: UptimeUrlParams) => `?${qs.stringify(params)}`;

View file

@ -0,0 +1,50 @@
/*
* 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 { getSupportedUrlParams } from '../get_supported_url_params';
import { CLIENT_DEFAULTS } from '../../../../../common/constants';
describe('getSupportedUrlParams', () => {
it('returns custom values', () => {
const customValues = {
autorefreshInterval: '23',
autorefreshIsPaused: 'false',
dateRangeStart: 'foo',
dateRangeEnd: 'bar',
search: 'monitor.status: down',
selectedPingStatus: 'up',
};
const result = getSupportedUrlParams(customValues);
expect(result).toEqual({
autorefreshInterval: 23,
autorefreshIsPaused: false,
dateRangeStart: 'foo',
dateRangeEnd: 'bar',
search: 'monitor.status: down',
selectedPingStatus: 'up',
});
});
it('returns default values', () => {
const {
AUTOREFRESH_INTERVAL,
AUTOREFRESH_IS_PAUSED,
DATE_RANGE_START,
DATE_RANGE_END,
SEARCH,
SELECTED_PING_LIST_STATUS,
} = CLIENT_DEFAULTS;
const result = getSupportedUrlParams({});
expect(result).toEqual({
autorefreshInterval: AUTOREFRESH_INTERVAL,
autorefreshIsPaused: AUTOREFRESH_IS_PAUSED,
dateRangeStart: DATE_RANGE_START,
dateRangeEnd: DATE_RANGE_END,
search: SEARCH,
selectedPingStatus: SELECTED_PING_LIST_STATUS,
});
});
});

View file

@ -0,0 +1,24 @@
/*
* 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 { parseAutorefreshInterval } from '../parse_autorefresh_interval';
describe('parseAutorefreshInterval', () => {
it('parses a number', () => {
const result = parseAutorefreshInterval('23', 50);
expect(result).toBe(23);
});
it('returns default value for empty string', () => {
const result = parseAutorefreshInterval('', 50);
expect(result).toBe(50);
});
it('returns default value for non-numeric string', () => {
const result = parseAutorefreshInterval('abc', 50);
expect(result).toBe(50);
});
});

View file

@ -0,0 +1,21 @@
/*
* 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 { parseIsPaused } from '../parse_is_paused';
describe('parseIsPaused', () => {
it('parses correct true isPaused value', () => {
expect(parseIsPaused('true', false)).toEqual(true);
});
it('parses correct false isPaused value', () => {
expect(parseIsPaused('false', true)).toEqual(false);
});
it('uses default value for non-boolean string', () => {
expect(parseIsPaused('foo', true)).toEqual(true);
});
});

View file

@ -0,0 +1,50 @@
/*
* 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 { parseIsPaused } from './parse_is_paused';
import { parseAutorefreshInterval } from './parse_autorefresh_interval';
import { CLIENT_DEFAULTS } from '../../../../common/constants';
export interface UptimeUrlParams {
autorefreshInterval: number;
autorefreshIsPaused: boolean;
dateRangeStart: string;
dateRangeEnd: string;
search: string;
selectedPingStatus: string;
}
const {
AUTOREFRESH_INTERVAL,
AUTOREFRESH_IS_PAUSED,
DATE_RANGE_START,
DATE_RANGE_END,
SEARCH,
SELECTED_PING_LIST_STATUS,
} = CLIENT_DEFAULTS;
export const getSupportedUrlParams = (params: {
[key: string]: string | undefined;
}): UptimeUrlParams => {
const {
autorefreshInterval,
autorefreshIsPaused,
dateRangeStart,
dateRangeEnd,
search,
selectedPingStatus,
} = params;
return {
autorefreshInterval: parseAutorefreshInterval(autorefreshInterval, AUTOREFRESH_INTERVAL),
autorefreshIsPaused: parseIsPaused(autorefreshIsPaused, AUTOREFRESH_IS_PAUSED),
dateRangeStart: dateRangeStart || DATE_RANGE_START,
dateRangeEnd: dateRangeEnd || DATE_RANGE_END,
search: search || SEARCH,
selectedPingStatus:
selectedPingStatus === undefined ? SELECTED_PING_LIST_STATUS : selectedPingStatus,
};
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { getSupportedUrlParams } from './get_supported_url_params';

View file

@ -0,0 +1,14 @@
/*
* 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.
*/
// TODO: add a comment explaining the purpose of this function
export const parseAutorefreshInterval = (
value: string | undefined,
defaultValue: number
): number => {
const parsed = parseInt(value || '', 10);
return isNaN(parsed) ? defaultValue : parsed;
};

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
// TODO: add a comment explaining the purpose of this function
export const parseIsPaused = (value: string | undefined, defaultValue: boolean): boolean => {
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
return defaultValue;
};

View file

@ -22,10 +22,12 @@ import {
} from '../components/functional';
import { UMUpdateBreadcrumbs } from '../lib/lib';
import { UptimeSettingsContext } from '../contexts';
import { useUrlParams } from '../hooks';
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
interface MonitorPageProps {
history: { push: any };
location: { pathname: string };
location: { pathname: string; search: string };
match: { params: { id: string } };
// this is the query function provided by Apollo's Client API
query: <T, TVariables = OperationVariables>(
@ -34,33 +36,36 @@ interface MonitorPageProps {
setBreadcrumbs: UMUpdateBreadcrumbs;
}
export const MonitorPage = ({ location, query, setBreadcrumbs }: MonitorPageProps) => {
export const MonitorPage = ({ history, location, query, setBreadcrumbs }: MonitorPageProps) => {
const [monitorId] = useState<string>(location.pathname.replace(/^(\/monitor\/)/, ''));
const [selectedStatus, setSelectedStatus] = useState<string | null>('down');
const { colors, dateRangeStart, dateRangeEnd, refreshApp, setHeadingText } = useContext(
UptimeSettingsContext
);
useEffect(() => {
query({
query: gql`
query MonitorPageTitle($monitorId: String!) {
monitorPageTitle: getMonitorPageTitle(monitorId: $monitorId) {
id
url
name
const { colors, refreshApp, setHeadingText } = useContext(UptimeSettingsContext);
const [params, updateUrlParams] = useUrlParams(history, location);
const { dateRangeStart, dateRangeEnd, selectedPingStatus } = params;
useEffect(
() => {
query({
query: gql`
query MonitorPageTitle($monitorId: String!) {
monitorPageTitle: getMonitorPageTitle(monitorId: $monitorId) {
id
url
name
}
}
`,
variables: { monitorId },
}).then((result: any) => {
const { name, url, id } = result.data.monitorPageTitle;
const heading: string = name || url || id;
setBreadcrumbs(getMonitorPageBreadcrumb(heading, stringifyUrlParams(params)));
if (setHeadingText) {
setHeadingText(heading);
}
`,
variables: { monitorId },
}).then((result: any) => {
const { name, url, id } = result.data.monitorPageTitle;
const heading: string = name || url || id;
setBreadcrumbs(getMonitorPageBreadcrumb(heading));
if (setHeadingText) {
setHeadingText(heading);
}
});
}, []);
});
},
[params]
);
return (
<Fragment>
<MonitorPageTitle monitorId={monitorId} variables={{ monitorId }} />
@ -73,13 +78,16 @@ export const MonitorPage = ({ location, query, setBreadcrumbs }: MonitorPageProp
<MonitorCharts {...colors} variables={{ dateRangeStart, dateRangeEnd, monitorId }} />
<EuiSpacer size="s" />
<PingList
onSelectedStatusUpdate={setSelectedStatus}
onSelectedStatusUpdate={(selectedStatus: string | null) =>
updateUrlParams({ selectedPingStatus: selectedStatus || '' })
}
onUpdateApp={refreshApp}
selectedOption={selectedPingStatus}
variables={{
dateRangeStart,
dateRangeEnd,
monitorId,
status: selectedStatus,
status: selectedPingStatus,
}}
/>
</Fragment>

View file

@ -7,14 +7,21 @@
// @ts-ignore EuiSearchBar missing
import { EuiSearchBar, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { Fragment, useContext, useEffect, useState } from 'react';
import React, { Fragment, useContext, useEffect } from 'react';
import { getOverviewPageBreadcrumbs } from '../breadcrumbs';
import { EmptyState, ErrorList, FilterBar, MonitorList, Snapshot } from '../components/functional';
import { UMUpdateBreadcrumbs } from '../lib/lib';
import { UptimeSettingsContext } from '../contexts';
import { useUrlParams } from '../hooks';
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
interface OverviewPageProps {
basePath: string;
history: any;
location: {
pathname: string;
search: string;
};
setBreadcrumbs: UMUpdateBreadcrumbs;
}
@ -22,12 +29,10 @@ type Props = OverviewPageProps;
export type UptimeSearchBarQueryChangeHandler = ({ query }: { query?: { text: string } }) => void;
export const OverviewPage = ({ basePath, setBreadcrumbs }: Props) => {
const { colors, dateRangeStart, dateRangeEnd, refreshApp, setHeadingText } = useContext(
UptimeSettingsContext
);
const [currentFilterQueryObj, setFilterQueryObj] = useState<object | undefined>(undefined);
const [currentFilterQuery, setCurrentFilterQuery] = useState<string | undefined>(undefined);
export const OverviewPage = ({ basePath, setBreadcrumbs, history, location }: Props) => {
const { colors, refreshApp, setHeadingText } = useContext(UptimeSettingsContext);
const [params, updateUrl] = useUrlParams(history, location);
const { dateRangeStart, dateRangeEnd, search } = params;
useEffect(() => {
setBreadcrumbs(getOverviewPageBreadcrumbs());
@ -41,39 +46,46 @@ export const OverviewPage = ({ basePath, setBreadcrumbs }: Props) => {
}
}, []);
const sharedProps = { dateRangeStart, dateRangeEnd, currentFilterQuery };
const filterQueryString = search || '';
const sharedProps = {
dateRangeStart,
dateRangeEnd,
filters: search ? JSON.stringify(EuiSearchBar.Query.toESQuery(filterQueryString)) : undefined,
};
const updateQuery: UptimeSearchBarQueryChangeHandler = ({ query }) => {
try {
let esQuery;
if (query && query.text) {
esQuery = EuiSearchBar.Query.toESQuery(query);
if (query && typeof query.text !== 'undefined') {
updateUrl({ search: query.text });
}
setFilterQueryObj(query);
setCurrentFilterQuery(esQuery ? JSON.stringify(esQuery) : esQuery);
if (refreshApp) {
refreshApp();
}
} catch (e) {
setFilterQueryObj(undefined);
setCurrentFilterQuery(undefined);
updateUrl({ search: '' });
}
};
const linkParameters = stringifyUrlParams(params);
return (
<Fragment>
<EmptyState basePath={basePath} implementsCustomErrorState={true} variables={sharedProps}>
<FilterBar
currentQuery={currentFilterQueryObj}
currentQuery={filterQueryString}
updateQuery={updateQuery}
variables={sharedProps}
/>
<EuiSpacer size="s" />
<Snapshot colors={colors} variables={sharedProps} />
<EuiSpacer size="s" />
<MonitorList dangerColor={colors.danger} variables={sharedProps} />
<MonitorList
dangerColor={colors.danger}
linkParameters={linkParameters}
variables={sharedProps}
/>
<EuiSpacer size="s" />
<ErrorList variables={sharedProps} />
<ErrorList linkParameters={linkParameters} variables={sharedProps} />
</EmptyState>
</Fragment>
);

View file

@ -24,12 +24,14 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import React, { useEffect, useState } from 'react';
import { ApolloProvider } from 'react-apollo';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { I18nContext } from 'ui/i18n';
import { overviewBreadcrumb, UMBreadcrumb } from './breadcrumbs';
import { UMBreadcrumb } from './breadcrumbs';
import { UMGraphQLClient, UMUpdateBreadcrumbs } from './lib/lib';
import { MonitorPage, OverviewPage } from './pages';
import { UptimeRefreshContext, UptimeSettingsContext } from './contexts';
import { UptimeDatePicker } from './components/functional/uptime_date_picker';
import { useUrlParams } from './hooks';
export interface UptimeAppColors {
danger: string;
@ -38,49 +40,21 @@ export interface UptimeAppColors {
mean: string;
}
export interface UptimePersistedState {
autorefreshIsPaused: boolean;
autorefreshInterval: number;
dateRangeStart: string;
dateRangeEnd: string;
}
export interface UptimeAppProps {
basePath: string;
darkMode: boolean;
client: UMGraphQLClient;
initialDateRangeStart: string;
initialDateRangeEnd: string;
initialAutorefreshInterval: number;
initialAutorefreshIsPaused: boolean;
kibanaBreadcrumbs: UMBreadcrumb[];
routerBasename: string;
setBreadcrumbs: UMUpdateBreadcrumbs;
persistState(state: UptimePersistedState): void;
renderGlobalHelpControls(): void;
}
// TODO: when EUI exports types for this, this should be replaced
interface SuperDateRangePickerRangeChangedEvent {
start: string;
end: string;
}
interface SuperDateRangePickerRefreshChangedEvent {
isPaused: boolean;
refreshInterval?: number;
}
const Application = (props: UptimeAppProps) => {
const {
basePath,
client,
darkMode,
initialAutorefreshIsPaused,
initialAutorefreshInterval,
initialDateRangeStart,
initialDateRangeEnd,
persistState,
renderGlobalHelpControls,
routerBasename,
setBreadcrumbs,
@ -103,19 +77,10 @@ const Application = (props: UptimeAppProps) => {
};
}
const [autorefreshIsPaused, setAutorefreshIsPaused] = useState<boolean>(
initialAutorefreshIsPaused
);
const [autorefreshInterval, setAutorefreshInterval] = useState<number>(
initialAutorefreshInterval
);
const [dateRangeStart, setDateRangeStart] = useState<string>(initialDateRangeStart);
const [dateRangeEnd, setDateRangeEnd] = useState<string>(initialDateRangeEnd);
const [lastRefresh, setLastRefresh] = useState<number>(Date.now());
const [headingText, setHeadingText] = useState<string | undefined>(undefined);
useEffect(() => {
setBreadcrumbs([overviewBreadcrumb]);
renderGlobalHelpControls();
}, []);
@ -126,97 +91,75 @@ const Application = (props: UptimeAppProps) => {
return (
<I18nContext>
<Router basename={routerBasename}>
<ApolloProvider client={client}>
<UptimeSettingsContext.Provider
value={{
autorefreshInterval,
autorefreshIsPaused,
basePath,
dateRangeStart,
dateRangeEnd,
colors,
refreshApp,
setHeadingText,
}}
>
<UptimeRefreshContext.Provider value={{ lastRefresh }}>
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
<div>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>{headingText}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{
// @ts-ignore onRefresh is not defined on EuiSuperDatePicker's type yet
<EuiSuperDatePicker
start={dateRangeStart}
end={dateRangeEnd}
isPaused={autorefreshIsPaused}
refreshInterval={autorefreshInterval}
onTimeChange={({ start, end }: SuperDateRangePickerRangeChangedEvent) => {
setDateRangeStart(start);
setDateRangeEnd(end);
persistState({
autorefreshInterval,
autorefreshIsPaused,
dateRangeStart,
dateRangeEnd,
});
refreshApp();
}}
// @ts-ignore onRefresh is not defined on EuiSuperDatePicker's type yet
onRefresh={refreshApp}
onRefreshChange={({
isPaused,
refreshInterval,
}: SuperDateRangePickerRefreshChangedEvent) => {
setAutorefreshInterval(
refreshInterval === undefined ? autorefreshInterval : refreshInterval
);
setAutorefreshIsPaused(isPaused);
persistState({
autorefreshInterval,
autorefreshIsPaused,
dateRangeStart,
dateRangeEnd,
});
}}
/>
}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<Switch>
<Route
exact
path="/"
render={routerProps => (
<OverviewPage
basePath={basePath}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
/>
)}
/>
<Route
path="/monitor/:id"
render={routerProps => (
<MonitorPage
query={client.query}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
/>
)}
/>
</Switch>
</div>
</EuiPage>
</UptimeRefreshContext.Provider>
</UptimeSettingsContext.Provider>
</ApolloProvider>
<Route
path="/"
render={(rootRouteProps: RouteComponentProps) => {
const [
{ autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd },
] = useUrlParams(rootRouteProps.history, rootRouteProps.location);
return (
<ApolloProvider client={client}>
<UptimeSettingsContext.Provider
value={{
autorefreshInterval,
autorefreshIsPaused,
basePath,
dateRangeStart,
dateRangeEnd,
colors,
refreshApp,
setHeadingText,
}}
>
<UptimeRefreshContext.Provider value={{ lastRefresh }}>
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
<div>
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
gutterSize="s"
>
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>{headingText}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UptimeDatePicker refreshApp={refreshApp} {...rootRouteProps} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<Switch>
<Route
exact
path="/"
render={routerProps => (
<OverviewPage
basePath={basePath}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
/>
)}
/>
<Route
path="/monitor/:id"
render={routerProps => (
<MonitorPage
query={client.query}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
/>
)}
/>
</Switch>
</div>
</EuiPage>
</UptimeRefreshContext.Provider>
</UptimeSettingsContext.Provider>
</ApolloProvider>
);
}}
/>
</Router>
</I18nContext>
);

View file

@ -11,12 +11,22 @@ export default ({ getPageObjects }: KibanaFunctionalTestDefaultProviders) => {
// TODO: add UI functional tests
const pageObjects = getPageObjects(['uptime']);
describe('overview page', () => {
const DEFAULT_DATE_START = '2019-01-28 12:40:08.078';
const DEFAULT_DATE_END = '2019-01-29 12:40:08.078';
it('loads and displays uptime data based on date range', async () => {
await pageObjects.uptime.goToUptimeOverviewAndLoadData(
'2019-01-28 12:40:08.078',
'2019-01-29 12:40:08.078',
DEFAULT_DATE_START,
DEFAULT_DATE_END,
'monitor-page-link-auto-http-0X131221E73F825974'
);
});
it('runs filter query without issues', async () => {
await pageObjects.uptime.inputFilterQuery(
DEFAULT_DATE_START,
DEFAULT_DATE_END,
'monitor.status:up monitor.id:auto-http-0X131221E73F825974'
);
});
});
};

View file

@ -37,5 +37,16 @@ export const UptimePageProvider = ({
throw new Error('Expected monitor name not found');
}
}
public async inputFilterQuery(
datePickerStartValue: string,
datePickerEndValue: string,
filterQuery: string
) {
await pageObjects.common.navigateToApp('uptime');
await pageObjects.timePicker.setAbsoluteRange(datePickerStartValue, datePickerEndValue);
await uptimeService.setFilterText(filterQuery);
await uptimeService.monitorIdExists('monitor-page-link-auto-http-0X131221E73F825974');
}
}();
};

View file

@ -8,6 +8,7 @@ import { KibanaFunctionalTestDefaultProviders } from '../../types/providers';
export const UptimeProvider = ({ getService }: KibanaFunctionalTestDefaultProviders) => {
const testSubjects = getService('testSubjects');
const browser = getService('browser');
return {
async assertExists(key: string) {
if (!(await testSubjects.exists(key))) {
@ -23,5 +24,10 @@ export const UptimeProvider = ({ getService }: KibanaFunctionalTestDefaultProvid
async getMonitorNameDisplayedOnPageTitle() {
return await testSubjects.getVisibleText('monitor-page-title');
},
async setFilterText(filterQuery: string) {
await testSubjects.click('xpack.uptime.filterBar');
await testSubjects.setValue('xpack.uptime.filterBar', filterQuery);
await browser.pressKeys(browser.keys.ENTER);
},
};
};