[Synthetics] Monitor details panel (#134814)

Co-authored-by: Abdul Zahid <awahab07@yahoo.com>
This commit is contained in:
Shahzad 2022-06-23 10:34:29 +02:00 committed by GitHub
parent d11c0be465
commit 0ee2ae074e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 421 additions and 43 deletions

View file

@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { ESSearchResponse } from '@kbn/core/types/elasticsearch';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { IInspectorInfo, isCompleteResponse } from '@kbn/data-plugin/common';
import { IInspectorInfo, isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
import { FETCH_STATUS, useFetcher } from './use_fetcher';
import { useInspectorContext } from '../context/inspector/use_inspector_context';
import { getInspectResponse } from '../../common/utils/get_inspect_response';
@ -69,6 +69,34 @@ export const useEsSearch = <DocumentSource extends unknown, TParams extends esty
search$.unsubscribe();
}
},
error: (err) => {
if (isErrorResponse(err)) {
console.error(err);
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: null,
esError: { originalError: err, name: err.name, message: err.message },
esRequestStatus: 2,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
}
},
});
});
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import moment from 'moment';
export const CLIENT_DEFAULTS = {
ABSOLUTE_DATE_RANGE_START: 0,
// 15 minutes
@ -43,3 +45,17 @@ export const CLIENT_DEFAULTS = {
};
export const EXCLUDE_RUN_ONCE_FILTER = { bool: { must_not: { exists: { field: 'run_once' } } } };
export const SUMMARY_FILTER = {
exists: {
field: 'summary',
},
};
export const getTimeSpanFilter = () => ({
range: {
'monitor.timespan': {
lte: moment().toISOString(),
gte: moment().subtract(5, 'minutes').toISOString(),
},
},
});

View file

@ -0,0 +1,74 @@
/*
* 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 { useEsSearch } from '@kbn/observability-plugin/public';
import { useParams } from 'react-router-dom';
import { useMemo } from 'react';
import { Ping } from '../../../../../../common/runtime_types';
import {
EXCLUDE_RUN_ONCE_FILTER,
getTimeSpanFilter,
SUMMARY_FILTER,
} from '../../../../../../common/constants/client_defaults';
import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context';
import { SYNTHETICS_INDEX_PATTERN, UNNAMED_LOCATION } from '../../../../../../common/constants';
export function useStatusByLocation() {
const { lastRefresh } = useSyntheticsRefreshContext();
const { monitorId } = useParams<{ monitorId: string }>();
const { data, loading } = useEsSearch(
{
index: SYNTHETICS_INDEX_PATTERN,
body: {
size: 0,
query: {
bool: {
filter: [
SUMMARY_FILTER,
EXCLUDE_RUN_ONCE_FILTER,
getTimeSpanFilter(),
{
term: {
config_id: monitorId,
},
},
],
},
},
sort: [{ '@timestamp': 'desc' }],
aggs: {
locations: {
terms: {
field: 'observer.geo.name',
missing: UNNAMED_LOCATION,
size: 1000,
},
aggs: {
summary: {
top_hits: {
size: 1,
},
},
},
},
},
},
},
[lastRefresh, monitorId],
{ name: 'getMonitorStatusByLocation' }
);
return useMemo(() => {
const locations = (data?.aggregations?.locations.buckets ?? []).map((loc) => {
return loc.summary.hits.hits?.[0]._source as Ping;
});
return { locations, loading };
}, [data, loading]);
}

View file

@ -5,14 +5,23 @@
* 2.0.
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { selectMonitorStatus } from '../../state/monitor_summary';
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import { getSyntheticsMonitorAction, selectMonitorStatus } from '../../state/monitor_summary';
import { useMonitorListBreadcrumbs } from '../monitors_page/hooks/use_breadcrumbs';
export const MonitorSummaryPage = () => {
const { data } = useSelector(selectMonitorStatus);
useMonitorListBreadcrumbs([{ text: data?.monitor.name ?? '' }]);
const dispatch = useDispatch();
const { monitorId } = useParams<{ monitorId: string }>();
useEffect(() => {
dispatch(getSyntheticsMonitorAction.get(monitorId));
}, [dispatch, monitorId]);
return <></>;
};

View file

@ -50,7 +50,7 @@ export const MonitorSummaryTabs = () => {
return (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[1]}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={(tab) => {}}
/>

View file

@ -0,0 +1,32 @@
/*
* 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 { EuiBadge, EuiBadgeGroup, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
import { useStatusByLocation } from '../hooks/use_status_by_location';
export const LocationsStatus = () => {
const { locations, loading } = useStatusByLocation();
if (loading) {
return <EuiLoadingSpinner />;
}
return (
<EuiBadgeGroup>
{locations.map((loc) => (
<EuiBadge
iconType={() => (
<EuiIcon type="dot" color={(loc.summary?.down ?? 0) > 0 ? 'danger' : 'success'} />
)}
color="hollow"
>
{loc.observer?.geo?.name}
</EuiBadge>
))}
</EuiBadgeGroup>
);
};

View file

@ -0,0 +1,111 @@
/*
* 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 {
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiBadge,
EuiSpacer,
EuiLink,
EuiLoadingSpinner,
} from '@elastic/eui';
import { capitalize } from 'lodash';
import { i18n } from '@kbn/i18n';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { MonitorTags } from './monitor_tags';
import { MonitorEnabled } from '../../monitors_page/management/monitor_list_table/monitor_enabled';
import { LocationsStatus } from './locations_status';
import {
getSyntheticsMonitorAction,
selectMonitorStatus,
syntheticsMonitorSelector,
} from '../../../state/monitor_summary';
import { ConfigKey } from '../../../../../../common/runtime_types';
export const MonitorDetailsPanel = () => {
const { data } = useSelector(selectMonitorStatus);
const { monitorId } = useParams<{ monitorId: string }>();
const dispatch = useDispatch();
const { data: monitor, loading } = useSelector(syntheticsMonitorSelector);
if (!data) {
return <EuiLoadingSpinner />;
}
return (
<>
<EuiSpacer />
<EuiDescriptionList type="responsiveColumn" style={{ maxWidth: '400px' }}>
<EuiDescriptionListTitle>{ENABLED_LABEL}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{monitor && (
<MonitorEnabled
initialLoading={loading}
id={monitorId}
monitor={monitor}
reloadPage={() => {
dispatch(getSyntheticsMonitorAction.get(monitorId));
}}
/>
)}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>{MONITOR_TYPE_LABEL}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
<EuiBadge>{capitalize(data.monitor.type)}</EuiBadge>
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>{FREQUENCY_LABEL}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>Every 10 mins</EuiDescriptionListDescription>
<EuiDescriptionListTitle>{LOCATIONS_LABEL}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
<LocationsStatus />
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>{URL_LABEL}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
<EuiLink href={data.url?.full} external>
{data.url?.full}
</EuiLink>
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>{TAGS_LABEL}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{monitor && <MonitorTags tags={monitor[ConfigKey.TAGS]} />}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</>
);
};
const FREQUENCY_LABEL = i18n.translate('xpack.synthetics.management.monitorList.frequency', {
defaultMessage: 'Frequency',
});
const LOCATIONS_LABEL = i18n.translate('xpack.synthetics.management.monitorList.locations', {
defaultMessage: 'Locations',
});
const URL_LABEL = i18n.translate('xpack.synthetics.management.monitorList.url', {
defaultMessage: 'URL',
});
const TAGS_LABEL = i18n.translate('xpack.synthetics.management.monitorList.tags', {
defaultMessage: 'Tags',
});
const ENABLED_LABEL = i18n.translate('xpack.synthetics.detailsPanel.monitorDetails.enabled', {
defaultMessage: 'Enabled',
});
const MONITOR_TYPE_LABEL = i18n.translate(
'xpack.synthetics.detailsPanel.monitorDetails.monitorType',
{
defaultMessage: 'Monitor type',
}
);

View file

@ -0,0 +1,19 @@
/*
* 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 { EuiBadge, EuiBadgeGroup } from '@elastic/eui';
export const MonitorTags = ({ tags }: { tags: string[] }) => {
return (
<EuiBadgeGroup>
{tags.map((tag) => (
<EuiBadge color="hollow">{tag}</EuiBadge>
))}
</EuiBadgeGroup>
);
};

View file

@ -5,9 +5,23 @@
* 2.0.
*/
import { EuiText } from '@elastic/eui';
import React from 'react';
import { EuiTitle, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MonitorDetailsPanel } from './monitor_details_panel';
export const SummaryTabContent = () => {
return <EuiText>Monitor summary tab content</EuiText>;
return (
<EuiPanel>
<EuiTitle size="s">
<h3>{MONITOR_DETAILS_LABEL}</h3>
</EuiTitle>
<MonitorDetailsPanel />
</EuiPanel>
);
};
const MONITOR_DETAILS_LABEL = i18n.translate('xpack.synthetics.detailsPanel.monitorDetails', {
defaultMessage: 'Monitor details',
});

View file

@ -132,12 +132,7 @@ export function getMonitorListColumns({
defaultMessage: 'Enabled',
}),
render: (_enabled: boolean, monitor: EncryptedSyntheticsSavedMonitor) => (
<MonitorEnabled
id={monitor.id}
monitor={monitor}
isDisabled={!canEditSynthetics}
reloadPage={reloadPage}
/>
<MonitorEnabled id={monitor.id} monitor={monitor} reloadPage={reloadPage} />
),
},
{

View file

@ -10,6 +10,7 @@ import React, { useEffect, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public';
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
import { ConfigKey, EncryptedSyntheticsMonitor } from '../../../../../../../common/runtime_types';
import { fetchUpsertMonitor } from '../../../../state';
@ -19,10 +20,12 @@ interface Props {
id: string;
monitor: EncryptedSyntheticsMonitor;
reloadPage: () => void;
isDisabled?: boolean;
initialLoading?: boolean;
}
export const MonitorEnabled = ({ id, monitor, reloadPage, isDisabled }: Props) => {
export const MonitorEnabled = ({ id, monitor, reloadPage, initialLoading }: Props) => {
const isDisabled = !useCanEditSynthetics();
const [isEnabled, setIsEnabled] = useState<boolean | null>(null);
const { notifications } = useKibana();
@ -69,7 +72,7 @@ export const MonitorEnabled = ({ id, monitor, reloadPage, isDisabled }: Props) =
return (
<>
{isLoading ? (
{isLoading || initialLoading ? (
<EuiLoadingSpinner size="m" />
) : (
<EuiSwitch

View file

@ -6,7 +6,7 @@
*/
import { createAction } from '@reduxjs/toolkit';
import { Ping } from '../../../../../common/runtime_types';
import { Ping, SyntheticsMonitor } from '../../../../../common/runtime_types';
import { QueryParams } from './api';
import { createAsyncAction } from '../utils/actions';
@ -15,3 +15,7 @@ export const setMonitorSummaryLocationAction = createAction<string>(
);
export const getMonitorStatusAction = createAsyncAction<QueryParams, Ping>('[MONITOR SUMMARY] GET');
export const getSyntheticsMonitorAction = createAsyncAction<string, SyntheticsMonitor>(
'fetchSyntheticsMonitorAction'
);

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import { SavedObject } from '@kbn/core/types';
import { apiService } from '../../../../utils/api_service';
import { Ping } from '../../../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { Ping, SyntheticsMonitor } from '../../../../../common/runtime_types';
import { API_URLS, SYNTHETICS_API_URLS } from '../../../../../common/constants';
export interface QueryParams {
monitorId: string;
@ -18,3 +19,11 @@ export interface QueryParams {
export const fetchMonitorStatus = async (params: QueryParams): Promise<Ping> => {
return await apiService.get(SYNTHETICS_API_URLS.MONITOR_STATUS, { ...params });
};
export const fetchSyntheticsMonitor = async (monitorId: string): Promise<SyntheticsMonitor> => {
const { attributes } = (await apiService.get(
`${API_URLS.SYNTHETICS_MONITORS}/${monitorId}`
)) as SavedObject<SyntheticsMonitor>;
return attributes;
};

View file

@ -7,8 +7,8 @@
import { takeLeading } from 'redux-saga/effects';
import { fetchEffectFactory } from '../utils/fetch_effect';
import { getMonitorStatusAction } from './actions';
import { fetchMonitorStatus } from './api';
import { getMonitorStatusAction, getSyntheticsMonitorAction } from './actions';
import { fetchMonitorStatus, fetchSyntheticsMonitor } from './api';
export function* fetchMonitorStatusEffect() {
yield takeLeading(
@ -20,3 +20,14 @@ export function* fetchMonitorStatusEffect() {
)
);
}
export function* fetchSyntheticsMonitorEffect() {
yield takeLeading(
getSyntheticsMonitorAction.get,
fetchEffectFactory(
fetchSyntheticsMonitor,
getSyntheticsMonitorAction.success,
getSyntheticsMonitorAction.fail
)
);
}

View file

@ -16,3 +16,5 @@ export const selectSelectedLocationId = createSelector(
);
export const selectMonitorStatus = createSelector(getState, (state) => state);
export const syntheticsMonitorSelector = (state: SyntheticsAppState) => state.syntheticsMonitor;

View file

@ -0,0 +1,38 @@
/*
* 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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { createReducer } from '@reduxjs/toolkit';
import { SyntheticsMonitor } from '../../../../../common/runtime_types';
import { getSyntheticsMonitorAction } from './actions';
export interface SyntheticsMonitorState {
data: SyntheticsMonitor | null;
loading: boolean;
error: IHttpFetchError<ResponseErrorBody> | null;
}
const initialState: SyntheticsMonitorState = {
data: null,
loading: false,
error: null,
};
export const syntheticsMonitorReducer = createReducer(initialState, (builder) => {
builder
.addCase(getSyntheticsMonitorAction.get, (state) => {
state.loading = true;
})
.addCase(getSyntheticsMonitorAction.success, (state, action) => {
state.data = action.payload;
state.loading = false;
})
.addCase(getSyntheticsMonitorAction.fail, (state, action) => {
state.error = action.payload as IHttpFetchError<ResponseErrorBody>;
state.loading = false;
});
});

View file

@ -6,7 +6,7 @@
*/
import { all, fork } from 'redux-saga/effects';
import { fetchMonitorStatusEffect } from './monitor_summary';
import { fetchMonitorStatusEffect, fetchSyntheticsMonitorEffect } from './monitor_summary';
import { fetchIndexStatusEffect } from './index_status';
import { fetchSyntheticsEnablementEffect } from './synthetics_enablement';
import { fetchMonitorListEffect } from './monitor_list';
@ -19,5 +19,6 @@ export const rootEffect = function* root(): Generator {
fork(fetchServiceLocationsEffect),
fork(fetchMonitorListEffect),
fork(fetchMonitorStatusEffect),
fork(fetchSyntheticsMonitorEffect),
]);
};

View file

@ -7,6 +7,7 @@
import { combineReducers } from '@reduxjs/toolkit';
import { syntheticsMonitorReducer } from './monitor_summary/synthetics_montior_reducer';
import { monitorStatusReducer } from './monitor_summary';
import { uiReducer } from './ui';
import { indexStatusReducer } from './index_status';
@ -21,6 +22,7 @@ export const rootReducer = combineReducers({
monitorList: monitorListReducer,
serviceLocations: serviceLocationsReducer,
monitorStatus: monitorStatusReducer,
syntheticsMonitor: syntheticsMonitorReducer,
});
export type SyntheticsAppState = ReturnType<typeof rootReducer>;

View file

@ -84,4 +84,9 @@ export const mockState: SyntheticsAppState = {
error: null,
selectedLocationId: null,
},
syntheticsMonitor: {
data: null,
loading: false,
error: null,
},
};

View file

@ -55,8 +55,11 @@ export class ServiceAPIClient {
this.server = server;
}
getHttpsAgent() {
getHttpsAgent(url: string) {
const config = this.config;
if (url !== this.config.devUrl && this.authorization && this.server.isDev) {
return;
}
if (config.tls && config.tls.certificate && config.tls.key) {
const tlsConfig = new SslConfig(config.tls);
@ -92,29 +95,31 @@ export class ServiceAPIClient {
return { allowed: true, signupUrl: null };
}
const httpsAgent = this.getHttpsAgent();
if (this.locations.length > 0 && httpsAgent) {
if (this.locations.length > 0) {
// get a url from a random location
const url = this.locations[Math.floor(Math.random() * this.locations.length)].url;
try {
const { data } = await axios({
method: 'GET',
url: url + '/allowed',
headers:
process.env.NODE_ENV !== 'production' && this.authorization
? {
Authorization: this.authorization,
}
: undefined,
httpsAgent,
});
const httpsAgent = this.getHttpsAgent(url);
const { allowed, signupUrl } = data;
return { allowed, signupUrl };
} catch (e) {
this.logger.error(e);
if (httpsAgent) {
try {
const { data } = await axios({
method: 'GET',
url: url + '/allowed',
headers:
process.env.NODE_ENV !== 'production' && this.authorization
? {
Authorization: this.authorization,
}
: undefined,
httpsAgent,
});
const { allowed, signupUrl } = data;
return { allowed, signupUrl };
} catch (e) {
this.logger.error(e);
}
}
}
@ -151,7 +156,7 @@ export class ServiceAPIClient {
Authorization: this.authorization,
}
: undefined,
httpsAgent: this.getHttpsAgent(),
httpsAgent: this.getHttpsAgent(url),
});
};