Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c3643b1
[CCR] Rename JS/JSX files to TS/TSX and remove .d.ts stubs
kapral18 Apr 10, 2026
5e0dc80
[CCR] Type API service requests and responses
kapral18 Apr 9, 2026
dfb8d77
[CCR] Introduce typed HTTP error helpers
kapral18 Apr 9, 2026
283dd85
[CCR] Type store actions and reducer payloads
kapral18 Apr 9, 2026
032bd50
[CCR] Tighten routing and utility typings
kapral18 Apr 9, 2026
afe87c2
[CCR] Harden error rendering and provider error typing
kapral18 Apr 9, 2026
e169c68
[CCR] Tighten follower index form typing
kapral18 Apr 9, 2026
ff50832
[CCR] Tighten auto-follow pattern form typing
kapral18 Apr 9, 2026
bc63792
[CCR] Type edit/add pages for CCR resources
kapral18 Apr 9, 2026
2ad62fb
[CCR] Tighten home lists and action menu typing
kapral18 Apr 9, 2026
08976e4
[CCR] Align remaining services and store plumbing
kapral18 Apr 9, 2026
4b8e05e
[CCR] Tighten client integration test helpers
kapral18 Apr 9, 2026
f24f4ff
[CCR] Remove PropTypes and align snapshots
kapral18 Apr 9, 2026
6035e5a
Changes from node scripts/lint_ts_projects --fix
kibanamachine Apr 10, 2026
08b2e6a
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Apr 10, 2026
c92b45d
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 10, 2026
4cdf38d
[CCR] Split auto-follow pattern save thunk
kapral18 Apr 17, 2026
2e5a960
[CCR] Simplify section_error message expression
kapral18 Apr 19, 2026
6bc37bc
[CCR] Type arrayToObject reducer accumulator explicitly
kapral18 Apr 19, 2026
c81bf59
[CCR] Drop unused defaultProps from renderWithRouter
kapral18 Apr 19, 2026
57c4096
[CCR] Document why undefined counts as a default in isSettingDefault
kapral18 Apr 19, 2026
d9cf18f
[CCR] Reuse RemoteClusterRow type in getRemoteClusterName
kapral18 Apr 19, 2026
3e8af65
[CCR] Make FormEntryRow.title required and drop title! assertion
kapral18 Apr 19, 2026
62a83a5
[CCR] Drop FollowerIndex shards from edit form prop
kapral18 Apr 19, 2026
ba46ea7
[CCR] Type AutoFollowPatternTable EUI props natively
kapral18 Apr 19, 2026
b39da96
[CCR] Type FollowerIndicesTable EUI props natively
kapral18 Apr 19, 2026
e852508
[CCR] Expose reactRouterOrThrow and use it from consumers
kapral18 Apr 19, 2026
6aeebd9
[CCR] Type common utils generically and drop dead wait helper
kapral18 Apr 19, 2026
804fb3f
[CCR] Type AutoFollowPatternActionMenu container connect explicitly
kapral18 Apr 19, 2026
ed40c97
[CCR] Throw on uninitialized CCR notifications instead of definite as…
kapral18 Apr 19, 2026
a7982c1
[CCR] Refactor FormEntryRow callback to scalar value
kapral18 Apr 19, 2026
3a10872
[CCR] Spread resume params at the top level of ccr.resumeFollow
kapral18 Apr 19, 2026
e131578
Merge branch 'main' into chore/ccr/migrate-to-ts-239613
kapral18 Apr 19, 2026
7138f59
Merge branch 'main' into chore/ccr/migrate-to-ts-239613
kapral18 Apr 22, 2026
255381e
Merge branch 'main' into chore/ccr/migrate-to-ts-239613
kapral18 Apr 27, 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 @@ -37,7 +37,10 @@ export const serializeAutoFollowPattern = ({
remoteCluster,
leaderIndexPatterns,
followIndexPattern,
}: AutoFollowPattern): AutoFollowPatternToEs => ({
}: Pick<
AutoFollowPattern,
'remoteCluster' | 'leaderIndexPatterns' | 'followIndexPattern'
>): AutoFollowPatternToEs => ({
remote_cluster: remoteCluster,
leader_index_patterns: leaderIndexPatterns,
follow_index_pattern: followIndexPattern,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ export const serializeAdvancedSettings = ({
read_poll_timeout: readPollTimeout,
});

export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({
export const serializeFollowerIndex = (
followerIndex: Pick<FollowerIndex, 'remoteCluster' | 'leaderIndex'> &
FollowerIndexAdvancedSettings
): FollowerIndexToEs => ({
remote_cluster: followerIndex.remoteCluster,
leader_index: followerIndex.leaderIndex,
...serializeAdvancedSettings(followerIndex),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,15 @@
* 2.0.
*/

export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]);
export const arrify = <T>(val: T | T[]): T[] => (Array.isArray(val) ? val : [val]);

/**
* Utilty to add some latency in a Promise chain
*
* @param {number} time Time in millisecond to wait
* Utility to remove empty fields ("") from a request body.
*/
export const wait =
(time = 1000) =>
(data: any): Promise<any> => {
return new Promise((resolve) => {
setTimeout(() => resolve(data), time);
});
};

/**
* Utility to remove empty fields ("") from a request body
*/
export const removeEmptyFields = (body: Record<string, any>): Record<string, any> =>
Object.entries(body).reduce(
(acc: Record<string, any>, [key, value]: [string, any]): Record<string, any> => {
if (value !== '') {
acc[key] = value;
}
return acc;
},
{}
);
export const removeEmptyFields = <T extends object>(body: T): Partial<T> =>
Object.entries(body).reduce((acc: Partial<T>, [key, value]): Partial<T> => {
if (value !== '') {
acc[key as keyof T] = value;
}
return acc;
}, {});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependsOn:
- '@kbn/licensing-types'
- '@kbn/data-views-plugin'
- '@kbn/index-management-shared-types'
- '@kbn/core-http-browser'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@

import { ILLEGAL_CHARACTERS_VISIBLE } from '@kbn/data-views-plugin/public';
import { fireEvent, screen, within, act } from '@testing-library/react';
import type { UserEvent } from '@testing-library/user-event';
import './mocks';
import { setupEnvironment, pageHelpers, getRandomString } from './helpers';

const { setup } = pageHelpers.autoFollowPatternAdd;

type SetupEnvironmentReturn = ReturnType<typeof setupEnvironment>;

describe('Create Auto-follow pattern', () => {
let httpRequestsMockHelpers;
let httpSetup;
let user;
let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
let httpSetup: SetupEnvironmentReturn['httpSetup'];
let user: UserEvent;

beforeAll(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -80,6 +83,96 @@ describe('Create Auto-follow pattern', () => {
});
});

describe('when the form is filled', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadRemoteClustersResponse([
{ name: 'my_remote', isConnected: true },
]);
({ user } = setup());
await act(async () => {
await jest.runOnlyPendingTimersAsync();
});
});

test('submitting a filled form should not surface required errors for filled fields', async () => {
// `AutoFollowPatternForm.onFieldsChange` calls `validateAutoFollowPattern`
// with a fixed object literal where untouched fields are present with
// value `undefined`. The validator must only evaluate fields whose values
// are explicitly provided; skipping `undefined` fields is what lets users
// fill the form incrementally without every still-empty field clobbering
// the valid state of fields they have already filled. Clicking Create
// flips `areErrorsVisible: true`, which is the only way a stomped error
// would become renderable, so the click is required to make the
// invariant observable.
const nameInput = screen.getByTestId('nameInput');
await user.type(nameInput, 'test1');

const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
await user.type(input, 'my*{enter}');

// Keep the save HTTP request pending so the submit path does not unmount
// the form before we can assert on the visible error state.
httpSetup.post.mockImplementation(() => new Promise(() => {}));

const saveButton = screen.getByTestId('submitButton');
await user.click(saveButton);

expect(screen.queryByTestId('formError')).not.toBeInTheDocument();
expect(screen.queryByText('Name is required.')).not.toBeInTheDocument();
expect(
screen.queryByText('At least one leader index pattern is required.')
).not.toBeInTheDocument();
});

test('mutating one filled field after a failed submit should not stomp validation state of other filled fields', async () => {
// Once `areErrorsVisible: true` is latched (see the preceding test), any
// edit to a single field drives `onFieldsChange` with an object literal
// in which the *other* fields are present with value `undefined`. The
// validator must treat that as "not touched, do not re-validate" rather
// than "empty, report as required"; otherwise an edit to Name can stomp
// the valid state of Leader index patterns and render a phantom
// "required" error under an already-filled combobox.
const nameInput = screen.getByTestId('nameInput');
await user.type(nameInput, 'test1');

const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
await user.type(input, 'my*{enter}');

// Keep the save HTTP request pending so the happy-path submit does not
// unmount the form before we mutate Name again. Without this, the
// not-in-document assertion below could pass trivially because the form
// navigated away.
httpSetup.post.mockImplementation(() => new Promise(() => {}));

const saveButton = screen.getByTestId('submitButton');
await user.click(saveButton);

// Mutate only the Name field. `fireEvent.change` writes the new value
// directly so we don't have to model cursor positioning inside an
// EuiFieldText.
fireEvent.change(nameInput, { target: { value: 'test' } });

// The leader index patterns combobox was not touched and still renders
// its ["my*"] tag; its error state must be left alone.
expect(screen.getByTestId('indexPatternInput').textContent).toContain('my*');
expect(
screen.queryByText('At least one leader index pattern is required.')
).not.toBeInTheDocument();
});
});

describe('form validation', () => {
describe('auto-follow pattern name', () => {
beforeEach(async () => {
Expand Down Expand Up @@ -178,7 +271,7 @@ describe('Create Auto-follow pattern', () => {
const errorCallOut = screen.getByTestId('notConnectedError');
const title = errorCallOut.querySelector('.euiCallOutHeader__title');

expect(title.textContent).toBe(`Remote cluster '${clusterName}' is not connected`);
expect(title?.textContent).toBe(`Remote cluster '${clusterName}' is not connected`);

const editButton = within(errorCallOut).getByTestId('editButton');
expect(editButton).toBeInTheDocument();
Expand All @@ -194,7 +287,7 @@ describe('Create Auto-follow pattern', () => {
test('should indicate in the select option that the cluster is not connected', () => {
const select = screen.getByTestId('remoteClusterSelect');
const option = select.querySelector('option');
expect(option.textContent).toBe(`${clusterName} (not connected)`);
expect(option?.textContent).toBe(`${clusterName} (not connected)`);
});
});
});
Expand All @@ -214,8 +307,11 @@ describe('Create Auto-follow pattern', () => {
test('should not allow spaces', () => {
const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ||
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
fireEvent.change(input, { target: { value: 'with space' } });
fireEvent.blur(input);

Expand All @@ -226,11 +322,14 @@ describe('Create Auto-follow pattern', () => {

test.each(ILLEGAL_CHARACTERS_VISIBLE)(
'should not allow invalid character %s',
(illegalChar) => {
(illegalChar: string) => {
const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ||
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}

fireEvent.change(input, { target: { value: `legalchar` } });
fireEvent.blur(input);
Expand Down Expand Up @@ -261,8 +360,11 @@ describe('Create Auto-follow pattern', () => {
// The combobox should already be rendered after beforeEach timer advancement
const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ||
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
fireEvent.change(input, { target: { value: 'kibana' } });
fireEvent.blur(input);

Expand All @@ -272,8 +374,11 @@ describe('Create Auto-follow pattern', () => {
test('should display 3 indices example when providing a wildcard(*)', () => {
const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ||
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
fireEvent.change(input, { target: { value: 'kibana-*' } });
fireEvent.blur(input);

Expand All @@ -286,8 +391,11 @@ describe('Create Auto-follow pattern', () => {
test('should only display 1 index example when *not* providing a wildcard', () => {
const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ||
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
fireEvent.change(input, { target: { value: 'kibana' } });
fireEvent.blur(input);

Expand All @@ -303,8 +411,11 @@ describe('Create Auto-follow pattern', () => {

const comboboxWrapper = screen.getByTestId('indexPatternInput');
const input =
comboboxWrapper.querySelector('[role="combobox"]') ||
comboboxWrapper.querySelector('[role="combobox"]') ??
comboboxWrapper.querySelector('input');
if (!input) {
throw new Error('expected index pattern input');
}
fireEvent.change(input, { target: { value: 'kibana' } });
fireEvent.blur(input);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@
*/

import { screen, within, act } from '@testing-library/react';
import type { UserEvent } from '@testing-library/user-event';
import './mocks';
import { setupEnvironment, pageHelpers } from './helpers';
import { AUTO_FOLLOW_PATTERN_EDIT, AUTO_FOLLOW_PATTERN_EDIT_NAME } from './helpers/constants';
import { API_BASE_PATH } from '../../../common/constants';

const { setup } = pageHelpers.autoFollowPatternEdit;

type SetupEnvironmentReturn = ReturnType<typeof setupEnvironment>;

describe('Edit Auto-follow pattern', () => {
let httpRequestsMockHelpers;
let user;
let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
let httpSetup: SetupEnvironmentReturn['httpSetup'];
let user: UserEvent;

beforeAll(() => {
jest.useFakeTimers();
Expand All @@ -26,7 +31,7 @@ describe('Edit Auto-follow pattern', () => {

beforeEach(() => {
jest.clearAllMocks();
({ httpRequestsMockHelpers } = setupEnvironment());
({ httpRequestsMockHelpers, httpSetup } = setupEnvironment());
httpRequestsMockHelpers.setGetAutoFollowPatternResponse(
AUTO_FOLLOW_PATTERN_EDIT_NAME,
AUTO_FOLLOW_PATTERN_EDIT
Expand Down Expand Up @@ -74,6 +79,50 @@ describe('Edit Auto-follow pattern', () => {
expect(screen.getByTestId('prefixInput')).toHaveValue('prefix_');
expect(screen.getByTestId('suffixInput')).toHaveValue('_suffix');
});

test('should PUT only the fields accepted by the update route body schema', async () => {
// The update route's bodySchema forbids unknown properties, so leaking
// extra fields (e.g. `name`, `errors`) from the form state into the
// dispatched payload would produce a 400 at the server boundary. The
// container's mapDispatchToProps whitelists the allowed fields before
// dispatching the save thunk; this test pins that contract.
const putEndpoint = `${API_BASE_PATH}/auto_follow_patterns/${AUTO_FOLLOW_PATTERN_EDIT_NAME}`;
// Make sure the PUT resolves so the save flow can complete without noise.
httpSetup.put.mockImplementation((path) => {
const resolved = typeof path === 'string' ? path : path.path;
if (resolved === putEndpoint) {
return Promise.resolve({});
}
return Promise.resolve({});
});

const submitButton = screen.getByTestId('submitButton');
await user.click(submitButton);

await act(async () => {
await jest.runOnlyPendingTimersAsync();
});

type PutCall = [string | { path: string }, { body?: string } | undefined];
const putCalls = httpSetup.put.mock.calls as unknown as PutCall[];
const putCall = putCalls.find(([path]) => {
const resolved = typeof path === 'string' ? path : path.path;
return resolved === putEndpoint;
});

expect(putCall).toBeDefined();
const [, options] = putCall!;
const rawBody = options?.body;
expect(typeof rawBody).toBe('string');
const body = JSON.parse(rawBody as string);

expect(body).toEqual({
active: AUTO_FOLLOW_PATTERN_EDIT.active,
remoteCluster: AUTO_FOLLOW_PATTERN_EDIT.remoteCluster,
leaderIndexPatterns: AUTO_FOLLOW_PATTERN_EDIT.leaderIndexPatterns,
followIndexPattern: AUTO_FOLLOW_PATTERN_EDIT.followIndexPattern,
});
});
});

describe('when the remote cluster is disconnected', () => {
Expand All @@ -93,7 +142,7 @@ describe('Edit Auto-follow pattern', () => {
expect(error).toBeInTheDocument();

const title = error.querySelector('.euiCallOutHeader__title');
expect(title.textContent).toBe(
expect(title?.textContent).toBe(
`Can't edit auto-follow pattern because remote cluster '${AUTO_FOLLOW_PATTERN_EDIT.remoteCluster}' is not connected`
);

Expand Down
Loading
Loading