diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx
index 25cf400bcd0fd..1c587568fe61d 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { MonitorSummary } from '../../../../../../common/runtime_types';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
-import { IntegrationGroup } from '../actions_popover/integration_group';
+import { IntegrationGroup, extractSummaryValues } from '../actions_popover/integration_group';
describe('IntegrationGroup', () => {
let summary: MonitorSummary;
@@ -38,4 +38,97 @@ describe('IntegrationGroup', () => {
const component = shallowWithIntl();
expect(component).toMatchSnapshot();
});
+
+ describe('extractSummaryValues', () => {
+ let mockSummary: Pick;
+
+ beforeEach(() => {
+ mockSummary = {
+ state: {
+ timestamp: 'foo',
+ url: {},
+ },
+ };
+ });
+
+ it('provides defaults when values are not present', () => {
+ expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(`
+ Object {
+ "containerId": undefined,
+ "domain": "",
+ "ip": undefined,
+ "podUid": undefined,
+ }
+ `);
+ });
+
+ it('finds url domain', () => {
+ mockSummary.state.url.domain = 'mydomain';
+
+ expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(`
+ Object {
+ "containerId": undefined,
+ "domain": "mydomain",
+ "ip": undefined,
+ "podUid": undefined,
+ }
+ `);
+ });
+
+ it('finds pod uid', () => {
+ mockSummary.state.checks = [
+ { kubernetes: { pod: { uid: 'myuid' } }, monitor: { status: 'up' }, timestamp: 123 },
+ ];
+
+ expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(`
+ Object {
+ "containerId": undefined,
+ "domain": "",
+ "ip": undefined,
+ "podUid": "myuid",
+ }
+ `);
+ });
+
+ it('does not throw for missing kubernetes fields', () => {
+ mockSummary.state.checks = [];
+
+ expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(`
+ Object {
+ "containerId": undefined,
+ "domain": "",
+ "ip": undefined,
+ "podUid": undefined,
+ }
+ `);
+ });
+
+ it('finds container id', () => {
+ mockSummary.state.checks = [
+ { container: { id: 'mycontainer' }, monitor: { status: 'up' }, timestamp: 123 },
+ ];
+
+ expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(`
+ Object {
+ "containerId": "mycontainer",
+ "domain": "",
+ "ip": undefined,
+ "podUid": undefined,
+ }
+ `);
+ });
+
+ it('finds ip field', () => {
+ mockSummary.state.checks = [{ monitor: { ip: '127.0.0.1', status: 'up' }, timestamp: 123 }];
+
+ expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(`
+ Object {
+ "containerId": undefined,
+ "domain": "",
+ "ip": "127.0.0.1",
+ "podUid": undefined,
+ }
+ `);
+ });
+ });
});
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx
index 6c5aad0f34e06..55a99ab8541f8 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx
@@ -7,7 +7,6 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useContext } from 'react';
import { i18n } from '@kbn/i18n';
-import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { IntegrationLink } from './integration_link';
import {
@@ -26,6 +25,20 @@ interface IntegrationGroupProps {
summary: MonitorSummary;
}
+export const extractSummaryValues = (summary: Pick) => {
+ const domain = summary.state.url?.domain ?? '';
+ const podUid = summary.state.checks?.[0]?.kubernetes?.pod.uid ?? undefined;
+ const containerId = summary.state.checks?.[0]?.container?.id ?? undefined;
+ const ip = summary.state.checks?.[0]?.monitor.ip ?? undefined;
+
+ return {
+ domain,
+ podUid,
+ containerId,
+ ip,
+ };
+};
+
export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => {
const {
basePath,
@@ -36,10 +49,7 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => {
isLogsAvailable,
} = useContext(UptimeSettingsContext);
- const domain = get(summary, 'state.url.domain', '');
- const podUid = get(summary, 'state.checks[0].kubernetes.pod.uid', undefined);
- const containerId = get(summary, 'state.checks[0].container.id', undefined);
- const ip = get(summary, 'state.checks[0].monitor.ip', undefined);
+ const { domain, podUid, containerId, ip } = extractSummaryValues(summary);
return isApmAvailable || isInfraAvailable || isLogsAvailable ? (
@@ -97,7 +107,7 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => {
{
defaultMessage: 'Check Infrastructure UI for the IP "{ip}"',
values: {
- ip,
+ ip: Array.isArray(ip) ? ip[0] : ip,
},
}
)}
@@ -184,7 +194,12 @@ export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => {
)}
tooltipContent={i18n.translate(
'xpack.uptime.monitorList.loggingIntegrationAction.ip.tooltip',
- { defaultMessage: 'Check Logging UI for the IP "{ip}"', values: { ip } }
+ {
+ defaultMessage: 'Check Logging UI for the IP "{ip}"',
+ values: {
+ ip: Array.isArray(ip) ? ip[0] : ip,
+ },
+ }
)}
/>
diff --git a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx
index 033f466ad4261..d6185f2c2589a 100644
--- a/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx
+++ b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx
@@ -7,7 +7,6 @@
import { CoreStart } from 'src/core/public';
import React from 'react';
import ReactDOM from 'react-dom';
-import { get } from 'lodash';
import { i18n as i18nFormatter } from '@kbn/i18n';
import { UptimeApp, UptimeAppProps } from '../../../uptime_app';
import { getIntegratedAppAvailability } from './capabilities_adapter';
@@ -38,7 +37,7 @@ export const getKibanaFrameworkAdapter = (
INTEGRATED_SOLUTIONS
);
- const canSave = get(capabilities, 'uptime.save', false) as boolean;
+ const canSave = (capabilities.uptime.save ?? false) as boolean;
const props: UptimeAppProps = {
basePath: basePath.get(),
diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts
new file mode 100644
index 0000000000000..dd7996b68c41f
--- /dev/null
+++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 { sortChecksBy } from '../enrich_monitor_groups';
+
+describe('enrich monitor groups', () => {
+ describe('sortChecksBy', () => {
+ it('identifies lesser geo name', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'less' } }, monitor: { status: 'up' } },
+ { observer: { geo: { name: 'more' } }, monitor: { status: 'up' } }
+ )
+ ).toBe(-1);
+ });
+
+ it('identifies greater geo name', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'more' } }, monitor: { status: 'up' } },
+ { observer: { geo: { name: 'less' } }, monitor: { status: 'up' } }
+ )
+ ).toBe(1);
+ });
+
+ it('identifies equivalent geo name and sorts by lesser ip', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } },
+ { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.2', status: 'up' } }
+ )
+ ).toBe(-1);
+ });
+
+ it('identifies equivalent geo name and sorts by greater ip', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.2', status: 'up' } },
+ { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } }
+ )
+ ).toBe(1);
+ });
+
+ it('identifies equivalent geo name and sorts by equivalent ip', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } },
+ { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } }
+ )
+ ).toBe(0);
+ });
+
+ it('handles equivalent ip arrays', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } },
+ { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } }
+ )
+ ).toBe(0);
+ });
+
+ it('handles non-equal ip arrays', () => {
+ expect(
+ sortChecksBy(
+ {
+ observer: { geo: { name: 'same' } },
+ monitor: { ip: ['127.0.0.2', '127.0.0.9'], status: 'up' },
+ },
+ {
+ observer: { geo: { name: 'same' } },
+ monitor: { ip: ['127.0.0.3', '127.0.0.1'], status: 'up' },
+ }
+ )
+ ).toBe(1);
+ });
+
+ it('handles undefined observer fields', () => {
+ expect(
+ sortChecksBy(
+ { observer: undefined, monitor: { ip: ['127.0.0.1'], status: 'up' } },
+ { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } }
+ )
+ ).toBe(-1);
+ });
+
+ it('handles undefined ip fields', () => {
+ expect(
+ sortChecksBy(
+ { observer: { geo: { name: 'same' } }, monitor: { ip: undefined, status: 'up' } },
+ { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } }
+ )
+ ).toBe(-1);
+ });
+ });
+});
diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts
index 52842005857a1..f5c4c55a4e300 100644
--- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { get, sortBy } from 'lodash';
import { QueryContext } from './query_context';
import {
Check,
@@ -245,17 +244,17 @@ export const enrichMonitorGroups: MonitorEnricher = async (
const items = await queryContext.search(params);
- const monitorBuckets = get(items, 'aggregations.monitors.buckets', []);
+ const monitorBuckets = items?.aggregations?.monitors?.buckets ?? [];
const monitorIds: string[] = [];
const summaries: MonitorSummary[] = monitorBuckets.map((monitor: any) => {
- const monitorId = get(monitor, 'key.monitor_id');
+ const monitorId = monitor.key.monitor_id;
monitorIds.push(monitorId);
const state: any = monitor.state?.value;
state.timestamp = state['@timestamp'];
const { checks } = state;
- if (checks) {
- state.checks = sortBy(checks, checksSortBy);
+ if (Array.isArray(checks)) {
+ checks.sort(sortChecksBy);
state.checks = state.checks.map((check: any) => ({
...check,
timestamp: check['@timestamp'],
@@ -276,7 +275,11 @@ export const enrichMonitorGroups: MonitorEnricher = async (
histogram: histogramMap[summary.monitor_id],
}));
- const sortedResItems: any = sortBy(resItems, 'monitor_id');
+ const sortedResItems: any = resItems.sort((a, b) => {
+ if (a.monitor_id === b.monitor_id) return 0;
+ return a.monitor_id > b.monitor_id ? 1 : -1;
+ });
+
if (queryContext.pagination.sortOrder === SortOrder.DESC) {
sortedResItems.reverse();
}
@@ -378,4 +381,29 @@ const cursorDirectionToOrder = (cd: CursorDirection): 'asc' | 'desc' => {
return CursorDirection[cd] === CursorDirection.AFTER ? 'asc' : 'desc';
};
-const checksSortBy = (check: Check) => [get(check, 'observer.geo.name'), get(check, 'monitor.ip')];
+const getStringValue = (value: string | Array | null | undefined): string => {
+ if (Array.isArray(value)) {
+ value.sort();
+ return value[0] ?? '';
+ }
+ return value ?? '';
+};
+
+export const sortChecksBy = (
+ a: Pick,
+ b: Pick
+) => {
+ const nameA: string = a.observer?.geo?.name ?? '';
+ const nameB: string = b.observer?.geo?.name ?? '';
+
+ if (nameA === nameB) {
+ const ipA = getStringValue(a.monitor.ip);
+ const ipB = getStringValue(b.monitor.ip);
+
+ if (ipA === ipB) {
+ return 0;
+ }
+ return ipA > ipB ? 1 : -1;
+ }
+ return nameA > nameB ? 1 : -1;
+};