Skip to content

Commit 0a43928

Browse files
committed
Self review
1 parent 8e38a06 commit 0a43928

13 files changed

Lines changed: 816 additions & 344 deletions

src/platform/plugins/shared/workflows_management/public/features/run_workflow/ui/test_utils/workflow_form_test_setup.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ const createMockSearchHitsResponse = (
118118
},
119119
});
120120

121+
export const mockFieldFormatter = {
122+
convert: jest.fn((value: unknown) => ({ text: String(value ?? '') })),
123+
convertToReact: jest.fn((value: unknown) => String(value ?? '')),
124+
};
125+
121126
/**
122127
* Creates mock Kibana services for event form tests
123128
*/
@@ -126,9 +131,7 @@ export const createEventFormKibanaMocks = () => {
126131
getActiveSpace: jest.fn().mockResolvedValue({ id: 'default' }),
127132
};
128133

129-
const mockFormatter = {
130-
convert: jest.fn((value: unknown) => ({ text: String(value ?? '') })),
131-
};
134+
const mockFormatter = mockFieldFormatter;
132135

133136
const mockDataView = {
134137
id: 'test-data-view',
@@ -211,9 +214,7 @@ export const createEventFormKibanaMocks = () => {
211214
* Creates mock Kibana services for index form tests
212215
*/
213216
export const createIndexFormKibanaMocks = () => {
214-
const mockFormatter = {
215-
convert: jest.fn((value: unknown) => ({ text: String(value ?? '') })),
216-
};
217+
const mockFormatter = mockFieldFormatter;
217218

218219
const createMockDataView = () => ({
219220
id: 'test-data-view-id',
@@ -264,11 +265,7 @@ export const createIndexFormKibanaMocks = () => {
264265
search: {
265266
search: jest.fn().mockReturnValue({
266267
pipe: jest.fn().mockReturnValue({
267-
subscribe: jest.fn(({ next, complete }) => {
268-
next(createMockSearchHitsResponse(mockDocumentHits));
269-
complete();
270-
return { unsubscribe: jest.fn() };
271-
}),
268+
toPromise: jest.fn().mockResolvedValue(createMockSearchHitsResponse(mockDocumentHits)),
272269
}),
273270
}),
274271
},
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { act, renderHook, waitFor } from '@testing-library/react';
11+
import type { EsHitRecord } from '@kbn/discover-utils/types';
12+
import {
13+
buildWorkflowExecuteHitSearchIdentityKey,
14+
useWorkflowExecuteHitSearch,
15+
} from './use_workflow_execute_hit_search';
16+
17+
const hit: EsHitRecord = { _id: '1', _index: 'logs-*', _source: { message: 'hello' } };
18+
19+
describe('buildWorkflowExecuteHitSearchIdentityKey', () => {
20+
it('includes data view, query, time range, and sort', () => {
21+
expect(
22+
buildWorkflowExecuteHitSearchIdentityKey({
23+
dataViewId: 'dv-1',
24+
submittedQueryString: 'message: test',
25+
timeRange: { from: 'now-15m', to: 'now' },
26+
tableSort: [['@timestamp', 'desc']],
27+
})
28+
).toBe('dv-1|message: test|now-15m|now|@timestamp:desc');
29+
});
30+
});
31+
32+
describe('useWorkflowExecuteHitSearch', () => {
33+
const baseOptions = {
34+
enabled: true,
35+
searchIdentityKey: 'identity-1',
36+
setErrors: jest.fn(),
37+
resolveFetchError: () => 'fetch failed',
38+
};
39+
40+
it('fetches the first page when enabled', async () => {
41+
const fetchPage = jest.fn().mockResolvedValue({ pageHits: [hit], total: 1 });
42+
43+
const { result } = renderHook(() =>
44+
useWorkflowExecuteHitSearch({
45+
...baseOptions,
46+
fetchPage,
47+
})
48+
);
49+
50+
await waitFor(() => {
51+
expect(result.current.hits).toEqual([hit]);
52+
});
53+
54+
expect(fetchPage).toHaveBeenCalledWith(0);
55+
expect(result.current.totalHits).toBe(1);
56+
expect(baseOptions.setErrors).toHaveBeenCalledWith(null);
57+
});
58+
59+
it('resets results and refetches when the search identity changes', async () => {
60+
const fetchPage = jest
61+
.fn()
62+
.mockResolvedValueOnce({ pageHits: [hit], total: 1 })
63+
.mockResolvedValueOnce({
64+
pageHits: [{ _id: '2', _index: 'logs-*', _source: { message: 'next' } }],
65+
total: 1,
66+
});
67+
68+
const { result, rerender } = renderHook(
69+
({ searchIdentityKey }: { searchIdentityKey: string }) =>
70+
useWorkflowExecuteHitSearch({
71+
...baseOptions,
72+
searchIdentityKey,
73+
fetchPage,
74+
}),
75+
{ initialProps: { searchIdentityKey: 'identity-1' } }
76+
);
77+
78+
await waitFor(() => {
79+
expect(result.current.hits).toEqual([hit]);
80+
});
81+
82+
rerender({ searchIdentityKey: 'identity-2' });
83+
84+
await waitFor(() => {
85+
expect(result.current.hits).toEqual([
86+
{ _id: '2', _index: 'logs-*', _source: { message: 'next' } },
87+
]);
88+
});
89+
90+
expect(fetchPage).toHaveBeenLastCalledWith(0);
91+
});
92+
93+
it('loads the next page when onFetchMoreRecords is invoked', async () => {
94+
const secondHit: EsHitRecord = { _id: '2', _index: 'logs-*', _source: { message: 'more' } };
95+
const fetchPage = jest
96+
.fn()
97+
.mockResolvedValueOnce({ pageHits: [hit], total: 2 })
98+
.mockResolvedValueOnce({ pageHits: [secondHit], total: 2 });
99+
100+
const { result } = renderHook(() =>
101+
useWorkflowExecuteHitSearch({
102+
...baseOptions,
103+
fetchPage,
104+
})
105+
);
106+
107+
await waitFor(() => {
108+
expect(result.current.onFetchMoreRecords).toBeDefined();
109+
});
110+
111+
act(() => {
112+
result.current.onFetchMoreRecords?.();
113+
});
114+
115+
await waitFor(() => {
116+
expect(result.current.hits).toEqual([hit, secondHit]);
117+
});
118+
119+
expect(fetchPage).toHaveBeenLastCalledWith(1);
120+
});
121+
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { useCallback, useEffect, useMemo, useState } from 'react';
11+
import type { EsHitRecord } from '@kbn/discover-utils/types';
12+
import type { TimeRange } from '@kbn/es-query';
13+
import type { SortOrder } from '@kbn/unified-data-table';
14+
import {
15+
mergeEsHitPages,
16+
resolveHitSearchHasMoreHits,
17+
resolveHitSearchTableLoadingState,
18+
} from './workflow_execute_hit_search_pagination';
19+
import { serializeWorkflowExecuteHitSortOrder } from './workflow_execute_hit_search_sort';
20+
21+
export interface WorkflowExecuteHitSearchPageResult {
22+
pageHits: EsHitRecord[];
23+
total: number;
24+
}
25+
26+
export interface UseWorkflowExecuteHitSearchOptions {
27+
enabled: boolean;
28+
searchIdentityKey: string;
29+
fetchPage: (pageIndex: number) => Promise<WorkflowExecuteHitSearchPageResult>;
30+
setErrors: (errors: string | null) => void;
31+
resolveFetchError: (error: unknown) => string;
32+
}
33+
34+
export function buildWorkflowExecuteHitSearchIdentityKey(params: {
35+
dataViewId: string | undefined;
36+
submittedQueryString: string;
37+
timeRange: TimeRange;
38+
tableSort: SortOrder[];
39+
}): string {
40+
const sortKey = serializeWorkflowExecuteHitSortOrder(params.tableSort);
41+
return `${params.dataViewId ?? ''}|${params.submittedQueryString}|${params.timeRange.from}|${
42+
params.timeRange.to
43+
}|${sortKey}`;
44+
}
45+
46+
export function useWorkflowExecuteHitSearch({
47+
enabled,
48+
searchIdentityKey,
49+
fetchPage,
50+
setErrors,
51+
resolveFetchError,
52+
}: UseWorkflowExecuteHitSearchOptions) {
53+
const [hits, setHits] = useState<EsHitRecord[]>([]);
54+
const [pageIndex, setPageIndex] = useState(0);
55+
const [totalHits, setTotalHits] = useState(0);
56+
const [lastPageHitsLength, setLastPageHitsLength] = useState(0);
57+
const [isFetching, setIsFetching] = useState(false);
58+
59+
useEffect(() => {
60+
setPageIndex(0);
61+
setHits([]);
62+
setTotalHits(0);
63+
setLastPageHitsLength(0);
64+
}, [searchIdentityKey]);
65+
66+
useEffect(() => {
67+
if (!enabled) {
68+
return;
69+
}
70+
71+
let cancelled = false;
72+
73+
const runFetch = async () => {
74+
setIsFetching(true);
75+
setErrors(null);
76+
77+
try {
78+
const { pageHits, total } = await fetchPage(pageIndex);
79+
if (cancelled) {
80+
return;
81+
}
82+
83+
setTotalHits(total);
84+
setLastPageHitsLength(pageHits.length);
85+
setHits((previousHits) => mergeEsHitPages(previousHits, pageHits, pageIndex));
86+
} catch (error) {
87+
if (cancelled) {
88+
return;
89+
}
90+
91+
setErrors(resolveFetchError(error));
92+
if (pageIndex === 0) {
93+
setHits([]);
94+
setTotalHits(0);
95+
setLastPageHitsLength(0);
96+
}
97+
} finally {
98+
if (!cancelled) {
99+
setIsFetching(false);
100+
}
101+
}
102+
};
103+
104+
void runFetch();
105+
106+
return () => {
107+
cancelled = true;
108+
};
109+
}, [enabled, searchIdentityKey, pageIndex, fetchPage, resolveFetchError, setErrors]);
110+
111+
const resetPageIndex = useCallback(() => {
112+
setPageIndex(0);
113+
}, []);
114+
115+
const hasMoreHits = resolveHitSearchHasMoreHits({
116+
totalHits,
117+
accumulatedHitsLength: hits.length,
118+
currentPageHitsLength: lastPageHitsLength,
119+
});
120+
121+
const onFetchMoreRecords = useMemo(
122+
() => (hasMoreHits && !isFetching ? () => setPageIndex((page) => page + 1) : undefined),
123+
[hasMoreHits, isFetching]
124+
);
125+
126+
const tableLoadingState = resolveHitSearchTableLoadingState(isFetching, hits.length);
127+
128+
return {
129+
hits,
130+
totalHits,
131+
isFetching,
132+
tableLoadingState,
133+
onFetchMoreRecords,
134+
resetPageIndex,
135+
};
136+
}

0 commit comments

Comments
 (0)