mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Synthetics] Remove Saved Object Client usage from use_recently_viewed_monitors
hook (#159033)
Relates to #https://github.com/elastic/kibana/issues/153399 ## Summary Removes the deprecated Saved Object Client dependency from `useRecentlyViewedMonitors` hook. - [`getAllSyntheticsMonitorRoute`](https://github.com/elastic/kibana/pull/159033/files#diff-ee80010902693c9786b3043c6ec4e2f42a62fc33e55f9c7666bbf7034fe6d165R34 ) will now accept additional filter `monitorQueryIds` which will only query the saved objects for the provided Monitor Query IDs. - The PR also refactors the [`useMonitorName`](https://github.com/elastic/kibana/pull/159033/files#diff-c066d4869d55ec490d753a0febd925eeb12778836edd3603481128bdc51142feR30) to fix a bug with name duplication case on Monitor Add/Edit where the form was allowing the duplicate name to be saved. Also, since `useMonitorName` was using [`useMonitorList`](https://github.com/elastic/kibana/pull/159033/files#diff-f4f40e819338bf8c22f9c060e4941576f9659d3c7a206130884554b776d4b606R22) which wasn't returning the correct "loading" state for a query search due to `quietFetchMonitorListAction`, `useMonitorName` is refactored to utilize the `fetchMonitorManagementList` to have its own data and "loading" flag. The `query` parameter from `useMonitorList` is removed as `useMonitorName` was the only consumer.
This commit is contained in:
parent
6383f45ed0
commit
ff5b71c10d
16 changed files with 453 additions and 224 deletions
|
@ -354,12 +354,6 @@ export const HeartbeatConfigCodec = t.intersection([
|
|||
}),
|
||||
]);
|
||||
|
||||
export const EncryptedSyntheticsMonitorWithIdCodec = t.intersection([
|
||||
EncryptedSyntheticsMonitorCodec,
|
||||
t.interface({ id: t.string }),
|
||||
]);
|
||||
|
||||
// TODO: Remove EncryptedSyntheticsMonitorWithIdCodec (as well as SyntheticsMonitorWithIdCodec if possible) along with respective TypeScript types in favor of EncryptedSyntheticsSavedMonitorCodec
|
||||
export const EncryptedSyntheticsSavedMonitorCodec = t.intersection([
|
||||
EncryptedSyntheticsMonitorCodec,
|
||||
t.interface({ id: t.string, updated_at: t.string, created_at: t.string }),
|
||||
|
@ -367,10 +361,6 @@ export const EncryptedSyntheticsSavedMonitorCodec = t.intersection([
|
|||
|
||||
export type SyntheticsMonitorWithId = t.TypeOf<typeof SyntheticsMonitorWithIdCodec>;
|
||||
|
||||
export type EncryptedSyntheticsMonitorWithId = t.TypeOf<
|
||||
typeof EncryptedSyntheticsMonitorWithIdCodec
|
||||
>;
|
||||
|
||||
export type EncryptedSyntheticsSavedMonitor = t.TypeOf<typeof EncryptedSyntheticsSavedMonitorCodec>;
|
||||
|
||||
export type HeartbeatConfig = t.TypeOf<typeof HeartbeatConfigCodec>;
|
||||
|
|
|
@ -19,6 +19,7 @@ export const FetchMonitorManagementListQueryArgsCodec = t.partial({
|
|||
monitorTypes: t.array(t.string),
|
||||
projects: t.array(t.string),
|
||||
schedules: t.array(t.string),
|
||||
monitorQueryIds: t.array(t.string),
|
||||
});
|
||||
|
||||
export type FetchMonitorManagementListQueryArgs = t.TypeOf<
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { mockGlobals } from '../../utils/testing';
|
||||
import { render } from '../../utils/testing/rtl_helpers';
|
||||
import { MonitorEditPage } from './monitor_edit_page';
|
||||
import { useMonitorName } from '../../hooks/use_monitor_name';
|
||||
import { ConfigKey } from '../../../../../common/runtime_types';
|
||||
|
||||
import * as observabilitySharedPublic from '@kbn/observability-shared-plugin/public';
|
||||
|
@ -21,6 +22,11 @@ mockGlobals();
|
|||
|
||||
jest.mock('@kbn/observability-shared-plugin/public');
|
||||
|
||||
jest.mock('../../hooks/use_monitor_name', () => ({
|
||||
...jest.requireActual('../../hooks/use_monitor_name'),
|
||||
useMonitorName: jest.fn().mockReturnValue({ nameAlreadyExists: false }),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => {
|
||||
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
|
||||
return {
|
||||
|
@ -191,4 +197,49 @@ describe('MonitorEditPage', () => {
|
|||
// error
|
||||
expect(getByText('Unable to load monitor configuration')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
'shows duplicate error when "nameAlreadyExists" is %s',
|
||||
(nameAlreadyExists) => {
|
||||
(useMonitorName as jest.Mock).mockReturnValue({ nameAlreadyExists });
|
||||
|
||||
jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
data: {
|
||||
attributes: {
|
||||
[ConfigKey.MONITOR_SOURCE_TYPE]: 'ui',
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: 'multistep',
|
||||
[ConfigKey.LOCATIONS]: [],
|
||||
[ConfigKey.THROTTLING_CONFIG]: PROFILES_MAP[PROFILE_VALUES_ENUM.DEFAULT],
|
||||
},
|
||||
},
|
||||
refetch: () => null,
|
||||
loading: false,
|
||||
});
|
||||
const { getByText, queryByText } = render(<MonitorEditPage />, {
|
||||
state: {
|
||||
serviceLocations: {
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central',
|
||||
label: 'Us Central',
|
||||
},
|
||||
{
|
||||
id: 'us_east',
|
||||
label: 'US East',
|
||||
},
|
||||
],
|
||||
locationsLoaded: true,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (nameAlreadyExists) {
|
||||
expect(getByText('Monitor name already exists')).toBeInTheDocument();
|
||||
} else {
|
||||
expect(queryByText('Monitor name already exists')).not.toBeInTheDocument();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ type MonitorOption = EuiSelectableOption & {
|
|||
|
||||
export const MonitorSearchableList = ({ closePopover }: { closePopover: () => void }) => {
|
||||
const history = useHistory();
|
||||
const recentlyViewed = useRecentlyViewedMonitors();
|
||||
const { recentMonitorOptions, loading: recentMonitorsLoading } = useRecentlyViewedMonitors();
|
||||
|
||||
const [options, setOptions] = useState<MonitorOption[]>([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
@ -37,13 +37,13 @@ export const MonitorSearchableList = ({ closePopover }: { closePopover: () => vo
|
|||
|
||||
const { basePath } = useSyntheticsSettingsContext();
|
||||
|
||||
const { values, loading } = useMonitorName({ search: searchValue });
|
||||
const { values, loading: searchLoading } = useMonitorName({ search: searchValue });
|
||||
|
||||
useEffect(() => {
|
||||
const newOptions: MonitorOption[] = [];
|
||||
if (recentlyViewed.length > 0 && !searchValue) {
|
||||
if (recentMonitorOptions.length > 0 && !searchValue) {
|
||||
const otherMonitors = values.filter((value) =>
|
||||
recentlyViewed.every((recent) => recent.key !== value.key)
|
||||
recentMonitorOptions.every((recent) => recent.key !== value.key)
|
||||
) as MonitorOption[];
|
||||
|
||||
if (otherMonitors.length > 0) {
|
||||
|
@ -55,11 +55,11 @@ export const MonitorSearchableList = ({ closePopover }: { closePopover: () => vo
|
|||
});
|
||||
}
|
||||
|
||||
setOptions([...recentlyViewed, ...newOptions, ...otherMonitors]);
|
||||
setOptions([...recentMonitorOptions, ...newOptions, ...otherMonitors]);
|
||||
} else {
|
||||
setOptions(values);
|
||||
}
|
||||
}, [recentlyViewed, searchValue, values]);
|
||||
}, [recentMonitorOptions, searchValue, values]);
|
||||
|
||||
const getLocationId = (option: MonitorOption) => {
|
||||
if (option.locationIds?.includes(selectedLocation?.id ?? '')) {
|
||||
|
@ -71,7 +71,7 @@ export const MonitorSearchableList = ({ closePopover }: { closePopover: () => vo
|
|||
return (
|
||||
<EuiSelectable<MonitorOption>
|
||||
searchable
|
||||
isLoading={loading}
|
||||
isLoading={searchLoading || recentMonitorsLoading}
|
||||
searchProps={{
|
||||
placeholder: PLACEHOLDER,
|
||||
compressed: true,
|
||||
|
@ -108,7 +108,7 @@ export const MonitorSearchableList = ({ closePopover }: { closePopover: () => vo
|
|||
{(list, search) => (
|
||||
<div style={{ width: 280 }}>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{options.length > 0 || searchValue ? (
|
||||
{options.length > 0 || searchValue || searchLoading || recentMonitorsLoading ? (
|
||||
search
|
||||
) : (
|
||||
<EuiText color="subdued" size="s" className="eui-textCenter">
|
||||
|
|
|
@ -4,34 +4,39 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import type { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
|
||||
|
||||
import { useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
import * as localStorageModule from 'react-use/lib/useLocalStorage';
|
||||
import { fetchMonitorManagementList } from '../../../state';
|
||||
|
||||
import * as useMonitorQueryModule from '../hooks/use_monitor_query_id';
|
||||
import { useRecentlyViewedMonitors } from './use_recently_viewed_monitors';
|
||||
import { mockCore, WrappedHelper } from '../../../utils/testing';
|
||||
import { syntheticsMonitorType } from '../../../../../../common/types/saved_objects';
|
||||
import { WrappedHelper } from '../../../utils/testing';
|
||||
import { MONITOR_ROUTE } from '../../../../../../common/constants';
|
||||
|
||||
const resultData = {
|
||||
resolved_objects: [
|
||||
{
|
||||
saved_object: {
|
||||
id: 'c9322230-2a11-11ed-962b-d3e7eeedf9d1',
|
||||
jest.mock('../../../state', () => ({
|
||||
...jest.requireActual('../../../state'),
|
||||
fetchMonitorManagementList: jest.fn(),
|
||||
}));
|
||||
|
||||
attributes: {
|
||||
name: 'Test Monitor',
|
||||
locations: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
jest.mock('@kbn/observability-shared-plugin/public', () => ({
|
||||
...jest.requireActual('@kbn/observability-shared-plugin/public'),
|
||||
useFetcher: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useRecentlyViewedMonitors', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns expected result', () => {
|
||||
const WrapperWithState = ({ children }: { children: React.ReactElement }) => {
|
||||
return (
|
||||
|
@ -41,48 +46,83 @@ describe('useRecentlyViewedMonitors', () => {
|
|||
);
|
||||
};
|
||||
|
||||
jest.spyOn(useMonitorQueryModule, 'useMonitorQueryId').mockImplementation(() => '1');
|
||||
(useFetcher as jest.Mock).mockImplementation((callback) => {
|
||||
callback();
|
||||
return { loading: false, status: 'success' as FETCH_STATUS.SUCCESS, refetch: () => {} };
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecentlyViewedMonitors(), { wrapper: WrapperWithState });
|
||||
expect(result.current).toEqual([]);
|
||||
expect(result.current).toEqual({ loading: false, recentMonitorOptions: [] });
|
||||
});
|
||||
|
||||
it('returns the result when found', async () => {
|
||||
const core = mockCore();
|
||||
it('fetches the persisted ids and persists the updated information', async () => {
|
||||
const currentMonitorQueryId = 'id-01';
|
||||
const monitorQueryId3 = 'persisted-id-03';
|
||||
let persistedIds = ['persisted-id-02', monitorQueryId3];
|
||||
const setPersistedIdsMock = jest.fn().mockImplementation((ids: string[]) => {
|
||||
persistedIds = ids;
|
||||
});
|
||||
|
||||
core.savedObjects!.client.bulkResolve = jest.fn().mockResolvedValue(resultData);
|
||||
jest
|
||||
.spyOn(useMonitorQueryModule, 'useMonitorQueryId')
|
||||
.mockImplementation(() => currentMonitorQueryId);
|
||||
|
||||
jest
|
||||
.spyOn(localStorageModule, 'default')
|
||||
.mockImplementation(() => [persistedIds, setPersistedIdsMock, () => {}]);
|
||||
|
||||
(useFetcher as jest.Mock).mockImplementation((callback) => {
|
||||
callback();
|
||||
return { loading: false, status: 'success' as FETCH_STATUS.SUCCESS, refetch: () => {} };
|
||||
});
|
||||
|
||||
// Return only 'persisted-id-03' to mark 'persisted-id-02' as a deleted monitor
|
||||
const fetchedMonitor = {
|
||||
id: 'uuid-monitor-03',
|
||||
attributes: {
|
||||
id: monitorQueryId3,
|
||||
name: 'Monitor 03',
|
||||
locations: [],
|
||||
},
|
||||
};
|
||||
(fetchMonitorManagementList as jest.Mock).mockReturnValue({
|
||||
monitors: [fetchedMonitor],
|
||||
});
|
||||
|
||||
const WrapperWithState = ({ children }: { children: React.ReactElement }) => {
|
||||
return (
|
||||
<WrappedHelper core={core} url="/monitor/1" path={MONITOR_ROUTE}>
|
||||
<WrappedHelper url="/monitor/1" path={MONITOR_ROUTE}>
|
||||
{children}
|
||||
</WrappedHelper>
|
||||
);
|
||||
};
|
||||
const { result, waitForValueToChange, rerender } = renderHook(
|
||||
() => useRecentlyViewedMonitors(),
|
||||
{
|
||||
wrapper: WrapperWithState,
|
||||
}
|
||||
);
|
||||
await waitForValueToChange(() => persistedIds);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useRecentlyViewedMonitors(), {
|
||||
wrapper: WrapperWithState,
|
||||
});
|
||||
expect(result.current).toEqual([]);
|
||||
// Sets the current monitor as well as updated information
|
||||
expect(setPersistedIdsMock).toHaveBeenCalledWith([currentMonitorQueryId, monitorQueryId3]);
|
||||
|
||||
expect(core.savedObjects?.client.bulkResolve).toHaveBeenCalledTimes(1);
|
||||
expect(core.savedObjects?.client.bulkResolve).toHaveBeenLastCalledWith([
|
||||
{ id: '1', type: syntheticsMonitorType },
|
||||
]);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
isGroupLabel: true,
|
||||
key: 'recently_viewed',
|
||||
label: 'Recently viewed',
|
||||
},
|
||||
{
|
||||
key: 'c9322230-2a11-11ed-962b-d3e7eeedf9d1',
|
||||
label: 'Test Monitor',
|
||||
locationIds: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
rerender();
|
||||
const expectedOptions = [
|
||||
{
|
||||
isGroupLabel: true,
|
||||
key: 'recently_viewed',
|
||||
label: 'Recently viewed',
|
||||
},
|
||||
{
|
||||
isGroupLabel: false,
|
||||
key: fetchedMonitor.id,
|
||||
label: fetchedMonitor.attributes.name,
|
||||
locationIds: fetchedMonitor.attributes.locations,
|
||||
monitorQueryId: monitorQueryId3,
|
||||
},
|
||||
];
|
||||
expect(result.current).toEqual({ loading: false, recentMonitorOptions: expectedOptions });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,75 +5,109 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
import { MonitorFields } from '../../../../../../common/runtime_types';
|
||||
import { syntheticsMonitorType } from '../../../../../../common/types/saved_objects';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { fetchMonitorManagementList, getMonitorListPageStateWithDefaults } from '../../../state';
|
||||
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
|
||||
|
||||
const HISTORY_LENGTH = 5;
|
||||
|
||||
interface RecentMonitorSelectableOption {
|
||||
key: string;
|
||||
monitorQueryId: string;
|
||||
label: string;
|
||||
locationIds: string[];
|
||||
isGroupLabel: boolean;
|
||||
}
|
||||
|
||||
export const useRecentlyViewedMonitors = () => {
|
||||
const [recentlyViewed, setRecentlyViewed] = useLocalStorage<string[]>(
|
||||
'xpack.synthetics.recentlyViewedMonitors',
|
||||
[]
|
||||
);
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
const [recentlyViewedMonitorQueryIds, setRecentlyViewedMonitorQueryIds] = useLocalStorage<
|
||||
string[]
|
||||
>('xpack.synthetics.recentlyViewedMonitors', []);
|
||||
const fetchedMonitorsRef = useRef<RecentMonitorSelectableOption[]>([]);
|
||||
const monitorQueryId = useMonitorQueryId();
|
||||
|
||||
const { savedObjects } = useKibana().services;
|
||||
const fetchedMonitorQueryIdsSnap = JSON.stringify(
|
||||
[...fetchedMonitorsRef.current.map(({ key }) => key)].sort()
|
||||
);
|
||||
|
||||
const updateRecentlyViewed = useCallback(() => {
|
||||
const updatedIdsToPersist = fetchedMonitorsRef.current.length
|
||||
? fetchedMonitorsRef.current.map(({ monitorQueryId: id }) => id)
|
||||
: recentlyViewedMonitorQueryIds ?? [];
|
||||
|
||||
if (monitorQueryId) {
|
||||
setRecentlyViewedMonitorQueryIds(
|
||||
[monitorQueryId, ...updatedIdsToPersist]
|
||||
.filter((id, index, arr) => arr.indexOf(id) === index)
|
||||
.slice(0, HISTORY_LENGTH)
|
||||
);
|
||||
}
|
||||
// Exclude `recentlyViewedMonitorQueryIds`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setRecentlyViewedMonitorQueryIds, monitorQueryId, fetchedMonitorQueryIdsSnap]);
|
||||
|
||||
useEffect(() => {
|
||||
const newRecentlyViewed = [
|
||||
...new Set([...(monitorId ? [monitorId] : []), ...(recentlyViewed ?? [])]),
|
||||
].slice(0, 5);
|
||||
updateRecentlyViewed();
|
||||
}, [updateRecentlyViewed, monitorQueryId]);
|
||||
|
||||
if (
|
||||
newRecentlyViewed?.[0] !== recentlyViewed?.[0] ||
|
||||
newRecentlyViewed.length !== recentlyViewed?.length
|
||||
) {
|
||||
setRecentlyViewed(newRecentlyViewed);
|
||||
}
|
||||
}, [monitorId, recentlyViewed, setRecentlyViewed]);
|
||||
|
||||
const { data } = useFetcher(async () => {
|
||||
const monitorsList = recentlyViewed ?? [];
|
||||
|
||||
const { resolved_objects: monitorObjects } = await savedObjects!.client.bulkResolve(
|
||||
monitorsList.map((monId) => ({
|
||||
type: syntheticsMonitorType,
|
||||
id: monId,
|
||||
}))
|
||||
const { loading } = useFetcher(async () => {
|
||||
const monitorQueryIdsToFetch = (recentlyViewedMonitorQueryIds ?? []).filter(
|
||||
(id) => id !== monitorQueryId
|
||||
);
|
||||
if (
|
||||
monitorQueryId &&
|
||||
monitorQueryIdsToFetch.length &&
|
||||
JSON.stringify([...monitorQueryIdsToFetch].sort()) !== fetchedMonitorQueryIdsSnap
|
||||
) {
|
||||
const fetchedResult = await fetchMonitorManagementList(
|
||||
getMonitorListPageStateWithDefaults({
|
||||
pageSize: HISTORY_LENGTH,
|
||||
monitorQueryIds: monitorQueryIdsToFetch,
|
||||
})
|
||||
);
|
||||
|
||||
const missingMonitors = monitorObjects
|
||||
.filter((mon) => mon.saved_object.error?.statusCode === 404)
|
||||
.map((mon) => mon.saved_object.id);
|
||||
if (fetchedResult?.monitors?.length) {
|
||||
const persistedOrderHash = monitorQueryIdsToFetch.reduce(
|
||||
(acc, cur, index) => ({ ...acc, [cur]: index }),
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
if (missingMonitors.length > 0) {
|
||||
setRecentlyViewed(monitorsList.filter((monId) => !missingMonitors.includes(monId)));
|
||||
// Reorder fetched monitors as per the persisted order
|
||||
const fetchedMonitorsInPersistedOrder = [...fetchedResult?.monitors].sort(
|
||||
(a, b) => persistedOrderHash[a.attributes.id] - persistedOrderHash[b.attributes.id]
|
||||
);
|
||||
fetchedMonitorsRef.current = fetchedMonitorsInPersistedOrder.map((mon) => {
|
||||
return {
|
||||
key: mon.id,
|
||||
monitorQueryId: mon.attributes.id,
|
||||
label: mon.attributes.name,
|
||||
locationIds: (mon.attributes.locations ?? []).map(({ id }) => id),
|
||||
isGroupLabel: false,
|
||||
};
|
||||
});
|
||||
|
||||
updateRecentlyViewed();
|
||||
}
|
||||
}
|
||||
}, [monitorQueryId]);
|
||||
|
||||
return monitorObjects
|
||||
.filter(
|
||||
({ saved_object: monitor }) => Boolean(monitor.attributes) && monitor.id !== monitorId
|
||||
)
|
||||
.map(({ saved_object: monitor }) => ({
|
||||
key: monitor.id,
|
||||
label: (monitor.attributes as MonitorFields).name,
|
||||
locationIds: (monitor.attributes as MonitorFields).locations.map((location) => location.id),
|
||||
}));
|
||||
}, [monitorId, recentlyViewed]);
|
||||
|
||||
return useMemo(() => {
|
||||
if ((data ?? []).length === 0) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{ key: 'recently_viewed', label: RECENTLY_VIEWED, isGroupLabel: true },
|
||||
...(data ?? []),
|
||||
];
|
||||
}, [data]);
|
||||
return useMemo(
|
||||
() => ({
|
||||
loading,
|
||||
recentMonitorOptions: fetchedMonitorsRef.current.length
|
||||
? [
|
||||
{ key: 'recently_viewed', label: RECENTLY_VIEWED, isGroupLabel: true },
|
||||
...fetchedMonitorsRef.current,
|
||||
]
|
||||
: [],
|
||||
}),
|
||||
// Make it also depend on `fetchedMonitorQueryIdsSnap`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[loading, fetchedMonitorQueryIdsSnap]
|
||||
);
|
||||
};
|
||||
|
||||
const RECENTLY_VIEWED = i18n.translate('xpack.synthetics.monitorSummary.recentlyViewed', {
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '../../../state';
|
||||
import { useSyntheticsRefreshContext } from '../../../contexts';
|
||||
|
||||
export function useMonitorList(query?: string) {
|
||||
export function useMonitorList() {
|
||||
const dispatch = useDispatch();
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
|
@ -40,11 +40,11 @@ export function useMonitorList(query?: string) {
|
|||
// Periodically refresh
|
||||
useEffect(() => {
|
||||
if (!isInitialMount.current) {
|
||||
dispatch(quietFetchMonitorListAction({ ...pageState, query }));
|
||||
dispatch(quietFetchMonitorListAction({ ...pageState }));
|
||||
}
|
||||
// specifically only want to run this on refreshInterval change on query change
|
||||
// specifically only want to run this on refreshInterval change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lastRefresh, query]);
|
||||
}, [lastRefresh]);
|
||||
|
||||
// On initial mount, load the page
|
||||
useDebounce(
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { WrappedHelper } from '../utils/testing/rtl_helpers';
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { fetchMonitorManagementList } from '../state';
|
||||
import { useMonitorName } from './use_monitor_name';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
|
@ -14,66 +14,62 @@ jest.mock('react-router-dom', () => ({
|
|||
useParams: jest.fn().mockReturnValue({ monitorId: '12345' }),
|
||||
}));
|
||||
|
||||
describe('useMonitorName', () => {
|
||||
const Wrapper = ({ children }: { children: React.ReactElement }) => {
|
||||
return (
|
||||
<WrappedHelper
|
||||
state={{
|
||||
monitorList: {
|
||||
error: null,
|
||||
loading: true,
|
||||
loaded: false,
|
||||
monitorUpsertStatuses: {},
|
||||
data: {
|
||||
absoluteTotal: 1,
|
||||
perPage: 5,
|
||||
page: 1,
|
||||
total: 1,
|
||||
monitors: [
|
||||
{
|
||||
attributes: {
|
||||
name: 'Test monitor name',
|
||||
config_id: '12345',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central_qa',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
name: 'Test monitor name 2',
|
||||
config_id: '12346',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central_qa',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
syncErrors: [],
|
||||
},
|
||||
pageState: {
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
sortOrder: 'asc',
|
||||
sortField: `name.keyword`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WrappedHelper>
|
||||
);
|
||||
};
|
||||
jest.mock('../state', () => ({
|
||||
...jest.requireActual('../state'),
|
||||
fetchMonitorManagementList: jest.fn(),
|
||||
}));
|
||||
|
||||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useMonitorName({}), { wrapper: Wrapper });
|
||||
describe('useMonitorName', () => {
|
||||
const testMonitors = [
|
||||
{
|
||||
attributes: {
|
||||
name: 'Test monitor name',
|
||||
config_id: '12345',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central_qa',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
name: 'Test monitor name 2',
|
||||
config_id: '12346',
|
||||
locations: [
|
||||
{
|
||||
id: 'us_central_qa',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(fetchMonitorManagementList as jest.Mock).mockResolvedValue({
|
||||
monitors: testMonitors,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns expected initial and after load state', async () => {
|
||||
const { result, waitForValueToChange } = renderHook(() => useMonitorName({}));
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
loading: true,
|
||||
values: [],
|
||||
nameAlreadyExists: false,
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.values);
|
||||
|
||||
expect(result.current).toStrictEqual({
|
||||
loading: false,
|
||||
values: [
|
||||
{
|
||||
key: '12346',
|
||||
|
@ -82,19 +78,18 @@ describe('useMonitorName', () => {
|
|||
},
|
||||
],
|
||||
nameAlreadyExists: false,
|
||||
validName: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns expected results after data', async () => {
|
||||
const { result } = renderHook(() => useMonitorName({ search: 'Test monitor name 2' }), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
it('returns correct "nameAlreadyExists" when name matches', async () => {
|
||||
const { result, waitForValueToChange } = renderHook(() =>
|
||||
useMonitorName({ search: 'Test monitor name 2' })
|
||||
);
|
||||
|
||||
await waitForValueToChange(() => result.current.values); // Wait until data has been loaded
|
||||
expect(result.current).toStrictEqual({
|
||||
loading: true,
|
||||
nameAlreadyExists: false,
|
||||
validName: 'Test monitor name 2',
|
||||
loading: false,
|
||||
nameAlreadyExists: true,
|
||||
values: [
|
||||
{
|
||||
key: '12346',
|
||||
|
@ -106,14 +101,14 @@ describe('useMonitorName', () => {
|
|||
});
|
||||
|
||||
it('returns expected results after data while editing monitor', async () => {
|
||||
const { result } = renderHook(() => useMonitorName({ search: 'Test monitor name' }), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
const { result, waitForValueToChange } = renderHook(() =>
|
||||
useMonitorName({ search: 'Test monitor name' })
|
||||
);
|
||||
|
||||
await waitForValueToChange(() => result.current.values); // Wait until data has been loaded
|
||||
expect(result.current).toStrictEqual({
|
||||
loading: true,
|
||||
nameAlreadyExists: false,
|
||||
validName: 'Test monitor name',
|
||||
loading: false,
|
||||
nameAlreadyExists: false, // Should be `false` for the currently editing monitor,
|
||||
values: [
|
||||
{
|
||||
key: '12346',
|
||||
|
|
|
@ -4,33 +4,46 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMonitorList } from '../components/monitors_page/hooks/use_monitor_list';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
|
||||
import { fetchMonitorManagementList, getMonitorListPageStateWithDefaults } from '../state';
|
||||
|
||||
export const useMonitorName = ({ search = '' }: { search?: string }) => {
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
|
||||
const { syntheticsMonitors, loading } = useMonitorList(search);
|
||||
const [debouncedSearch, setDebouncedSearch] = useState<string>(search);
|
||||
useDebounce(() => setDebouncedSearch(search), 500, [search]);
|
||||
|
||||
const { loading, data: monitors } = useFetcher(async () => {
|
||||
const fetchedResult = await fetchMonitorManagementList(
|
||||
getMonitorListPageStateWithDefaults({ query: debouncedSearch })
|
||||
);
|
||||
|
||||
return (fetchedResult?.monitors ?? []).map((monitor) => ({
|
||||
label: monitor.attributes.name,
|
||||
key: monitor.attributes.config_id,
|
||||
locationIds: monitor.attributes.locations.map((location) => location.id),
|
||||
}));
|
||||
}, [debouncedSearch]);
|
||||
|
||||
return useMemo(() => {
|
||||
const searchPattern = search.replace(/\s/g, '').toLowerCase();
|
||||
const nameAlreadyExists = Boolean(
|
||||
syntheticsMonitors?.some(
|
||||
(monitor) => monitor.name.trim().toLowerCase() === search && monitorId !== monitor.config_id
|
||||
(monitors ?? []).some(
|
||||
(monitor) =>
|
||||
monitorId !== monitor.key &&
|
||||
monitor.label.replace(/\s/g, '').toLowerCase() === searchPattern
|
||||
)
|
||||
);
|
||||
|
||||
const values = syntheticsMonitors.map((monitor) => ({
|
||||
label: monitor.name as string,
|
||||
key: monitor.config_id,
|
||||
locationIds: monitor.locations.map((location) => location.id),
|
||||
}));
|
||||
|
||||
return {
|
||||
loading,
|
||||
loading: loading || debouncedSearch !== search, // Also keep busy while waiting for debounce
|
||||
nameAlreadyExists,
|
||||
validName: nameAlreadyExists ? '' : search,
|
||||
values: values.filter((val) => val.key !== monitorId),
|
||||
values: (monitors ?? []).filter((val) => val.key !== monitorId),
|
||||
};
|
||||
}, [loading, monitorId, syntheticsMonitors, search]);
|
||||
}, [loading, monitorId, monitors, search, debouncedSearch]);
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ function toMonitorManagementListQueryArgs(
|
|||
monitorTypes: pageState.monitorTypes,
|
||||
projects: pageState.projects,
|
||||
schedules: pageState.schedules,
|
||||
monitorQueryIds: pageState.monitorQueryIds,
|
||||
searchFields: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigKey } from '../../../../../common/constants/monitor_management';
|
||||
import { MonitorListPageState } from './models';
|
||||
|
||||
const DEFAULT_PAGE_STATE: MonitorListPageState = {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
sortOrder: 'asc',
|
||||
sortField: `${ConfigKey.NAME}.keyword`,
|
||||
};
|
||||
|
||||
export function getMonitorListPageStateWithDefaults(override?: Partial<MonitorListPageState>) {
|
||||
return {
|
||||
...DEFAULT_PAGE_STATE,
|
||||
...(override ?? {}),
|
||||
};
|
||||
}
|
|
@ -10,7 +10,6 @@ import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
|
|||
|
||||
import { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import {
|
||||
ConfigKey,
|
||||
MonitorManagementListResult,
|
||||
SyntheticsMonitor,
|
||||
MonitorFiltersResult,
|
||||
|
@ -19,6 +18,8 @@ import {
|
|||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
||||
import { MonitorListPageState } from './models';
|
||||
import { getMonitorListPageStateWithDefaults } from './helpers';
|
||||
|
||||
import {
|
||||
cleanMonitorListState,
|
||||
clearMonitorUpsertStatus,
|
||||
|
@ -47,12 +48,7 @@ export interface MonitorListState {
|
|||
const initialState: MonitorListState = {
|
||||
data: { page: 1, perPage: 10, total: null, monitors: [], syncErrors: [], absoluteTotal: 0 },
|
||||
monitorUpsertStatuses: {},
|
||||
pageState: {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
sortOrder: 'asc',
|
||||
sortField: `${ConfigKey.NAME}.keyword`,
|
||||
},
|
||||
pageState: getMonitorListPageStateWithDefaults(),
|
||||
loading: false,
|
||||
loaded: false,
|
||||
error: null,
|
||||
|
@ -139,4 +135,5 @@ export * from './models';
|
|||
export * from './actions';
|
||||
export * from './effects';
|
||||
export * from './selectors';
|
||||
export * from './helpers';
|
||||
export { fetchDeleteMonitor, fetchUpsertMonitor, fetchCreateMonitor } from './api';
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface MonitorFilterState {
|
|||
projects?: string[];
|
||||
schedules?: string[];
|
||||
locations?: string[];
|
||||
monitorQueryIds?: string[]; // Monitor Query IDs
|
||||
}
|
||||
|
||||
export interface MonitorListPageState extends MonitorFilterState {
|
||||
|
|
|
@ -10,7 +10,10 @@ import { invert } from 'lodash';
|
|||
import { DataStream, ServiceLocations } from '../../../../../common/runtime_types';
|
||||
import { MonitorFilterState } from '../../state';
|
||||
|
||||
export type SyntheticsMonitorFilterField = keyof Omit<MonitorFilterState, 'query'>;
|
||||
export type SyntheticsMonitorFilterField = keyof Omit<
|
||||
MonitorFilterState,
|
||||
'query' | 'monitorQueryIds'
|
||||
>;
|
||||
|
||||
export interface LabelWithCountValue {
|
||||
label: string;
|
||||
|
|
|
@ -31,6 +31,7 @@ export const QuerySchema = schema.object({
|
|||
schedules: StringOrArraySchema,
|
||||
status: StringOrArraySchema,
|
||||
searchAfter: schema.maybe(schema.arrayOf(schema.string())),
|
||||
monitorQueryIds: StringOrArraySchema,
|
||||
});
|
||||
|
||||
export type MonitorsQuery = TypeOf<typeof QuerySchema>;
|
||||
|
@ -76,6 +77,7 @@ export const getMonitors = async (
|
|||
searchAfter,
|
||||
projects,
|
||||
schedules,
|
||||
monitorQueryIds,
|
||||
} = context.request.query;
|
||||
|
||||
const filterStr = await getMonitorFilters({
|
||||
|
@ -85,10 +87,11 @@ export const getMonitors = async (
|
|||
locations,
|
||||
projects,
|
||||
schedules,
|
||||
monitorQueryIds,
|
||||
context,
|
||||
});
|
||||
|
||||
return context.savedObjectsClient.find({
|
||||
const findParams = {
|
||||
type: syntheticsMonitorType,
|
||||
perPage,
|
||||
page,
|
||||
|
@ -99,7 +102,9 @@ export const getMonitors = async (
|
|||
filter: filterStr,
|
||||
searchAfter,
|
||||
fields,
|
||||
});
|
||||
};
|
||||
|
||||
return context.savedObjectsClient.find(findParams);
|
||||
};
|
||||
|
||||
export const getMonitorFilters = async ({
|
||||
|
@ -109,6 +114,7 @@ export const getMonitorFilters = async ({
|
|||
projects,
|
||||
monitorTypes,
|
||||
schedules,
|
||||
monitorQueryIds,
|
||||
context,
|
||||
}: {
|
||||
filter?: string;
|
||||
|
@ -117,6 +123,7 @@ export const getMonitorFilters = async ({
|
|||
locations?: string | string[];
|
||||
projects?: string | string[];
|
||||
schedules?: string | string[];
|
||||
monitorQueryIds?: string | string[];
|
||||
context: RouteContext;
|
||||
}) => {
|
||||
const locationFilter = await parseLocationFilter(context, locations);
|
||||
|
@ -128,6 +135,7 @@ export const getMonitorFilters = async ({
|
|||
getKqlFilter({ field: 'type', values: monitorTypes }),
|
||||
getKqlFilter({ field: 'locations.id', values: locationFilter }),
|
||||
getKqlFilter({ field: 'schedule.number', values: schedules }),
|
||||
getKqlFilter({ field: 'id', values: monitorQueryIds }),
|
||||
]
|
||||
.filter((f) => !!f)
|
||||
.join(' AND ');
|
||||
|
@ -187,8 +195,17 @@ export const findLocationItem = (query: string, locations: ServiceLocations) =>
|
|||
* @param monitorQuery { MonitorsQuery }
|
||||
*/
|
||||
export const isMonitorsQueryFiltered = (monitorQuery: MonitorsQuery) => {
|
||||
const { query, tags, monitorTypes, locations, status, filter, projects, schedules } =
|
||||
monitorQuery;
|
||||
const {
|
||||
query,
|
||||
tags,
|
||||
monitorTypes,
|
||||
locations,
|
||||
status,
|
||||
filter,
|
||||
projects,
|
||||
schedules,
|
||||
monitorQueryIds,
|
||||
} = monitorQuery;
|
||||
|
||||
return (
|
||||
!!query ||
|
||||
|
@ -198,7 +215,8 @@ export const isMonitorsQueryFiltered = (monitorQuery: MonitorsQuery) => {
|
|||
!!tags?.length ||
|
||||
!!status?.length ||
|
||||
!!projects?.length ||
|
||||
!!schedules?.length
|
||||
!!schedules?.length ||
|
||||
!!monitorQueryIds?.length
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -83,6 +83,68 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(firstPageResp.body.monitors[0].id).not.eql(secondPageResp.body.monitors[0].id);
|
||||
});
|
||||
|
||||
it('with single monitorQueryId filter', async () => {
|
||||
const [_, { id: id2 }] = await Promise.all(monitors.map(saveMonitor));
|
||||
|
||||
const resp = await supertest
|
||||
.get(`${API_URLS.SYNTHETICS_MONITORS}?page=1&perPage=10&monitorQueryIds=${id2}`)
|
||||
.expect(200);
|
||||
|
||||
const resultMonitorIds = resp.body.monitors.map(
|
||||
({ attributes: { id } }: { attributes: Partial<MonitorFields> }) => id
|
||||
);
|
||||
expect(resultMonitorIds.length).eql(1);
|
||||
expect(resultMonitorIds).eql([id2]);
|
||||
});
|
||||
|
||||
it('with multiple monitorQueryId filter', async () => {
|
||||
const [_, { id: id2 }, { id: id3 }] = await Promise.all(monitors.map(saveMonitor));
|
||||
|
||||
const resp = await supertest
|
||||
.get(
|
||||
`${API_URLS.SYNTHETICS_MONITORS}?page=1&perPage=10&sortField=name.keyword&sortOrder=asc&monitorQueryIds=${id2}&monitorQueryIds=${id3}`
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const resultMonitorIds = resp.body.monitors.map(
|
||||
({ attributes: { id } }: { attributes: Partial<MonitorFields> }) => id
|
||||
);
|
||||
|
||||
expect(resultMonitorIds.length).eql(2);
|
||||
expect(resultMonitorIds).eql([id2, id3]);
|
||||
});
|
||||
|
||||
it('monitorQueryId respects custom_heartbeat_id while filtering', async () => {
|
||||
const customHeartbeatId0 = 'custom-heartbeat-id-test-01';
|
||||
const customHeartbeatId1 = 'custom-heartbeat-id-test-02';
|
||||
await Promise.all(
|
||||
[
|
||||
{
|
||||
...monitors[0],
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: customHeartbeatId0,
|
||||
[ConfigKey.NAME]: `NAME-${customHeartbeatId0}`,
|
||||
},
|
||||
{
|
||||
...monitors[1],
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: customHeartbeatId1,
|
||||
[ConfigKey.NAME]: `NAME-${customHeartbeatId1}`,
|
||||
},
|
||||
].map(saveMonitor)
|
||||
);
|
||||
|
||||
const resp = await supertest
|
||||
.get(
|
||||
`${API_URLS.SYNTHETICS_MONITORS}?page=1&perPage=10&sortField=name.keyword&sortOrder=asc&monitorQueryIds=${customHeartbeatId0}&monitorQueryIds=${customHeartbeatId1}`
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const resultMonitorIds = resp.body.monitors
|
||||
.map(({ attributes: { id } }: { attributes: Partial<MonitorFields> }) => id)
|
||||
.filter((id: string, index: number, arr: string[]) => arr.indexOf(id) === index); // Filter only unique
|
||||
expect(resultMonitorIds.length).eql(2);
|
||||
expect(resultMonitorIds).eql([customHeartbeatId0, customHeartbeatId1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get one monitor', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue