Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
7736576
mock reset endpoint, UI first approach
miguelmartin-elastic Mar 9, 2026
daf42b8
pass monitors as prop
miguelmartin-elastic Mar 9, 2026
c6d6565
use icon with tooltip for missing policy warning
miguelmartin-elastic Mar 9, 2026
a3b0758
fetch configs from hook if none is provided
miguelmartin-elastic Mar 9, 2026
043faaf
_health api spec proposal
miguelmartin-elastic Mar 10, 2026
32ea22e
refactor monirots_integration_health
miguelmartin-elastic Mar 10, 2026
b01ce6a
add unit tests
miguelmartin-elastic Mar 10, 2026
889718b
move types to common/
miguelmartin-elastic Mar 10, 2026
de0a995
rename from status to health
miguelmartin-elastic Mar 10, 2026
86e9383
fix import
miguelmartin-elastic Mar 10, 2026
e4ff75e
i18n status display messages
miguelmartin-elastic Mar 10, 2026
8f77683
store configId for debugging purposes
miguelmartin-elastic Mar 11, 2026
d16abdd
remove unused import
miguelmartin-elastic Mar 11, 2026
37e3483
check if agentPolicy exists
miguelmartin-elastic Mar 11, 2026
d379d26
rename isUnhealthy to be more generic
miguelmartin-elastic Mar 11, 2026
e91cfb4
refactor unhealthy tooltip
miguelmartin-elastic Mar 11, 2026
e143d07
refactor ResetLocationMonitors
miguelmartin-elastic Mar 11, 2026
7908d0b
checkout bulk operation reset
miguelmartin-elastic Mar 11, 2026
c60be8b
parse error from reason
miguelmartin-elastic Mar 11, 2026
03df478
remove oas docs
miguelmartin-elastic Mar 11, 2026
3ee0c34
move health api to service
miguelmartin-elastic Mar 11, 2026
c5cc305
fix naming
miguelmartin-elastic Mar 11, 2026
00cc5a5
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 11, 2026
5bc168b
fix mock
miguelmartin-elastic Mar 12, 2026
1c82841
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 12, 2026
8fa37fe
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 12, 2026
d5d0b36
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 13, 2026
158ac3d
remove spaceId
miguelmartin-elastic Mar 13, 2026
2111b8f
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 13, 2026
19cf9c5
fix type
miguelmartin-elastic Mar 13, 2026
996629e
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 13, 2026
e9581aa
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 13, 2026
42a2722
Support legacy policy ID format in monitor health checks
miguelmartin-elastic Mar 13, 2026
4d7c9e9
Document intentional priority order of health status checks
miguelmartin-elastic Mar 13, 2026
947aa55
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 13, 2026
1ad223d
Merge branch 'main' of https://github.com/elastic/kibana into synthet…
miguelmartin-elastic Mar 16, 2026
b0f8372
wire up reset api, add unit tests
miguelmartin-elastic Mar 16, 2026
b9a80f6
add toast feedback and reset confirmation modal
miguelmartin-elastic Mar 16, 2026
c68b4fd
listen to refresh
miguelmartin-elastic Mar 16, 2026
7a9783c
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 16, 2026
10cb00b
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 16, 2026
f9886a9
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 16, 2026
1c75100
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 18, 2026
1a672bf
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 20, 2026
1347bf1
fix integ test
miguelmartin-elastic Mar 20, 2026
14bb9da
* simplify reset callouts by removing internal state
miguelmartin-elastic Mar 20, 2026
b6a66fa
do not mark monitors with public locations only as unhealthy, cover w…
miguelmartin-elastic Mar 20, 2026
6b40f25
fix minor issues
miguelmartin-elastic Mar 20, 2026
0965ae8
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 20, 2026
29e9733
handle partial errors from reset api
miguelmartin-elastic Mar 20, 2026
7880bf3
Revert "fix integ test"
miguelmartin-elastic Mar 23, 2026
7a20f23
Merge branch 'main' of https://github.com/elastic/kibana into synthet…
miguelmartin-elastic Mar 23, 2026
b3a0c62
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 23, 2026
17cd6a5
return 500 status code when reset call fails
miguelmartin-elastic Mar 23, 2026
5a8232e
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 24, 2026
e94d3ec
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 25, 2026
2a35b5c
add reset action to monitor list row actions menu
miguelmartin-elastic Mar 25, 2026
7016a7d
exclude non-fixable monitors from bulk reset and show skipped in modal
miguelmartin-elastic Mar 25, 2026
1e949ef
skip locations with missing agent policy during default reset
miguelmartin-elastic Mar 25, 2026
0cc68b0
rename getValidLocationIds and fix empty agentPolicyId edge case, add…
miguelmartin-elastic Mar 25, 2026
5dbf14d
convert private locations reset action to icon type for consistent ac…
miguelmartin-elastic Mar 25, 2026
ac733fe
move missing integration callout from edit page to monitor details su…
miguelmartin-elastic Mar 25, 2026
d1c76da
add configIds filter to monitor list and link unhealthy badge to filt…
miguelmartin-elastic Mar 25, 2026
764371a
align single and bulk health API error response field from 'error' to…
miguelmartin-elastic Mar 25, 2026
0a340ce
add missingAgentPolicyId
miguelmartin-elastic Mar 25, 2026
1dd1868
rename locations to privateLocations
miguelmartin-elastic Mar 25, 2026
2495f94
rename isUnhealthy to isHealthy in MonitorHealthStatus
miguelmartin-elastic Mar 25, 2026
5d6c994
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 25, 2026
6737a3c
include configIds in isMonitorsQueryFiltered to fix absoluteTotal count
miguelmartin-elastic Mar 26, 2026
1f943f3
fix Reset action enabled guards in monitor list actions column
miguelmartin-elastic Mar 26, 2026
5164a7f
add configIds to expected defaults in getSupportedUrlParams test
miguelmartin-elastic Mar 26, 2026
bb06301
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 26, 2026
48e31d5
Merge branch 'main' of https://github.com/elastic/kibana into synthet…
miguelmartin-elastic Mar 26, 2026
cdefd2d
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 26, 2026
65ee6e6
Remove break_monitors.sh test script (A8)
miguelmartin-elastic Mar 27, 2026
ed4b8d8
Add writeAccess: false to GET monitor health route (A1)
miguelmartin-elastic Mar 27, 2026
bfdf133
Add permission guards to Reset action in monitor list (A2, A3)
miguelmartin-elastic Mar 27, 2026
d151aff
Rename isResetFixable → isFixableByReset (A5, A6)
miguelmartin-elastic Mar 27, 2026
2ed2415
Remove unused getUnhealthyLocationCount function (A7)
miguelmartin-elastic Mar 27, 2026
0335af5
Fix skippedMonitors in locations_table reset action (A4)
miguelmartin-elastic Mar 27, 2026
277887e
Fix multiline rendering in unhealthy tooltip (A9)
miguelmartin-elastic Mar 27, 2026
e25f9c3
Simplify getUnhealthyMonitorsForLocation: remove unnecessary seenConf…
miguelmartin-elastic Mar 27, 2026
549360c
Improve unhealthy tooltip visual hierarchy (A9 enhancement)
miguelmartin-elastic Mar 27, 2026
ab25068
Refactor unhealthy tooltip to use EUI components instead of inline st…
miguelmartin-elastic Mar 27, 2026
0aeab27
rename explicitConfigIds to configIds
miguelmartin-elastic Mar 27, 2026
9010313
Improve error statusCode capture to include all SavedObjects error types
miguelmartin-elastic Mar 27, 2026
2fcb0e4
Improve error statusCode capture to include all SavedObjects error types
miguelmartin-elastic Mar 27, 2026
77b2a7b
Add unit tests for statusCode capture from SavedObjects errors
miguelmartin-elastic Mar 27, 2026
c6f2f77
Simplify statusCode capture: use output.statusCode with 500 fallback
miguelmartin-elastic Mar 27, 2026
3aa11ed
Update error tests to use real SavedObjects errors instead of generic…
miguelmartin-elastic Mar 27, 2026
8fa943c
Make MonitorHealthError.statusCode required and propagate it in route…
miguelmartin-elastic Mar 27, 2026
9b9d7e7
Fix reset action in private locations table not opening modal
miguelmartin-elastic Mar 27, 2026
c982ced
merge main
miguelmartin-elastic Mar 27, 2026
ec150e0
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 30, 2026
abc0346
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 30, 2026
920f6f5
[Synthetics] Fix false "Failed to reset" toast when Fleet sync errors…
miguelmartin-elastic Apr 1, 2026
1606e53
[Synthetics] Improve unhealthy tooltip styling per design feedback
miguelmartin-elastic Apr 1, 2026
ebf59fe
[Synthetics] Remove AgentPolicyMismatch and PackageNotInstalled healt…
miguelmartin-elastic Apr 1, 2026
b6abfac
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 1, 2026
88ce1bf
restructure delete action in private locations table to be icon-based…
miguelmartin-elastic Apr 1, 2026
56c3c6d
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Apr 1, 2026
a072b11
Merge remote-tracking branch 'upstream' into synthetics/missing-integ…
miguelmartin-elastic Apr 1, 2026
c3e738f
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 1, 2026
cf023a0
remove unnecessary JSON.stringify from useEffect deps
miguelmartin-elastic Apr 6, 2026
21e1b1e
run node scripts/i18n_check.js --fix
miguelmartin-elastic Apr 6, 2026
921d465
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Apr 6, 2026
5f33b51
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 6, 2026
9d0640b
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Apr 6, 2026
9275d27
fix unit test
miguelmartin-elastic Apr 6, 2026
9b9a3a9
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Apr 6, 2026
f0159d3
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 6, 2026
560c7b2
fix scout test
miguelmartin-elastic Apr 6, 2026
8f981fe
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Apr 6, 2026
7df675a
fix scout test lint issue
miguelmartin-elastic Apr 6, 2026
c96927e
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41776,7 +41776,6 @@
"xpack.synthetics.monitorManagement.createLocationMonitors": "Überwachen erstellen",
"xpack.synthetics.monitorManagement.createMonitorLabel": "Überwachen erstellen",
"xpack.synthetics.monitorManagement.createPrivateLocations": "Privaten Standort erstellen",
"xpack.synthetics.monitorManagement.deleteLocation": "Standort löschen",
"xpack.synthetics.monitorManagement.deleteLocationLabel": "Standort löschen",
"xpack.synthetics.monitorManagement.deleteLocationName": "Möchten Sie „{location}“ löschen?",
"xpack.synthetics.monitorManagement.deleteMonitorNameLabel": "Möchten Sie {monitorCount, number} ausgewählte {monitorCount, plural, one {monitor} other {Monitors}} löschen?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42206,8 +42206,7 @@
"xpack.synthetics.monitorManagement.createLocationMonitors": "Créer le moniteur",
"xpack.synthetics.monitorManagement.createMonitorLabel": "Créer le moniteur",
"xpack.synthetics.monitorManagement.createPrivateLocations": "Créer un emplacement privé",
"xpack.synthetics.monitorManagement.deleteLocation": "Supprimer l’emplacement",
"xpack.synthetics.monitorManagement.deleteLocationLabel": "Supprimer l'emplacement",
"xpack.synthetics.monitorManagement.deleteLocationLabel": "Supprimer l’emplacement",
"xpack.synthetics.monitorManagement.deleteLocationName": "Supprimer \"{location}\"",
"xpack.synthetics.monitorManagement.deleteMonitorNameLabel": "Supprimer {monitorCount, number} {monitorCount, plural, one {moniteur sélectionné} other {moniteurs sélectionnés}} ?",
"xpack.synthetics.monitorManagement.disabled.label": "Désactivé",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42268,7 +42268,6 @@
"xpack.synthetics.monitorManagement.createLocationMonitors": "監視の作成",
"xpack.synthetics.monitorManagement.createMonitorLabel": "監視の作成",
"xpack.synthetics.monitorManagement.createPrivateLocations": "非公開の場所を作成",
"xpack.synthetics.monitorManagement.deleteLocation": "場所を削除",
"xpack.synthetics.monitorManagement.deleteLocationLabel": "場所を削除",
"xpack.synthetics.monitorManagement.deleteLocationName": "「{location}」を削除",
"xpack.synthetics.monitorManagement.deleteMonitorNameLabel": "{monitorCount, number}個の選択した{monitorCount, plural, one {monitor} other {モニター}}を削除しますか?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42246,7 +42246,6 @@
"xpack.synthetics.monitorManagement.createLocationMonitors": "创建监测",
"xpack.synthetics.monitorManagement.createMonitorLabel": "创建监测",
"xpack.synthetics.monitorManagement.createPrivateLocations": "创建专用位置",
"xpack.synthetics.monitorManagement.deleteLocation": "删除位置",
"xpack.synthetics.monitorManagement.deleteLocationLabel": "删除位置",
"xpack.synthetics.monitorManagement.deleteLocationName": "删除“{location}”",
"xpack.synthetics.monitorManagement.deleteMonitorNameLabel": "删除 {monitorCount, number} 个选定的{monitorCount, plural, one {监测} other {监测}}?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export enum SYNTHETICS_API_URLS {
SERVICE_ALLOWED = '/internal/synthetics/service/allowed',
SYNTHETICS_PROJECT_APIKEY = '/internal/synthetics/service/api_key',
SYNTHETICS_HAS_INTEGRATION_MONITORS = '/internal/synthetics/fleet/has_integration_monitors',
SYNTHETICS_MONITORS_HEALTH = '/internal/synthetics/monitors/_health',
SYNTHETICS_MONITOR_HEALTH = '/internal/synthetics/monitors/{monitorId}/_health',
PRIVATE_LOCATIONS_CLEANUP = `/internal/synthetics/private_locations/_cleanup`,
SYNC_GLOBAL_PARAMS = `/internal/synthetics/sync_global_params`,
SYNC_GLOBAL_PARAMS_SETTINGS = `/internal/synthetics/sync_global_params/_settings`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './snapshot';
export * from './network_events';
export * from './monitor_management';
export * from './monitor_management/synthetics_private_locations';
export * from './monitor_health';
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.
*/

export enum PrivateLocationHealthStatusValue {
Healthy = 'healthy',
MissingPackagePolicy = 'missing_package_policy',
MissingAgentPolicy = 'missing_agent_policy',
MissingLocation = 'missing_location',
}

export interface PrivateLocationHealthStatus {
locationId: string;
locationLabel: string;
status: PrivateLocationHealthStatusValue;
packagePolicyId: string;
agentPolicyId?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it optional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agentPolicyId is optional because it's not available in all failure scenarios:

  • When the private location doesn't exist (missing_location), there's no agent policy to reference
  • When the synthetics package isn't installed (package_not_installed), policy info can't be determined

It is set in scenarios where the information is available (healthy, missing_package_policy, agent_policy_mismatch), and omitted when it's not determinable

reason?: string;
}

export interface MonitorHealthStatus {
configId: string;
monitorName: string;
isHealthy: boolean;
privateLocations: PrivateLocationHealthStatus[];
}

export interface MonitorHealthError {
configId: string;
message: string;
statusCode: number;
}

export interface MonitorsHealthResponse {
monitors: MonitorHealthStatus[];
errors: MonitorHealthError[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const FetchMonitorQueryArgsCommon = {
projects: t.array(t.string),
schedules: t.array(t.string),
monitorQueryIds: t.array(t.string),
configIds: t.array(t.string),
sortField: t.string,
sortOrder: t.union([t.literal('desc'), t.literal('asc')]),
showFromAllSpaces: t.boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { PrivateLocationHealthStatusValue } from '../../../../../../common/runtime_types';

export const STATUS_LABELS: Record<
Exclude<PrivateLocationHealthStatusValue, PrivateLocationHealthStatusValue.Healthy>,
string
> = {
[PrivateLocationHealthStatusValue.MissingPackagePolicy]: i18n.translate(
'xpack.synthetics.monitorHealth.status.missingPackagePolicy',
{
defaultMessage:
'The Fleet package policy for this monitor and private location pair does not exist.',
}
),
[PrivateLocationHealthStatusValue.MissingAgentPolicy]: i18n.translate(
'xpack.synthetics.monitorHealth.status.missingAgentPolicy',
{
defaultMessage: 'The agent policy referenced by this private location no longer exists.',
}
),
[PrivateLocationHealthStatusValue.MissingLocation]: i18n.translate(
'xpack.synthetics.monitorHealth.status.missingLocation',
{
defaultMessage: 'The monitor references a private location that no longer exists.',
}
),
};

export const getStatusLabel = (status: PrivateLocationHealthStatusValue): string | undefined => {
if (status === PrivateLocationHealthStatusValue.Healthy) return undefined;
return STATUS_LABELS[status];
};

export const RESET_FIXABLE_STATUSES = new Set([
PrivateLocationHealthStatusValue.MissingPackagePolicy,
]);

export const isFixableByResetStatus = (status: PrivateLocationHealthStatusValue): boolean =>
RESET_FIXABLE_STATUSES.has(status);
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
* 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 { renderHook, act } from '@testing-library/react';
import * as reactRedux from 'react-redux';
import { PrivateLocationHealthStatusValue } from '../../../../../../common/runtime_types';
import { useMonitorIntegrationHealth } from './use_monitor_integration_health';

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
useDispatch: jest.fn(),
}));

