[Uptime] Added test now mode for monitors (#123712)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2022-02-03 21:00:31 +01:00 committed by GitHub
parent 52f0ea20fe
commit 7369e2f48d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 895 additions and 81 deletions

View file

@ -41,4 +41,5 @@ export enum API_URLS {
SERVICE_LOCATIONS = '/internal/uptime/service/locations',
SYNTHETICS_MONITORS = '/internal/uptime/service/monitors',
RUN_ONCE_MONITOR = '/internal/uptime/service/monitors/run_once',
TRIGGER_MONITOR = '/internal/uptime/service/monitors/trigger',
}

View file

@ -27,6 +27,7 @@ export const StateType = t.intersection([
monitor: t.intersection([
t.partial({
name: t.string,
duration: t.type({ us: t.number }),
}),
t.type({
type: t.string,
@ -73,6 +74,7 @@ export const MonitorSummaryType = t.intersection([
t.partial({
histogram: HistogramType,
minInterval: t.number,
configId: t.string,
}),
]);

View file

@ -220,6 +220,7 @@ export const PingType = t.intersection([
service: t.partial({
name: t.string,
}),
config_id: t.string,
}),
]);

View file

@ -20,7 +20,7 @@ interface Props {
export const BrowserTestRunResult = ({ monitorId }: Props) => {
const { data, loading, stepEnds, journeyStarted, summaryDoc, stepListData } =
useBrowserRunOnceMonitors({
monitorId,
configId: monitorId,
});
const hits = data?.hits.hits;

View file

@ -26,7 +26,7 @@ describe('useBrowserRunOnceMonitors', function () {
},
});
const { result } = renderHook(() => useBrowserRunOnceMonitors({ monitorId: 'test-id' }), {
const { result } = renderHook(() => useBrowserRunOnceMonitors({ configId: 'test-id' }), {
wrapper: WrappedHelper,
});

View file

@ -15,10 +15,12 @@ import { fetchJourneySteps } from '../../../../state/api/journey';
import { isStepEnd } from '../../../synthetics/check_steps/steps_list';
export const useBrowserEsResults = ({
monitorId,
configId,
testRunId,
lastRefresh,
}: {
monitorId: string;
configId: string;
testRunId?: string;
lastRefresh: number;
}) => {
const { settings } = useSelector(selectDynamicSettings);
@ -37,7 +39,7 @@ export const useBrowserEsResults = ({
filter: [
{
term: {
config_id: monitorId,
config_id: configId,
},
},
{
@ -45,28 +47,47 @@ export const useBrowserEsResults = ({
'synthetics.type': ['heartbeat/summary', 'journey/start'],
},
},
...(testRunId
? [
{
term: {
test_run_id: testRunId,
},
},
]
: []),
],
},
},
},
size: 10,
}),
[monitorId, settings?.heartbeatIndices, lastRefresh],
[configId, settings?.heartbeatIndices, lastRefresh],
{ name: 'TestRunData' }
);
};
export const useBrowserRunOnceMonitors = ({ monitorId }: { monitorId: string }) => {
const { refreshTimer, lastRefresh } = useTickTick();
export const useBrowserRunOnceMonitors = ({
configId,
testRunId,
skipDetails = false,
refresh = true,
}: {
configId: string;
testRunId?: string;
refresh?: boolean;
skipDetails?: boolean;
}) => {
const { refreshTimer, lastRefresh } = useTickTick(3 * 1000, refresh);
const [checkGroupId, setCheckGroupId] = useState('');
const [stepEnds, setStepEnds] = useState<JourneyStep[]>([]);
const [summary, setSummary] = useState<JourneyStep>();
const { data, loading } = useBrowserEsResults({ monitorId, lastRefresh });
const { data, loading } = useBrowserEsResults({ configId, testRunId, lastRefresh });
const { data: stepListData } = useFetcher(() => {
if (checkGroupId) {
if (checkGroupId && !skipDetails) {
return fetchJourneySteps({
checkGroup: checkGroupId,
});

View file

@ -15,7 +15,7 @@ interface Props {
}
export function SimpleTestResults({ monitorId }: Props) {
const [summaryDocs, setSummaryDocs] = useState<Ping[]>([]);
const { summaryDoc, loading } = useSimpleRunOnceMonitors({ monitorId });
const { summaryDoc, loading } = useSimpleRunOnceMonitors({ configId: monitorId });
useEffect(() => {
if (summaryDoc) {

View file

@ -12,8 +12,14 @@ import { Ping } from '../../../../../common/runtime_types';
import { createEsParams, useEsSearch } from '../../../../../../observability/public';
import { useTickTick } from '../use_tick_tick';
export const useSimpleRunOnceMonitors = ({ monitorId }: { monitorId: string }) => {
const { refreshTimer, lastRefresh } = useTickTick();
export const useSimpleRunOnceMonitors = ({
configId,
testRunId,
}: {
configId: string;
testRunId?: string;
}) => {
const { refreshTimer, lastRefresh } = useTickTick(2 * 1000, false);
const { settings } = useSelector(selectDynamicSettings);
@ -31,7 +37,7 @@ export const useSimpleRunOnceMonitors = ({ monitorId }: { monitorId: string }) =
filter: [
{
term: {
config_id: monitorId,
config_id: configId,
},
},
{
@ -39,13 +45,22 @@ export const useSimpleRunOnceMonitors = ({ monitorId }: { monitorId: string }) =
field: 'summary',
},
},
...(testRunId
? [
{
term: {
test_run_id: testRunId,
},
},
]
: []),
],
},
},
},
size: 10,
}),
[monitorId, settings?.heartbeatIndices, lastRefresh],
[configId, settings?.heartbeatIndices, lastRefresh],
{ name: 'TestRunData' }
);

View file

@ -63,7 +63,7 @@ export function TestResultHeader({ doc, title, summaryDocs, journeyStarted, isCo
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiBadge style={{ width: 100 }} color={journeyStarted ? 'primary' : 'warning'}>
{journeyStarted ? IN_PROGRESS : PENDING_LABEL}
{journeyStarted ? IN_PROGRESS_LABEL : PENDING_LABEL}
</EuiBadge>
</EuiFlexItem>
<EuiFlexItem>
@ -86,7 +86,7 @@ export function TestResultHeader({ doc, title, summaryDocs, journeyStarted, isCo
);
}
const PENDING_LABEL = i18n.translate('xpack.uptime.monitorManagement.pending', {
export const PENDING_LABEL = i18n.translate('xpack.uptime.monitorManagement.pending', {
defaultMessage: 'PENDING',
});
@ -98,7 +98,7 @@ const COMPLETED_LABEL = i18n.translate('xpack.uptime.monitorManagement.completed
defaultMessage: 'COMPLETED',
});
const IN_PROGRESS = i18n.translate('xpack.uptime.monitorManagement.inProgress', {
export const IN_PROGRESS_LABEL = i18n.translate('xpack.uptime.monitorManagement.inProgress', {
defaultMessage: 'IN PROGRESS',
});

View file

@ -8,13 +8,18 @@
import { useEffect, useState, useContext } from 'react';
import { UptimeRefreshContext } from '../../../contexts';
export function useTickTick() {
const { refreshApp, lastRefresh } = useContext(UptimeRefreshContext);
export function useTickTick(interval?: number, refresh = true) {
const { refreshApp } = useContext(UptimeRefreshContext);
const [nextTick, setNextTick] = useState(Date.now());
const [tickTick] = useState<NodeJS.Timer>(() =>
setInterval(() => {
refreshApp();
}, 5 * 1000)
if (refresh) {
refreshApp();
}
setNextTick(Date.now());
}, interval ?? 5 * 1000)
);
useEffect(() => {
@ -23,5 +28,5 @@ export function useTickTick() {
};
}, [tickTick]);
return { refreshTimer: tickTick, lastRefresh };
return { refreshTimer: tickTick, lastRefresh: nextTick };
}

View file

@ -229,7 +229,12 @@ describe('MonitorListStatusColumn', () => {
it('provides expected tooltip and display times', async () => {
const { getByText } = render(
<EuiThemeProvider darkMode={false}>
<MonitorListStatusColumn status="up" timestamp="2314123" summaryPings={[]} />
<MonitorListStatusColumn
status="up"
timestamp="2314123"
summaryPings={[]}
monitorType="http"
/>
</EuiThemeProvider>
);
@ -244,7 +249,12 @@ describe('MonitorListStatusColumn', () => {
it('can handle a non-numeric timestamp value', () => {
const { getByText } = render(
<EuiThemeProvider darkMode={false}>
<MonitorListStatusColumn status="up" timestamp={new Date().toString()} summaryPings={[]} />
<MonitorListStatusColumn
status="up"
timestamp={new Date().toString()}
summaryPings={[]}
monitorType="http"
/>
</EuiThemeProvider>
);
@ -259,6 +269,7 @@ describe('MonitorListStatusColumn', () => {
<MonitorListStatusColumn
status="up"
timestamp={new Date().toString()}
monitorType="http"
summaryPings={summaryPings.filter((ping) => ping.observer!.geo!.name! === 'Islamabad')}
/>
</EuiThemeProvider>
@ -278,6 +289,7 @@ describe('MonitorListStatusColumn', () => {
status="up"
timestamp={new Date().toString()}
summaryPings={summaryPings}
monitorType="http"
/>
</EuiThemeProvider>
);
@ -295,6 +307,7 @@ describe('MonitorListStatusColumn', () => {
<EuiThemeProvider darkMode={false}>
<MonitorListStatusColumn
status="up"
monitorType="http"
timestamp={new Date().toString()}
summaryPings={summaryPings}
/>

View file

@ -5,13 +5,23 @@
* 2.0.
*/
import React, { useContext } from 'react';
import moment from 'moment';
import React, { useCallback, useContext } from 'react';
import moment, { Moment } from 'moment';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip, EuiBadge, EuiSpacer } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiToolTip,
EuiBadge,
EuiSpacer,
EuiHighlight,
EuiHorizontalRule,
} from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
import { parseTimestamp } from '../parse_timestamp';
import { Ping } from '../../../../../common/runtime_types';
import { DataStream, Ping } from '../../../../../common/runtime_types';
import {
STATUS,
SHORT_TIMESPAN_LOCALE,
@ -22,10 +32,18 @@ import {
import { UptimeThemeContext } from '../../../../contexts';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../../common/translations';
import { MonitorProgress } from './progress/monitor_progress';
import { refreshedMonitorSelector } from '../../../../state/reducers/monitor_list';
import { testNowRunSelector } from '../../../../state/reducers/test_now_runs';
import { clearTestNowMonitorAction } from '../../../../state/actions';
interface MonitorListStatusColumnProps {
configId?: string;
monitorId?: string;
status: string;
monitorType: string;
timestamp: string;
duration?: number;
summaryPings: Ping[];
}
@ -63,7 +81,7 @@ export const getShortTimeStamp = (timeStamp: moment.Moment, relative = false) =>
shortTimestamp = timeStamp.fromNow();
}
// Reset it so, it does't impact other part of the app
// Reset it so, it doesn't impact other part of the app
moment.locale(prevLocale);
return shortTimestamp;
} else {
@ -144,7 +162,11 @@ export const getLocationStatus = (summaryPings: Ping[], status: string) => {
};
export const MonitorListStatusColumn = ({
monitorType,
configId,
monitorId,
status,
duration,
summaryPings = [],
timestamp: tsString,
}: MonitorListStatusColumnProps) => {
@ -156,16 +178,39 @@ export const MonitorListStatusColumn = ({
const { statusMessage, locTooltip } = getLocationStatus(summaryPings, status);
const dispatch = useDispatch();
const stopProgressTrack = useCallback(() => {
if (configId) {
dispatch(clearTestNowMonitorAction(configId));
}
}, [configId, dispatch]);
const refreshedMonitorIds = useSelector(refreshedMonitorSelector);
const testNowRun = useSelector(testNowRunSelector(configId));
return (
<div>
<StatusColumnFlexG alignItems="center" gutterSize="none" wrap={false} responsive={false}>
<StatusColumnFlexG alignItems="center" gutterSize="xs" wrap={false} responsive={false}>
<EuiFlexItem grow={false} style={{ flexBasis: 40 }}>
<EuiBadge
className="eui-textCenter"
color={status === STATUS.UP ? 'success' : dangerBehindText}
>
{getHealthMessage(status)}
</EuiBadge>
{testNowRun && configId && testNowRun?.testRunId ? (
<MonitorProgress
monitorId={monitorId!}
configId={configId}
testRunId={testNowRun?.testRunId}
monitorType={monitorType as DataStream}
duration={duration ?? 0}
stopProgressTrack={stopProgressTrack}
/>
) : (
<EuiBadge
className="eui-textCenter"
color={status === STATUS.UP ? 'success' : dangerBehindText}
>
{getHealthMessage(status)}
</EuiBadge>
)}
</EuiFlexItem>
</StatusColumnFlexG>
<EuiSpacer size="xs" />
@ -183,20 +228,39 @@ export const MonitorListStatusColumn = ({
</EuiToolTip>
<EuiToolTip
content={
<EuiText color="ghost" size="xs">
{timestamp.toLocaleString()}
</EuiText>
<>
<EuiText color="text" size="xs">
<strong> {timestamp.fromNow()}</strong>
</EuiText>
<EuiHorizontalRule margin="xs" />
<EuiText color="ghost" size="xs">
{timestamp.toLocaleString()}
</EuiText>
</>
}
>
<EuiText size="xs" color="subdued" className="eui-textNoWrap">
Checked {getShortTimeStamp(timestamp)}
</EuiText>
{monitorId && refreshedMonitorIds?.includes(monitorId) ? (
<EuiHighlight highlightAll={true} search={getCheckedLabel(timestamp)}>
{getCheckedLabel(timestamp)}
</EuiHighlight>
) : (
<EuiText size="xs" color="subdued" className="eui-textNoWrap">
{getCheckedLabel(timestamp)}
</EuiText>
)}
</EuiToolTip>
</EuiText>
</div>
);
};
const getCheckedLabel = (timestamp: Moment) => {
return i18n.translate('xpack.uptime.monitorList.statusColumn.checkedTimestamp', {
defaultMessage: 'Checked {timestamp}',
values: { timestamp: getShortTimeStamp(timestamp) },
});
};
const PaddedText = euiStyled(EuiText)`
padding-right: ${(props) => props.theme.eui.paddingSizes.xs};
`;

View file

@ -0,0 +1,89 @@
/*
* 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 { EuiBadge, EuiProgress } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { useBrowserRunOnceMonitors } from '../../../../monitor_management/test_now_mode/browser/use_browser_run_once_monitors';
import {
IN_PROGRESS_LABEL,
PENDING_LABEL,
} from '../../../../monitor_management/test_now_mode/test_result_header';
export const BrowserMonitorProgress = ({
configId,
testRunId,
duration,
isUpdating,
updateMonitorStatus,
}: {
configId: string;
testRunId: string;
duration: number;
isUpdating: boolean;
updateMonitorStatus: () => void;
}) => {
const { journeyStarted, summaryDoc, data } = useBrowserRunOnceMonitors({
configId,
testRunId,
refresh: false,
skipDetails: true,
});
const [startTime, setStartTime] = useState(Date.now());
const [passedTime, setPassedTime] = useState(0);
useEffect(() => {
if (summaryDoc) {
updateMonitorStatus();
}
}, [updateMonitorStatus, summaryDoc]);
useEffect(() => {
const interVal = setInterval(() => {
if (journeyStarted) {
setPassedTime((Date.now() - startTime) * 1000);
}
}, 500);
const startTimeValue = startTime;
return () => {
if ((Date.now() - startTimeValue) * 1000 > duration) {
clearInterval(interVal);
}
};
}, [data, duration, journeyStarted, startTime]);
useEffect(() => {
if (journeyStarted) {
setStartTime(Date.now());
}
}, [journeyStarted]);
if (isUpdating || passedTime > duration) {
return (
<>
<EuiBadge>{IN_PROGRESS_LABEL}</EuiBadge>
<EuiProgress size="xs" />
</>
);
}
return (
<span>
{journeyStarted ? (
<>
<EuiBadge>{IN_PROGRESS_LABEL}</EuiBadge>
<EuiProgress value={passedTime} max={duration} size="xs" />
</>
) : (
<>
<EuiBadge>{PENDING_LABEL}</EuiBadge>
<EuiProgress size="xs" />
</>
)}
</span>
);
};

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { SimpleMonitorProgress } from './simple_monitor_progress';
import { BrowserMonitorProgress } from './browser_monitor_progress';
import { DataStream } from '../../../../../../common/runtime_types';
import { useUpdatedMonitor } from './use_updated_monitor';
import { refreshedMonitorSelector } from '../../../../../state/reducers/monitor_list';
export const MonitorProgress = ({
monitorId,
configId,
testRunId,
duration,
monitorType,
stopProgressTrack,
}: {
monitorId: string;
configId: string;
testRunId: string;
duration: number;
monitorType: DataStream;
stopProgressTrack: () => void;
}) => {
const { updateMonitorStatus, isUpdating } = useUpdatedMonitor({
testRunId,
monitorId,
});
const refreshedMonitorId = useSelector(refreshedMonitorSelector);
useEffect(() => {
if (refreshedMonitorId.includes(monitorId)) {
stopProgressTrack();
}
}, [isUpdating, monitorId, refreshedMonitorId, stopProgressTrack]);
return monitorType === 'browser' ? (
<BrowserMonitorProgress
configId={configId}
testRunId={testRunId}
duration={duration}
isUpdating={isUpdating}
updateMonitorStatus={updateMonitorStatus}
/>
) : (
<SimpleMonitorProgress
monitorId={monitorId}
testRunId={testRunId}
duration={duration}
isUpdating={isUpdating}
updateMonitorStatus={updateMonitorStatus}
/>
);
};

View file

@ -0,0 +1,62 @@
/*
* 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 { EuiBadge, EuiProgress } from '@elastic/eui';
import React, { useEffect, useRef, useState } from 'react';
import { useSimpleRunOnceMonitors } from '../../../../monitor_management/test_now_mode/simple/use_simple_run_once_monitors';
import { IN_PROGRESS_LABEL } from '../../../../monitor_management/test_now_mode/test_result_header';
export const SimpleMonitorProgress = ({
monitorId,
testRunId,
duration,
isUpdating,
updateMonitorStatus,
}: {
monitorId: string;
testRunId: string;
duration: number;
isUpdating: boolean;
updateMonitorStatus: () => void;
}) => {
const { summaryDoc, data } = useSimpleRunOnceMonitors({
configId: monitorId,
testRunId,
});
const startTime = useRef(Date.now());
const [passedTime, setPassedTime] = useState(Date.now());
useEffect(() => {
if (summaryDoc) {
updateMonitorStatus();
}
}, [updateMonitorStatus, summaryDoc]);
useEffect(() => {
setPassedTime(Date.now() - startTime.current);
}, [data]);
const passedTimeMicro = passedTime * 1000;
if (isUpdating || passedTimeMicro > duration) {
return (
<>
<EuiBadge>{IN_PROGRESS_LABEL}</EuiBadge>
<EuiProgress size="xs" />
</>
);
}
return (
<span>
<EuiBadge>{IN_PROGRESS_LABEL}</EuiBadge>
<EuiProgress value={passedTimeMicro} max={duration} size="xs" />
</span>
);
};

View file

@ -0,0 +1,44 @@
/*
* 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 { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getUpdatedMonitor, setUpdatingMonitorId } from '../../../../../state/actions';
import { isUpdatingMonitorSelector } from '../../../../../state/reducers/monitor_list';
export const useUpdatedMonitor = ({
testRunId,
monitorId,
}: {
testRunId: string;
monitorId: string;
}) => {
const dispatch = useDispatch();
const isUpdatingMonitors = useSelector(isUpdatingMonitorSelector);
const updateMonitorStatus = useCallback(() => {
if (testRunId) {
dispatch(
getUpdatedMonitor.get({
dateRangeStart: 'now-10m',
dateRangeEnd: 'now',
filters: JSON.stringify({
bool: {
should: [{ match_phrase: { test_run_id: testRunId } }],
minimum_should_match: 1,
},
}),
pageSize: 1,
})
);
dispatch(setUpdatingMonitorId(monitorId));
}
}, [dispatch, monitorId, testRunId]);
return { updateMonitorStatus, isUpdating: isUpdatingMonitors.includes(monitorId) };
};

View file

@ -0,0 +1,60 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { useDispatch, useSelector } from 'react-redux';
import { EuiButtonIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { testNowMonitorAction } from '../../../../state/actions';
import { testNowRunSelector } from '../../../../state/reducers/test_now_runs';
export const TestNowColumn = ({
monitorId,
configId,
}: {
monitorId: string;
configId?: string;
}) => {
const dispatch = useDispatch();
const testNowRun = useSelector(testNowRunSelector(configId));
if (!configId) {
return <>--</>;
}
const testNowClick = () => {
dispatch(testNowMonitorAction.get(configId));
};
if (testNowRun && testNowRun.status === 'loading') {
return <EuiLoadingSpinner size="s" />;
}
return (
<EuiToolTip content={testNowRun ? TEST_SCHEDULED_LABEL : TEST_NOW_LABEL}>
<EuiButtonIcon
iconType="play"
onClick={() => testNowClick()}
isDisabled={Boolean(testNowRun)}
aria-label={TEST_NOW_ARIA_LABEL}
/>
</EuiToolTip>
);
};
export const TEST_NOW_ARIA_LABEL = i18n.translate('xpack.uptime.monitorList.testNow.AriaLabel', {
defaultMessage: 'CLick to run test now',
});
export const TEST_NOW_LABEL = i18n.translate('xpack.uptime.monitorList.testNow.label', {
defaultMessage: 'Test now',
});
export const TEST_SCHEDULED_LABEL = i18n.translate('xpack.uptime.monitorList.testNow.scheduled', {
defaultMessage: 'Test is already scheduled',
});

View file

@ -16,7 +16,6 @@ import {
MonitorSummary,
} from '../../../../common/runtime_types';
import { MonitorListComponent, noItemsMessage } from './monitor_list';
import * as redux from 'react-redux';
import moment from 'moment';
import { IHttpFetchError, ResponseErrorBody } from '../../../../../../../src/core/public';
import { mockMoment } from '../../../lib/helper/test_helpers';
@ -56,7 +55,7 @@ const testFooPings: Ping[] = [
const testFooSummary: MonitorSummary = {
monitor_id: 'foo',
state: {
monitor: { type: 'http' },
monitor: { type: 'http', duration: { us: 1000 } },
summaryPings: testFooPings,
summary: {
up: 1,
@ -91,7 +90,7 @@ const testBarPings: Ping[] = [
const testBarSummary: MonitorSummary = {
monitor_id: 'bar',
state: {
monitor: { type: 'http' },
monitor: { type: 'http', duration: { us: 1000 } },
summaryPings: testBarPings,
summary: {
up: 2,
@ -129,12 +128,6 @@ describe('MonitorList component', () => {
};
beforeEach(() => {
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
useDispatchSpy.mockReturnValue(jest.fn());
const useSelectorSpy = jest.spyOn(redux, 'useSelector');
useSelectorSpy.mockReturnValue(true);
localStorageMock = {
getItem: jest.fn().mockImplementation(() => '25'),
setItem: jest.fn(),
@ -156,6 +149,7 @@ describe('MonitorList component', () => {
}}
pageSize={10}
setPageSize={jest.fn()}
refreshedMonitorIds={[]}
/>
);
expect(await findByText(NO_DATA_MESSAGE)).toBeInTheDocument();
@ -170,6 +164,7 @@ describe('MonitorList component', () => {
}}
pageSize={10}
setPageSize={jest.fn()}
refreshedMonitorIds={[]}
/>
);
@ -190,6 +185,7 @@ describe('MonitorList component', () => {
}}
pageSize={10}
setPageSize={jest.fn()}
refreshedMonitorIds={[]}
/>
);
@ -226,6 +222,7 @@ describe('MonitorList component', () => {
}}
pageSize={10}
setPageSize={jest.fn()}
refreshedMonitorIds={[]}
/>
);
@ -254,6 +251,7 @@ describe('MonitorList component', () => {
}}
pageSize={10}
setPageSize={jest.fn()}
refreshedMonitorIds={[]}
/>
);
@ -283,6 +281,7 @@ describe('MonitorList component', () => {
}}
pageSize={10}
setPageSize={jest.fn()}
refreshedMonitorIds={[]}
/>
);

View file

@ -32,15 +32,19 @@ import { CertStatusColumn } from './columns/cert_status_column';
import { MonitorListHeader } from './monitor_list_header';
import { TAGS_LABEL, URL_LABEL } from '../../common/translations';
import { EnableMonitorAlert } from './columns/enable_alert';
import { STATUS_ALERT_COLUMN } from './translations';
import { STATUS_ALERT_COLUMN, TEST_NOW_COLUMN } from './translations';
import { MonitorNameColumn } from './columns/monitor_name_col';
import { MonitorTags } from '../../common/monitor_tags';
import { useMonitorHistogram } from './use_monitor_histogram';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { TestNowColumn } from './columns/test_now_col';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
interface Props extends MonitorListProps {
pageSize: number;
setPageSize: (val: number) => void;
monitorList: MonitorList;
refreshedMonitorIds: string[];
}
export const noItemsMessage = (loading: boolean, filters?: string) => {
@ -52,8 +56,15 @@ export const MonitorListComponent: ({
filters,
monitorList: { list, error, loading },
pageSize,
refreshedMonitorIds,
setPageSize,
}: Props) => any = ({ filters, monitorList: { list, error, loading }, pageSize, setPageSize }) => {
}: Props) => any = ({
filters,
refreshedMonitorIds = [],
monitorList: { list, error, loading },
pageSize,
setPageSize,
}) => {
const [expandedDrawerIds, updateExpandedDrawerIds] = useState<string[]>([]);
const { width } = useWindowSize();
const [hideExtraColumns, setHideExtraColumns] = useState(false);
@ -94,6 +105,8 @@ export const MonitorListComponent: ({
}, {});
};
const { config } = useUptimeSettingsContext();
const columns = [
...[
{
@ -103,12 +116,27 @@ export const MonitorListComponent: ({
mobileOptions: {
fullWidth: true,
},
render: (status: string, { state: { timestamp, summaryPings } }: MonitorSummary) => {
render: (
status: string,
{
monitor_id: monitorId,
state: {
timestamp,
summaryPings,
monitor: { type, duration },
},
configId,
}: MonitorSummary
) => {
return (
<MonitorListStatusColumn
configId={configId}
status={status}
timestamp={timestamp}
summaryPings={summaryPings ?? []}
monitorType={type}
duration={duration!.us}
monitorId={monitorId}
/>
);
},
@ -166,20 +194,31 @@ export const MonitorListComponent: ({
},
]
: []),
...[
{
align: 'center' as const,
field: '',
name: STATUS_ALERT_COLUMN,
width: '100px',
render: (item: MonitorSummary) => (
<EnableMonitorAlert
monitorId={item.monitor_id}
selectedMonitor={item.state.summaryPings[0]}
/>
),
},
],
{
align: 'center' as const,
field: '',
name: STATUS_ALERT_COLUMN,
width: '100px',
render: (item: MonitorSummary) => (
<EnableMonitorAlert
monitorId={item.monitor_id}
selectedMonitor={item.state.summaryPings[0]}
/>
),
},
...(config.ui?.monitorManagement?.enabled
? [
{
align: 'center' as const,
field: '',
name: TEST_NOW_COLUMN,
width: '100px',
render: (item: MonitorSummary) => (
<TestNowColumn monitorId={item.monitor_id} configId={item.configId} />
),
},
]
: []),
...(!hideExtraColumns
? [
{
@ -205,7 +244,7 @@ export const MonitorListComponent: ({
];
return (
<EuiPanel hasBorder>
<WrapperPanel hasBorder>
<MonitorListHeader />
<EuiSpacer size="m" />
<EuiBasicTable
@ -226,7 +265,9 @@ export const MonitorListComponent: ({
onClick: () => toggleDrawer(monitorId),
'aria-label': labels.getExpandDrawerLabel(monitorId),
})
: undefined
: ({ monitor_id: monitorId }) => ({
className: refreshedMonitorIds.includes(monitorId) ? 'refresh-row' : undefined,
})
}
/>
<EuiSpacer size="m" />
@ -253,6 +294,17 @@ export const MonitorListComponent: ({
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</WrapperPanel>
);
};
const WrapperPanel = euiStyled(EuiPanel)`
&&& {
.refresh-row{
background-color: #f0f4fb;
-webkit-transition: background-color 3000ms linear;
-ms-transition: background-color 3000ms linear;
transition: background-color 3000ms linear;
}
}
`;

View file

@ -7,7 +7,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getMonitorList } from '../../../state/actions';
import { clearRefreshedMonitorId, getMonitorList } from '../../../state/actions';
import { esKuerySelector, monitorListSelector } from '../../../state/selectors';
import { MonitorListComponent } from './monitor_list';
import { useUrlParams } from '../../../hooks';
@ -15,6 +15,7 @@ import { UptimeRefreshContext } from '../../../contexts';
import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts';
import { useMappingCheck } from '../../../hooks/use_mapping_check';
import { useOverviewFilterCheck } from '../../../hooks/use_overview_filter_check';
import { refreshedMonitorSelector } from '../../../state/reducers/monitor_list';
export interface MonitorListProps {
filters?: string;
@ -43,6 +44,8 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
const { lastRefresh } = useContext(UptimeRefreshContext);
const refreshedMonitorIds = useSelector(refreshedMonitorSelector);
const monitorList = useSelector(monitorListSelector);
useMappingCheck(monitorList.error);
@ -81,12 +84,23 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
dispatch(getConnectorsAction.get());
}, [dispatch]);
useEffect(() => {
if (refreshedMonitorIds) {
refreshedMonitorIds.forEach((id) => {
setTimeout(() => {
dispatch(clearRefreshedMonitorId(id));
}, 5 * 1000);
});
}
}, [dispatch, refreshedMonitorIds]);
return (
<MonitorListComponent
{...props}
monitorList={monitorList}
pageSize={pageSize}
setPageSize={setPageSize}
refreshedMonitorIds={refreshedMonitorIds}
/>
);
};

View file

@ -73,3 +73,7 @@ export const RESPONSE_ANOMALY_SCORE = i18n.translate(
export const STATUS_ALERT_COLUMN = i18n.translate('xpack.uptime.monitorList.statusAlert.label', {
defaultMessage: 'Status alert',
});
export const TEST_NOW_COLUMN = i18n.translate('xpack.uptime.monitorList.testNow.label', {
defaultMessage: 'Test now',
});

View file

@ -59,6 +59,7 @@ export const mockState: AppState = {
summaries: [],
},
loading: false,
refreshedMonitorIds: [],
},
monitorManagementList: {
list: {
@ -115,4 +116,7 @@ export const mockState: AppState = {
cacheSize: 0,
hitCount: [],
},
testNowRuns: {
testNowRuns: [],
},
};

View file

@ -7,9 +7,25 @@
import { createAction } from 'redux-actions';
import { FetchMonitorStatesQueryArgs, MonitorSummariesResult } from '../../../common/runtime_types';
import { createAsyncAction } from './utils';
import { TestNowResponse } from '../api';
export const getMonitorList = createAction<FetchMonitorStatesQueryArgs>('GET_MONITOR_LIST');
export const getMonitorListSuccess = createAction<MonitorSummariesResult>(
'GET_MONITOR_LIST_SUCCESS'
);
export const getMonitorListFailure = createAction<Error>('GET_MONITOR_LIST_FAIL');
export const setUpdatingMonitorId = createAction<string>('SET_UPDATING_MONITOR_ID');
export const clearRefreshedMonitorId = createAction<string>('CLEAR_REFRESH_MONITOR_ID');
export const testNowMonitorAction = createAsyncAction<string, TestNowResponse | undefined>(
'TEST_NOW_MONITOR_ACTION'
);
export const clearTestNowMonitorAction = createAction<string>('CLEAR_TEST_NOW_MONITOR_ACTION');
export const getUpdatedMonitor = createAsyncAction<
FetchMonitorStatesQueryArgs,
MonitorSummariesResult
>('GET_UPDATED_MONITOR');

View file

@ -67,3 +67,13 @@ export const runOnceMonitor = async ({
}): Promise<{ errors: Array<{ error: Error }> }> => {
return await apiService.post(API_URLS.RUN_ONCE_MONITOR + `/${id}`, monitor);
};
export interface TestNowResponse {
errors?: Array<{ error: Error }>;
testRunId: string;
monitorId: string;
}
export const testNowMonitor = async (configId: string): Promise<TestNowResponse | undefined> => {
return await apiService.get(API_URLS.TRIGGER_MONITOR + `/${configId}`);
};

View file

@ -7,7 +7,11 @@
import { fork } from 'redux-saga/effects';
import { fetchMonitorDetailsEffect } from './monitor';
import { fetchMonitorListEffect } from './monitor_list';
import {
fetchMonitorListEffect,
fetchRunNowMonitorEffect,
fetchUpdatedMonitorEffect,
} from './monitor_list';
import { fetchMonitorManagementEffect } from './monitor_management';
import { fetchMonitorStatusEffect } from './monitor_status';
import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings';
@ -27,6 +31,7 @@ import {
export function* rootEffect() {
yield fork(fetchMonitorDetailsEffect);
yield fork(fetchMonitorListEffect);
yield fork(fetchUpdatedMonitorEffect);
yield fork(fetchMonitorManagementEffect);
yield fork(fetchMonitorStatusEffect);
yield fork(fetchDynamicSettingsEffect);
@ -42,4 +47,5 @@ export function* rootEffect() {
yield fork(fetchScreenshotBlocks);
yield fork(generateBlockStatsOnPut);
yield fork(pruneBlockCache);
yield fork(fetchRunNowMonitorEffect);
}

View file

@ -5,9 +5,15 @@
* 2.0.
*/
import { takeLatest } from 'redux-saga/effects';
import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '../actions';
import { fetchMonitorList } from '../api';
import { takeEvery, takeLatest } from 'redux-saga/effects';
import {
getMonitorList,
getMonitorListSuccess,
getMonitorListFailure,
getUpdatedMonitor,
testNowMonitorAction,
} from '../actions';
import { fetchMonitorList, testNowMonitor } from '../api';
import { fetchEffectFactory } from './fetch_effect';
export function* fetchMonitorListEffect() {
@ -16,3 +22,17 @@ export function* fetchMonitorListEffect() {
fetchEffectFactory(fetchMonitorList, getMonitorListSuccess, getMonitorListFailure)
);
}
export function* fetchUpdatedMonitorEffect() {
yield takeLatest(
getUpdatedMonitor.get,
fetchEffectFactory(fetchMonitorList, getUpdatedMonitor.success, getUpdatedMonitor.fail)
);
}
export function* fetchRunNowMonitorEffect() {
yield takeEvery(
testNowMonitorAction.get,
fetchEffectFactory(testNowMonitor, testNowMonitorAction.success, testNowMonitorAction.fail)
);
}

View file

@ -23,6 +23,7 @@ import { journeyReducer } from './journey';
import { networkEventsReducer } from './network_events';
import { syntheticsReducer } from './synthetics';
import { monitorManagementListReducer } from './monitor_management';
import { testNowRunsReducer } from './test_now_runs';
export const rootReducer = combineReducers({
monitor: monitorReducer,
@ -42,4 +43,5 @@ export const rootReducer = combineReducers({
journeys: journeyReducer,
networkEvents: networkEventsReducer,
synthetics: syntheticsReducer,
testNowRuns: testNowRunsReducer,
});

View file

@ -7,13 +7,24 @@
import { handleActions, Action } from 'redux-actions';
import { IHttpFetchError, ResponseErrorBody } from 'src/core/public';
import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '../actions';
import {
getMonitorList,
getMonitorListSuccess,
getMonitorListFailure,
getUpdatedMonitor,
clearRefreshedMonitorId,
setUpdatingMonitorId,
} from '../actions';
import { MonitorSummariesResult } from '../../../common/runtime_types';
import { AppState } from '../index';
import { TestNowResponse } from '../api';
export interface MonitorList {
error?: IHttpFetchError<ResponseErrorBody>;
loading: boolean;
refreshedMonitorIds?: string[];
isUpdating?: string[];
list: MonitorSummariesResult;
error?: IHttpFetchError<ResponseErrorBody>;
}
export const initialState: MonitorList = {
@ -23,9 +34,13 @@ export const initialState: MonitorList = {
summaries: [],
},
loading: false,
refreshedMonitorIds: [],
};
type Payload = MonitorSummariesResult & IHttpFetchError<ResponseErrorBody>;
type Payload = MonitorSummariesResult &
IHttpFetchError<ResponseErrorBody> &
string &
TestNowResponse;
export const monitorListReducer = handleActions<MonitorList, Payload>(
{
@ -50,6 +65,64 @@ export const monitorListReducer = handleActions<MonitorList, Payload>(
error: action.payload,
loading: false,
}),
[String(setUpdatingMonitorId)]: (state: MonitorList, action: Action<string>) => ({
...state,
isUpdating: [...(state.isUpdating ?? []), action.payload],
}),
[String(getUpdatedMonitor.get)]: (state: MonitorList) => ({
...state,
}),
[String(getUpdatedMonitor.success)]: (
state: MonitorList,
action: Action<MonitorSummariesResult>
) => {
const summaries = state.list.summaries;
const newSummary = action.payload.summaries?.[0];
if (!newSummary) {
return { ...state, isUpdating: [] };
}
return {
...state,
loading: false,
error: undefined,
isUpdating: state.isUpdating?.filter((item) => item !== newSummary.monitor_id),
refreshedMonitorIds: [...(state.refreshedMonitorIds ?? []), newSummary.monitor_id],
list: {
...state.list,
summaries: summaries.map((summary) => {
if (summary.monitor_id === newSummary.monitor_id) {
return newSummary;
}
return summary;
}),
},
};
},
[String(getUpdatedMonitor.fail)]: (
state: MonitorList,
action: Action<IHttpFetchError<ResponseErrorBody>>
) => ({
...state,
error: action.payload,
loading: false,
isUpdating: [],
}),
[String(clearRefreshedMonitorId)]: (state: MonitorList, action: Action<string>) => ({
...state,
refreshedMonitorIds: (state.refreshedMonitorIds ?? []).filter(
(item) => item !== action.payload
),
}),
},
initialState
);
export const refreshedMonitorSelector = ({ monitorList }: AppState) => {
return monitorList.refreshedMonitorIds ?? [];
};
export const isUpdatingMonitorSelector = ({ monitorList }: AppState) =>
monitorList.isUpdating ?? [];

View file

@ -0,0 +1,82 @@
/*
* 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 { createReducer, PayloadAction } from '@reduxjs/toolkit';
import { WritableDraft } from 'immer/dist/types/types-external';
import { clearTestNowMonitorAction, testNowMonitorAction } from '../actions';
import { TestNowResponse } from '../api';
import { AppState } from '../index';
export enum TestRunStats {
LOADING = 'loading',
IN_PROGRESS = 'in-progress',
COMPLETED = 'completed',
}
interface TestNowRun {
monitorId: string;
testRunId?: string;
status: TestRunStats;
}
export interface TestNowRunsState {
testNowRuns: TestNowRun[];
}
export const initialState: TestNowRunsState = {
testNowRuns: [],
};
export const testNowRunsReducer = createReducer(initialState, (builder) => {
builder
.addCase(
String(testNowMonitorAction.get),
(state: WritableDraft<TestNowRunsState>, action: PayloadAction<string>) => ({
...state,
testNowRuns: [
...state.testNowRuns,
{ monitorId: action.payload, status: TestRunStats.LOADING },
],
})
)
.addCase(
String(testNowMonitorAction.success),
(state: WritableDraft<TestNowRunsState>, { payload }: PayloadAction<TestNowResponse>) => ({
...state,
testNowRuns: state.testNowRuns.map((tRun) =>
tRun.monitorId === payload.monitorId
? {
monitorId: payload.monitorId,
testRunId: payload.testRunId,
status: TestRunStats.IN_PROGRESS,
}
: tRun
),
})
)
.addCase(
String(testNowMonitorAction.fail),
(state: WritableDraft<TestNowRunsState>, action: PayloadAction<TestNowResponse>) => ({
...state,
testNowRuns: [...(state.testNowRuns ?? [])],
})
)
.addCase(
String(clearTestNowMonitorAction),
(state: WritableDraft<TestNowRunsState>, action: PayloadAction<string>) => ({
...state,
testNowRuns: state.testNowRuns.filter((tRun) => tRun.monitorId !== action.payload),
})
);
});
export const testNowRunsSelector = ({ testNowRuns }: AppState) => testNowRuns.testNowRuns;
export const testNowRunSelector =
(monitorId?: string) =>
({ testNowRuns }: AppState) =>
testNowRuns.testNowRuns.find((tRun) => monitorId && monitorId === tRun.monitorId);

View file

@ -83,11 +83,13 @@ export const summaryPingsToSummary = (summaryPings: Ping[]): MonitorSummary => {
const latest = summaryPings[summaryPings.length - 1];
return {
monitor_id: latest.monitor.id,
configId: latest.config_id,
state: {
timestamp: latest.timestamp,
monitor: {
name: latest.monitor?.name,
type: latest.monitor?.type,
duration: latest.monitor?.duration,
},
url: latest.url ?? {},
summary: {

View file

@ -179,7 +179,15 @@ export class SyntheticsService {
};
}
async pushConfigs(request?: KibanaRequest, configs?: SyntheticsMonitorWithId[]) {
async pushConfigs(
request?: KibanaRequest,
configs?: Array<
SyntheticsMonitorWithId & {
fields_under_root?: boolean;
fields?: { config_id: string };
}
>
) {
const monitors = this.formatConfigs(configs || (await this.getMonitorConfigs()));
if (monitors.length === 0) {
this.logger.debug('No monitor found which can be pushed to service.');
@ -226,6 +234,32 @@ export class SyntheticsService {
}
}
async triggerConfigs(
request?: KibanaRequest,
configs?: Array<
SyntheticsMonitorWithId & {
fields_under_root?: boolean;
fields?: { config_id: string; test_run_id: string };
}
>
) {
const monitors = this.formatConfigs(configs || (await this.getMonitorConfigs()));
if (monitors.length === 0) {
return;
}
const data = {
monitors,
output: await this.getOutput(request),
};
try {
return await this.apiClient.runOnce(data);
} catch (e) {
this.logger.error(e);
throw e;
}
}
async deleteConfigs(request: KibanaRequest, configs: SyntheticsMonitorWithId[]) {
const data = {
monitors: this.formatConfigs(configs),

View file

@ -37,6 +37,7 @@ import { addSyntheticsMonitorRoute } from './synthetics_service/add_monitor';
import { editSyntheticsMonitorRoute } from './synthetics_service/edit_monitor';
import { deleteSyntheticsMonitorRoute } from './synthetics_service/delete_monitor';
import { runOnceSyntheticsMonitorRoute } from './synthetics_service/run_once_monitor';
import { testNowMonitorRoute } from './synthetics_service/test_now_monitor';
export * from './types';
export { createRouteWithAuth } from './create_route_with_auth';
@ -69,4 +70,5 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [
editSyntheticsMonitorRoute,
deleteSyntheticsMonitorRoute,
runOnceSyntheticsMonitorRoute,
testNowMonitorRoute,
];

View file

@ -38,6 +38,10 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
{
...newMonitor.attributes,
id: newMonitor.id,
fields: {
config_id: newMonitor.id,
},
fields_under_root: true,
},
]);

View file

@ -47,6 +47,10 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
{
...(editMonitor.attributes as SyntheticsMonitor),
id: editMonitor.id,
fields: {
config_id: editMonitor.id,
},
fields_under_root: true,
},
]);

View file

@ -0,0 +1,48 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { v4 as uuidv4 } from 'uuid';
import { SyntheticsMonitor } from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { syntheticsMonitorType } from '../../lib/saved_objects/synthetics_monitor';
export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.TRIGGER_MONITOR + '/{monitorId}',
validate: {
params: schema.object({
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, savedObjectsClient, response, server }): Promise<any> => {
const { monitorId } = request.params;
const monitor = await savedObjectsClient.get<SyntheticsMonitor>(
syntheticsMonitorType,
monitorId
);
const { syntheticsService } = server;
const testRunId = uuidv4();
const errors = await syntheticsService.triggerConfigs(request, [
{
...monitor.attributes,
id: monitorId,
fields_under_root: true,
fields: { config_id: monitorId, test_run_id: testRunId },
},
]);
if (errors && errors?.length > 0) {
return { errors, testRunId, monitorId };
}
return { testRunId, monitorId };
},
});