jest.mock('../../../state/monitor_management/api', () => ({
resetMonitorAPI: jest.fn(),
resetMonitorBulkAPI: jest.fn(),
}));

import { resetMonitorAPI, resetMonitorBulkAPI } from '../../../state/monitor_management/api';

const mockedResetMonitorAPI = resetMonitorAPI as jest.MockedFunction<typeof resetMonitorAPI>;
const mockedResetMonitorBulkAPI = resetMonitorBulkAPI as jest.MockedFunction<
typeof resetMonitorBulkAPI
>;

const healthyMonitor = {
configId: 'mon-1',
monitorName: 'Monitor 1',
isHealthy: true,
privateLocations: [
{
locationId: 'loc-1',
locationLabel: 'Location 1',
status: PrivateLocationHealthStatusValue.Healthy,
packagePolicyId: 'mon-1-loc-1',
},
],
};

const unhealthyMonitor = {
configId: 'mon-2',
monitorName: 'Monitor 2',
isHealthy: false,
privateLocations: [
{
locationId: 'loc-1',
locationLabel: 'Location 1',
status: PrivateLocationHealthStatusValue.MissingPackagePolicy,
packagePolicyId: 'mon-2-loc-1',
reason: 'Missing',
},
{
locationId: 'loc-2',
locationLabel: 'Location 2',
status: PrivateLocationHealthStatusValue.Healthy,
packagePolicyId: 'mon-2-loc-2',
},
],
};

const setupSelectors = (healthData: { monitors: (typeof healthyMonitor)[]; errors: unknown[] }) => {
(reactRedux.useSelector as jest.Mock).mockImplementation((selector: any) => {
const fakeState = {
monitorList: {
data: { monitors: [] },
loaded: true,
loading: false,
},
monitorHealth: {
data: healthData,
loading: false,
loaded: true,
error: null,
},
};
return selector(fakeState);
});
};

describe('useMonitorIntegrationHealth', () => {
let dispatchSpy: jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
dispatchSpy = jest.fn();
(reactRedux.useDispatch as jest.Mock).mockReturnValue(dispatchSpy);
});

describe('status helpers', () => {
it('isUnhealthy returns true for unhealthy monitors', () => {
setupSelectors({ monitors: [healthyMonitor, unhealthyMonitor], errors: [] });

const { result } = renderHook(() =>
useMonitorIntegrationHealth({ configIds: ['mon-1', 'mon-2'] })
);

expect(result.current.isUnhealthy('mon-1')).toBe(false);
expect(result.current.isUnhealthy('mon-2')).toBe(true);
expect(result.current.isUnhealthy('non-existent')).toBe(false);
});

it('getUnhealthyLocationStatuses returns only unhealthy locations', () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });

const { result } = renderHook(() => useMonitorIntegrationHealth({ configIds: ['mon-2'] }));

const statuses = result.current.getUnhealthyLocationStatuses('mon-2');
expect(statuses).toHaveLength(1);
expect(statuses[0].locationId).toBe('loc-1');
expect(statuses[0].status).toBe(PrivateLocationHealthStatusValue.MissingPackagePolicy);
});

it('getUnhealthyMonitorCountForLocation counts monitors with unhealthy status at that location', () => {
setupSelectors({ monitors: [healthyMonitor, unhealthyMonitor], errors: [] });

const { result } = renderHook(() =>
useMonitorIntegrationHealth({ configIds: ['mon-1', 'mon-2'] })
);

expect(result.current.getUnhealthyMonitorCountForLocation('loc-1')).toBe(1);
expect(result.current.getUnhealthyMonitorCountForLocation('loc-2')).toBe(0);
});

it('getUnhealthyConfigIdsForLocation returns config IDs of unhealthy monitors', () => {
setupSelectors({ monitors: [healthyMonitor, unhealthyMonitor], errors: [] });

const { result } = renderHook(() =>
useMonitorIntegrationHealth({ configIds: ['mon-1', 'mon-2'] })
);

expect(result.current.getUnhealthyConfigIdsForLocation('loc-1')).toEqual(['mon-2']);
});
});

describe('resetMonitor', () => {
it('calls resetMonitorAPI and re-fetches health', async () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });
mockedResetMonitorAPI.mockResolvedValue({ id: 'mon-2', reset: true });

const { result } = renderHook(() => useMonitorIntegrationHealth({ configIds: ['mon-2'] }));

let resetResult: { error?: Error } | undefined;
await act(async () => {
resetResult = await result.current.resetMonitor('mon-2');
});

expect(resetResult).toEqual({});
expect(mockedResetMonitorAPI).toHaveBeenCalledWith({ id: 'mon-2' });
expect(result.current.isResetting).toBe(false);
const healthDispatches = dispatchSpy.mock.calls.filter(
([action]: any) => action.type === '[MONITOR HEALTH] GET'
);
expect(healthDispatches.length).toBeGreaterThanOrEqual(2);
});

it('returns error and sets isResetting to false on API failure', async () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });
mockedResetMonitorAPI.mockRejectedValue(new Error('Server error'));

const { result } = renderHook(() => useMonitorIntegrationHealth({ configIds: ['mon-2'] }));

let resetResult: { error?: Error } | undefined;
await act(async () => {
resetResult = await result.current.resetMonitor('mon-2');
});

expect(resetResult?.error).toBeInstanceOf(Error);
expect(resetResult?.error?.message).toBe('Server error');
expect(result.current.isResetting).toBe(false);
});
});

describe('resetMonitors (bulk)', () => {
it('returns error and does not refetch when a result item has reset: false', async () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });
mockedResetMonitorBulkAPI.mockResolvedValue({
result: [{ id: 'mon-2', reset: false, error: 'fleet error' }],
});

const { result } = renderHook(() => useMonitorIntegrationHealth({ configIds: ['mon-2'] }));

let resetResult: { error?: Error } | undefined;
await act(async () => {
resetResult = await result.current.resetMonitors(['mon-2']);
});

expect(resetResult?.error).toBeInstanceOf(Error);
expect(result.current.isResetting).toBe(false);
const healthDispatches = dispatchSpy.mock.calls.filter(
([action]: any) => action.type === '[MONITOR HEALTH] GET'
);
expect(healthDispatches.length).toBe(1); // only initial fetch, no refetch
});

it('returns error and does not refetch when top-level errors are present', async () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });
mockedResetMonitorBulkAPI.mockResolvedValue({
result: [{ id: 'mon-2', reset: false }],
});

const { result } = renderHook(() => useMonitorIntegrationHealth({ configIds: ['mon-2'] }));

let resetResult: { error?: Error } | undefined;
await act(async () => {
resetResult = await result.current.resetMonitors(['mon-2']);
});

expect(resetResult?.error).toBeInstanceOf(Error);
expect(result.current.isResetting).toBe(false);
});

it('calls resetMonitorBulkAPI and re-fetches health', async () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });
mockedResetMonitorBulkAPI.mockResolvedValue({
result: [{ id: 'mon-2', reset: true }],
});

const { result } = renderHook(() => useMonitorIntegrationHealth({ configIds: ['mon-2'] }));

let resetResult: { error?: Error } | undefined;
await act(async () => {
resetResult = await result.current.resetMonitors(['mon-2']);
});

expect(resetResult).toEqual({});
expect(mockedResetMonitorBulkAPI).toHaveBeenCalledWith({ ids: ['mon-2'] });
expect(result.current.isResetting).toBe(false);
});
});
});
Loading
Loading