diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts b/x-pack/platform/plugins/private/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts
index 67d365d2f68f8..7450ee3b39b9e 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts
@@ -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,
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/common/services/follower_index_serialization.ts b/x-pack/platform/plugins/private/cross_cluster_replication/common/services/follower_index_serialization.ts
index afe46a4183bf2..9f4deabf68d99 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/common/services/follower_index_serialization.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/common/services/follower_index_serialization.ts
@@ -136,7 +136,10 @@ export const serializeAdvancedSettings = ({
read_poll_timeout: readPollTimeout,
});
-export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({
+export const serializeFollowerIndex = (
+ followerIndex: Pick &
+ FollowerIndexAdvancedSettings
+): FollowerIndexToEs => ({
remote_cluster: followerIndex.remoteCluster,
leader_index: followerIndex.leaderIndex,
...serializeAdvancedSettings(followerIndex),
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/common/services/utils.ts b/x-pack/platform/plugins/private/cross_cluster_replication/common/services/utils.ts
index 900d8c9046efd..88ecead6b2514 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/common/services/utils.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/common/services/utils.ts
@@ -5,31 +5,15 @@
* 2.0.
*/
-export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]);
+export const arrify = (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 => {
- return new Promise((resolve) => {
- setTimeout(() => resolve(data), time);
- });
- };
-
-/**
- * Utility to remove empty fields ("") from a request body
- */
-export const removeEmptyFields = (body: Record): Record =>
- Object.entries(body).reduce(
- (acc: Record, [key, value]: [string, any]): Record => {
- if (value !== '') {
- acc[key] = value;
- }
- return acc;
- },
- {}
- );
+export const removeEmptyFields = (body: T): Partial =>
+ Object.entries(body).reduce((acc: Partial, [key, value]): Partial => {
+ if (value !== '') {
+ acc[key as keyof T] = value;
+ }
+ return acc;
+ }, {});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml b/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml
index 2a8cc33d7ff07..d277b103369f2 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml
@@ -38,6 +38,7 @@ dependsOn:
- '@kbn/licensing-types'
- '@kbn/data-views-plugin'
- '@kbn/index-management-shared-types'
+ - '@kbn/core-http-browser'
tags:
- plugin
- prod
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.ts
similarity index 66%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.ts
index 8e8e4967c8343..654434d466854 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.ts
@@ -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;
+
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();
@@ -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 () => {
@@ -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();
@@ -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)`);
});
});
});
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.tsx
similarity index 62%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.tsx
index b5d3536a1665c..94ed4ac56c304 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.tsx
@@ -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;
+
describe('Edit Auto-follow pattern', () => {
- let httpRequestsMockHelpers;
- let user;
+ let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
+ let httpSetup: SetupEnvironmentReturn['httpSetup'];
+ let user: UserEvent;
beforeAll(() => {
jest.useFakeTimers();
@@ -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
@@ -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', () => {
@@ -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`
);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.tsx
similarity index 93%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.tsx
index 72d4ae4dce3fe..085c3959bc461 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.tsx
@@ -6,24 +6,32 @@
*/
import { screen, within, act } from '@testing-library/react';
+import type { UserEvent } from '@testing-library/user-event';
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
import './mocks';
import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern';
import { setupEnvironment, pageHelpers, getRandomString } from './helpers';
+import type { AutoFollowPatternListSetupResult } from './helpers/auto_follow_pattern_list.helpers';
const { setup } = pageHelpers.autoFollowPatternList;
-const getActionsCell = (testId, rowIndex = 0) => {
+type SetupEnvironmentReturn = ReturnType;
+
+const getActionsCell = (testId: string, rowIndex = 0): HTMLElement => {
const table = new EuiTableTestHarness(testId);
const tableRows = table.getRows();
- return within(tableRows[rowIndex]).getAllByRole('cell').pop();
+ const cell = within(tableRows[rowIndex]).getAllByRole('cell').pop();
+ if (!cell) {
+ throw new Error(`expected actions cell for ${testId} row ${rowIndex}`);
+ }
+ return cell;
};
describe('', () => {
- let httpRequestsMockHelpers;
- let httpSetup;
- let user;
+ let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
+ let httpSetup: SetupEnvironmentReturn['httpSetup'];
+ let user: UserEvent;
beforeAll(() => {
jest.useFakeTimers();
@@ -75,16 +83,16 @@ describe('', () => {
});
describe('when there are multiple pages of auto-follow patterns', () => {
- let actions;
+ let actions: AutoFollowPatternListSetupResult['actions'];
beforeEach(async () => {
const autoFollowPatterns = [
- getAutoFollowPatternMock({ name: 'unique', followPattern: '{{leader_index}}' }),
+ getAutoFollowPatternMock({ name: 'unique', followIndexPattern: '{{leader_index}}' }),
];
for (let i = 0; i < 29; i++) {
autoFollowPatterns.push(
- getAutoFollowPatternMock({ name: `${i}`, followPattern: '{{leader_index}}' })
+ getAutoFollowPatternMock({ name: `${i}`, followIndexPattern: '{{leader_index}}' })
);
}
@@ -114,7 +122,7 @@ describe('', () => {
});
describe('when there are auto-follow patterns', () => {
- let actions;
+ let actions: AutoFollowPatternListSetupResult['actions'];
// For deterministic tests, we need to make sure that autoFollowPattern1 comes before autoFollowPattern2
// in the table list that is rendered. As the table orders alphabetically by index name
@@ -206,6 +214,7 @@ describe('', () => {
// We will delete the *first* auto-follow pattern in the table
httpRequestsMockHelpers.setDeleteAutoFollowPatternResponse(autoFollowPattern1.name, {
itemsDeleted: [autoFollowPattern1.name],
+ errors: [],
});
// After delete, the list loader will fetch again; return remaining item
httpRequestsMockHelpers.setLoadAutoFollowPatternsResponse({
@@ -385,7 +394,7 @@ describe('', () => {
expect(within(detailPanel2).getByTestId('titleErrors')).toBeInTheDocument();
const errors = within(detailPanel2).queryAllByTestId('recentError');
- expect(errors.map((error) => error.textContent)).toEqual([
+ expect(errors.map((error: HTMLElement) => error.textContent)).toEqual([
'April 16th, 2020 8:00:00 PM: bar',
]);
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts
index 55e4753a577e0..b67c67f9d9894 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts
@@ -8,19 +8,21 @@
import { getRandomString } from '@kbn/test-jest-helpers';
import type { AutoFollowPattern } from '../../../../common/types';
+type AutoFollowPatternMockParams = Partial<{
+ name: string;
+ active: boolean;
+ remoteCluster: string;
+ leaderIndexPatterns: string[];
+ followIndexPattern: string;
+}>;
+
export const getAutoFollowPatternMock = ({
name = getRandomString(),
active = false,
remoteCluster = getRandomString(),
leaderIndexPatterns = [`${getRandomString()}-*`],
followIndexPattern = getRandomString(),
-}: {
- name: string;
- active: boolean;
- remoteCluster: string;
- leaderIndexPatterns: string[];
- followIndexPattern: string;
-}): AutoFollowPattern => ({
+}: AutoFollowPatternMockParams = {}): AutoFollowPattern => ({
name,
active,
remoteCluster,
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts
index 6e55249a0f545..0c8c04988a0c3 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts
@@ -11,19 +11,19 @@ import type { FollowerIndex } from '../../../../common/types';
const chance = new Chance();
-interface FollowerIndexMock {
+type FollowerIndexMockParams = Partial<{
name: string;
remoteCluster: string;
leaderIndex: string;
status: string;
-}
+}>;
export const getFollowerIndexMock = ({
name = getRandomString(),
remoteCluster = getRandomString(),
leaderIndex = getRandomString(),
status = 'Active',
-}: FollowerIndexMock): FollowerIndex => ({
+}: FollowerIndexMockParams = {}): FollowerIndex => ({
name,
remoteCluster,
leaderIndex,
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.tsx
similarity index 96%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.tsx
index 8e2c56d6ec93b..4ed16bd7e171e 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.tsx
@@ -7,15 +7,18 @@
import { ILLEGAL_CHARACTERS_VISIBLE } from '@kbn/data-views-plugin/public';
import { fireEvent, screen, act } from '@testing-library/react';
+import type { UserEvent } from '@testing-library/user-event';
import './mocks';
import { setupEnvironment, pageHelpers } from './helpers';
const { setup } = pageHelpers.followerIndexAdd;
+type SetupEnvironmentReturn = ReturnType;
+
describe('Create Follower index', () => {
- let httpSetup;
- let httpRequestsMockHelpers;
- let user;
+ let httpSetup: SetupEnvironmentReturn['httpSetup'];
+ let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
+ let user: UserEvent;
beforeAll(() => {
jest.useFakeTimers();
@@ -248,7 +251,7 @@ describe('Create Follower index', () => {
await user.click(toggle);
Object.entries(advancedSettingsInputFields).forEach(([testSubj, data]) => {
- const input = screen.getByTestId(testSubj);
+ const input = screen.getByTestId(testSubj) as HTMLInputElement;
if (data.type === 'number') {
expect(input.type).toBe('number');
}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.tsx
similarity index 68%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.tsx
index 085cd3d9ebda5..6be6850ced608 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.tsx
@@ -6,6 +6,7 @@
*/
import { screen, within, act, fireEvent } from '@testing-library/react';
+import type { UserEvent } from '@testing-library/user-event';
import { API_BASE_PATH } from '../../../common/constants';
import './mocks';
import { FOLLOWER_INDEX_EDIT, FOLLOWER_INDEX_EDIT_NAME } from './helpers/constants';
@@ -13,10 +14,12 @@ import { setupEnvironment, pageHelpers } from './helpers';
const { setup } = pageHelpers.followerIndexEdit;
+type SetupEnvironmentReturn = ReturnType;
+
describe('Edit follower index', () => {
- let httpSetup;
- let httpRequestsMockHelpers;
- let user;
+ let httpSetup: SetupEnvironmentReturn['httpSetup'];
+ let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
+ let user: UserEvent;
beforeAll(() => {
jest.useFakeTimers();
@@ -86,7 +89,8 @@ describe('Edit follower index', () => {
// Verify GET was called during mount
const getCalls = httpSetup.get.mock.calls;
const getFollowerCall = getCalls.find(
- (call) => call[0] === `${API_BASE_PATH}/follower_indices/${FOLLOWER_INDEX_EDIT_NAME}`
+ (call: unknown[]) =>
+ call[0] === `${API_BASE_PATH}/follower_indices/${FOLLOWER_INDEX_EDIT_NAME}`
);
expect(getFollowerCall).toBeDefined();
@@ -115,6 +119,47 @@ describe('Edit follower index', () => {
})
);
});
+
+ // The follower index edit backend route (and likewise the create route)
+ // validates the body with `schema.object({...}).unknowns: 'forbid'`. The
+ // loaded `FollowerIndex` shape includes the read-only server-only fields
+ // `status` and `shards`; if the form forwards them on save, the server
+ // will 400 the request. The PUT body must never contain those fields
+ // regardless of what the GET response looked like.
+ test('should not forward read-only fields (status, shards) in the PUT body', async () => {
+ const maxRetryDelayInput = screen.getByTestId('maxRetryDelayInput');
+ fireEvent.change(maxRetryDelayInput, { target: { value: '10s' } });
+ fireEvent.blur(maxRetryDelayInput);
+
+ const saveButton = screen.getByTestId('submitButton');
+ fireEvent.click(saveButton);
+
+ const confirmButton = await screen.findByTestId('confirmModalConfirmButton');
+ fireEvent.click(confirmButton);
+
+ await act(async () => {
+ await jest.runOnlyPendingTimersAsync();
+ });
+
+ const putEndpoint = `${API_BASE_PATH}/follower_indices/${FOLLOWER_INDEX_EDIT_NAME}`;
+ 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).not.toHaveProperty('status');
+ expect(body).not.toHaveProperty('shards');
+ // And the save must still include the advanced setting the user changed,
+ // guarding against the flip side — accidentally stripping too much.
+ expect(body).toHaveProperty('maxRetryDelay', '10s');
+ });
});
describe('when the remote cluster is disconnected', () => {
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.tsx
similarity index 90%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.tsx
index abe44a98654d0..2dada5f5a6c3b 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.tsx
@@ -6,17 +6,22 @@
*/
import { fireEvent, screen, within, act } from '@testing-library/react';
+import type { UserEvent } from '@testing-library/user-event';
import './mocks';
import { getFollowerIndexMock } from './fixtures/follower_index';
import { setupEnvironment, pageHelpers, getRandomString } from './helpers';
+import type { FollowerIndexListSetupResult } from './helpers/follower_index_list.helpers';
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
+import type { FollowerIndex } from '../../../common/types';
const { setup } = pageHelpers.followerIndexList;
+type SetupEnvironmentReturn = ReturnType;
+
describe('', () => {
- let httpRequestsMockHelpers;
- let httpSetup;
- let user;
+ let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
+ let httpSetup: SetupEnvironmentReturn['httpSetup'];
+ let user: UserEvent;
beforeAll(() => {
jest.useFakeTimers();
@@ -64,10 +69,10 @@ describe('', () => {
});
describe('when there are multiple pages of follower indices', () => {
- const followerIndices = [{ name: 'unique', seeds: [] }];
+ const followerIndices: FollowerIndex[] = [getFollowerIndexMock({ name: 'unique' })];
for (let i = 0; i < 29; i++) {
- followerIndices.push({ name: `name${i}`, seeds: [] });
+ followerIndices.push(getFollowerIndexMock({ name: `name${i}` }));
}
beforeEach(async () => {
@@ -102,9 +107,9 @@ describe('', () => {
});
describe('when there are follower indices', () => {
- let table;
- let tableCellsValues;
- let actions;
+ let table: EuiTableTestHarness;
+ let tableCellsValues: string[][];
+ let actions: FollowerIndexListSetupResult['actions'];
const index1 = getFollowerIndexMock({ name: `a${getRandomString()}` });
const index2 = getFollowerIndexMock({ name: `b${getRandomString()}`, status: 'paused' });
@@ -165,7 +170,7 @@ describe('', () => {
const contextMenu = await screen.findByTestId('contextMenu');
const buttons = within(contextMenu).queryAllByRole('button');
- const buttonsLabel = buttons.map((btn) => btn.textContent);
+ const buttonsLabel = buttons.map((btn: HTMLElement) => btn.textContent);
expect(buttonsLabel).toEqual([
'Pause replication',
@@ -183,7 +188,7 @@ describe('', () => {
const contextMenu = await screen.findByTestId('contextMenu');
const buttons = within(contextMenu).queryAllByRole('button');
- const buttonsLabel = buttons.map((btn) => btn.textContent);
+ const buttonsLabel = buttons.map((btn: HTMLElement) => btn.textContent);
expect(buttonsLabel).toEqual([
'Resume replication',
@@ -244,8 +249,11 @@ describe('', () => {
await user.click(actionButton);
const contextMenuPanel = document.querySelector('.euiContextMenuPanel');
+ if (!contextMenuPanel) {
+ throw new Error('expected row context menu panel');
+ }
const buttons = contextMenuPanel.querySelectorAll('button.euiContextMenuItem');
- const buttonLabels = Array.from(buttons).map((btn) => btn.textContent);
+ const buttonLabels = Array.from(buttons).map((btn: Element) => btn.textContent);
expect(buttonLabels).toEqual([
'Pause replication',
@@ -261,8 +269,11 @@ describe('', () => {
await user.click(actionButton);
const contextMenuPanel = document.querySelector('.euiContextMenuPanel');
+ if (!contextMenuPanel) {
+ throw new Error('expected row context menu panel');
+ }
const buttons = contextMenuPanel.querySelectorAll('button.euiContextMenuItem');
- const buttonLabels = Array.from(buttons).map((btn) => btn.textContent);
+ const buttonLabels = Array.from(buttons).map((btn: Element) => btn.textContent);
expect(buttonLabels).toEqual([
'Resume replication',
@@ -358,7 +369,7 @@ describe('', () => {
});
test('should set the correct follower index settings values', async () => {
- const mapSettingsToFollowerIndexProp = {
+ const mapSettingsToFollowerIndexProp: Record = {
maxReadReqOpCount: 'maxReadRequestOperationCount',
maxOutstandingReadReq: 'maxOutstandingReadRequests',
maxReadReqSize: 'maxReadRequestSize',
@@ -379,7 +390,8 @@ describe('', () => {
Object.entries(mapSettingsToFollowerIndexProp).forEach(([setting, prop]) => {
const element = within(detailPanel).getByTestId(setting);
- expect(element.textContent).toEqual(index1[prop].toString());
+ const value = index1[prop];
+ expect(element.textContent).toEqual(value !== undefined ? String(value) : '');
});
});
@@ -406,8 +418,8 @@ describe('', () => {
const codeBlocks = within(detailPanel).queryAllByTestId('shardsStats');
expect(codeBlocks.length).toBe(index1.shards.length);
- codeBlocks.forEach((codeBlock, i) => {
- expect(JSON.parse(codeBlock.textContent)).toEqual(index1.shards[i]);
+ codeBlocks.forEach((codeBlock: HTMLElement, i: number) => {
+ expect(JSON.parse(codeBlock.textContent ?? '')).toEqual(index1.shards[i]);
});
});
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.ts
similarity index 55%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.ts
index 73721c4be7c1f..bce4ebeafec90 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.ts
@@ -5,24 +5,24 @@
* 2.0.
*/
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { AutoFollowPatternAdd } from '../../../app/sections/auto_follow_pattern_add';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+export const setup = (
+ componentProps: Partial> = {}
+): CcrRenderResult => {
return renderWithRouter(AutoFollowPatternAdd, {
store: createCrossClusterReplicationStore(),
- onRouter: (router) => {
- routing.reactRouter = {
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
...router,
getUrlForApp: () => '',
};
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.ts
similarity index 60%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.ts
index eaaa95c2c0078..8177f3e1cafe0 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.ts
@@ -5,27 +5,27 @@
* 2.0.
*/
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { AutoFollowPatternEdit } from '../../../app/sections/auto_follow_pattern_edit';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+export const setup = (
+ componentProps: Partial> = {}
+): CcrRenderResult => {
return renderWithRouter(AutoFollowPatternEdit, {
store: createCrossClusterReplicationStore(),
initialEntries: [`/${AUTO_FOLLOW_PATTERN_EDIT_NAME}`],
routePath: '/:id',
- onRouter: (router) => {
- routing.reactRouter = {
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
...router,
getUrlForApp: () => '',
};
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.ts
similarity index 66%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.ts
index a8e646f1ae51b..41bc0b85c873c 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.ts
@@ -6,40 +6,46 @@
*/
import { screen, within, act } from '@testing-library/react';
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+interface AutoFollowPatternListActions {
+ selectAutoFollowPatternAt: (index: number) => Promise;
+ clickBulkDeleteButton: () => Promise;
+ clickConfirmModalDeleteAutoFollowPattern: () => Promise;
+ clickAutoFollowPatternAt: (index: number) => Promise;
+ clickPaginationNextButton: () => Promise;
+ search: (value: string) => Promise;
+}
+
+export type AutoFollowPatternListSetupResult = CcrRenderResult & {
+ actions: AutoFollowPatternListActions;
+};
+
+export const setup = (
+ componentProps: Partial> = {}
+): AutoFollowPatternListSetupResult => {
const result = renderWithRouter(AutoFollowPatternList, {
store: createCrossClusterReplicationStore(),
- onRouter: (router) => {
- routing.reactRouter = {
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
...router,
- history: {
- ...router.history,
- parentHistory: {
- createHref: () => '',
- push: () => {},
- },
- },
getUrlForApp: () => '',
};
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
return {
...result,
// Helper actions for this specific page
actions: {
- async selectAutoFollowPatternAt(index) {
+ async selectAutoFollowPatternAt(index: number) {
const table = new EuiTableTestHarness('autoFollowPatternListTable');
const checkbox = within(table.getRows()[index]).getByRole('checkbox');
await result.user.click(checkbox);
@@ -59,12 +65,11 @@ export const setup = (props = {}) => {
await result.user.click(confirmBtn);
// Wait for delete HTTP request and list reload
await act(async () => {
- // eslint-disable-next-line no-undef
await jest.runOnlyPendingTimersAsync();
});
},
- async clickAutoFollowPatternAt(index) {
+ async clickAutoFollowPatternAt(index: number) {
const link = screen.getAllByTestId('autoFollowPatternLink')[index];
await result.user.click(link);
},
@@ -75,7 +80,7 @@ export const setup = (props = {}) => {
await result.user.click(nextBtn);
},
- async search(value) {
+ async search(value: string) {
const input = screen.getByTestId('autoFollowPatternSearch');
await result.user.clear(input);
await result.user.type(input, value);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.ts
similarity index 97%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.ts
index 50cf96533c105..4c744566633bc 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.ts
@@ -9,6 +9,7 @@ export const AUTO_FOLLOW_PATTERN_EDIT_NAME = 'my-autofollow';
export const AUTO_FOLLOW_PATTERN_EDIT = {
name: AUTO_FOLLOW_PATTERN_EDIT_NAME,
+ active: true,
remoteCluster: 'cluster-2',
leaderIndexPatterns: ['my-pattern-*'],
followIndexPattern: 'prefix_{{leader_index}}_suffix',
@@ -31,4 +32,5 @@ export const FOLLOWER_INDEX_EDIT = {
maxWriteBufferSize: '256mb',
maxRetryDelay: '225ms',
readPollTimeout: '2m',
+ shards: [],
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.ts
similarity index 54%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.ts
index 90f936cf9dc6b..83cb696cdfab9 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.ts
@@ -5,24 +5,24 @@
* 2.0.
*/
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { FollowerIndexAdd } from '../../../app/sections/follower_index_add';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+export const setup = (
+ componentProps: Partial> = {}
+): CcrRenderResult => {
return renderWithRouter(FollowerIndexAdd, {
store: createCrossClusterReplicationStore(),
- onRouter: (router) => {
- routing.reactRouter = {
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
...router,
getUrlForApp: () => '',
};
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.ts
similarity index 59%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.ts
index 6e4e749286e2f..4f69fcb2d775a 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.ts
@@ -5,27 +5,27 @@
* 2.0.
*/
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { FollowerIndexEdit } from '../../../app/sections/follower_index_edit';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
import { FOLLOWER_INDEX_EDIT_NAME } from './constants';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+export const setup = (
+ componentProps: Partial> = {}
+): CcrRenderResult => {
return renderWithRouter(FollowerIndexEdit, {
store: createCrossClusterReplicationStore(),
initialEntries: [`/${FOLLOWER_INDEX_EDIT_NAME}`],
routePath: '/:id',
- onRouter: (router) => {
- routing.reactRouter = {
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
...router,
getUrlForApp: () => '',
};
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.ts
similarity index 56%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.ts
index 58b20ff7f5568..7c69f702a8344 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.ts
@@ -6,38 +6,45 @@
*/
import { screen, within } from '@testing-library/react';
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { FollowerIndicesList } from '../../../app/sections/home/follower_indices_list';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+interface FollowerIndexListActions {
+ selectFollowerIndexAt: (index: number) => Promise;
+ openContextMenu: () => Promise;
+ clickContextMenuButtonAt: (index: number) => Promise;
+ openTableRowContextMenuAt: (index: number) => Promise;
+ clickFollowerIndexAt: (index: number) => Promise;
+ clickPaginationNextButton: () => Promise;
+}
+
+export type FollowerIndexListSetupResult = CcrRenderResult & {
+ actions: FollowerIndexListActions;
+};
+
+export const setup = (
+ componentProps: Partial> = {}
+): FollowerIndexListSetupResult => {
const result = renderWithRouter(FollowerIndicesList, {
store: createCrossClusterReplicationStore(),
- onRouter: (router) => {
- routing.reactRouter = {
- history: {
- ...router.history,
- parentHistory: {
- createHref: () => '',
- push: () => {},
- },
- },
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
+ ...router,
getUrlForApp: () => '',
};
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
return {
...result,
actions: {
- async selectFollowerIndexAt(index) {
+ async selectFollowerIndexAt(index: number) {
const table = new EuiTableTestHarness('followerIndexListTable');
const checkbox = within(table.getRows()[index]).getByRole('checkbox');
await result.user.click(checkbox);
@@ -48,20 +55,24 @@ export const setup = (props = {}) => {
await result.user.click(btn);
},
- async clickContextMenuButtonAt(index) {
+ async clickContextMenuButtonAt(index: number) {
const menu = screen.getByTestId('contextMenu');
const buttons = within(menu).getAllByRole('button');
await result.user.click(buttons[index]);
},
- async openTableRowContextMenuAt(index) {
+ async openTableRowContextMenuAt(index: number) {
const table = new EuiTableTestHarness('followerIndexListTable');
- const actionsCell = within(table.getRows()[index]).getAllByRole('cell').pop();
+ const cells = within(table.getRows()[index]).getAllByRole('cell');
+ const actionsCell = cells[cells.length - 1];
+ if (!actionsCell) {
+ throw new Error('expected actions cell');
+ }
const btn = within(actionsCell).getByRole('button');
await result.user.click(btn);
},
- async clickFollowerIndexAt(index) {
+ async clickFollowerIndexAt(index: number) {
const links = screen.getAllByTestId('followerIndexLink');
await result.user.click(links[index]);
},
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.ts
similarity index 52%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.ts
index 2338668cf11b9..ff7944d63f303 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.ts
@@ -5,23 +5,26 @@
* 2.0.
*/
-import { renderWithRouter } from './render';
+import type { ComponentProps } from 'react';
+import { renderWithRouter, type CcrRenderResult, type OnRouterPayload } from './render';
import { CrossClusterReplicationHome } from '../../../app/sections/home';
import { createCrossClusterReplicationStore } from '../../../app/store';
-import { routing } from '../../../app/services/routing';
+import { routing, type CcrReactRouter } from '../../../app/services/routing';
-/**
- * @param {object} [props]
- * @returns {ReturnType}
- */
-export const setup = (props = {}) => {
+export const setup = (
+ componentProps: Partial> = {}
+): CcrRenderResult => {
return renderWithRouter(CrossClusterReplicationHome, {
store: createCrossClusterReplicationStore(),
initialEntries: ['/follower_indices'],
routePath: '/:section',
- onRouter: (router) => {
- routing.reactRouter = router;
+ onRouter: (router: OnRouterPayload) => {
+ const ccrRouter: CcrReactRouter = {
+ ...router,
+ getUrlForApp: () => '',
+ };
+ routing.reactRouter = ccrRouter;
},
- defaultProps: props,
+ componentProps,
});
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
deleted file mode 100644
index 8e54f122d8d42..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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 { httpServiceMock } from '@kbn/core/public/mocks';
-import { API_BASE_PATH } from '../../../../common/constants';
-
-// Register helpers to mock HTTP Requests
-const registerHttpRequestMockHelpers = (httpSetup) => {
- const mockResponses = new Map(
- ['GET', 'PUT', 'DELETE', 'POST'].map((method) => [method, new Map()])
- );
-
- const mockMethodImplementation = (method, path) => {
- return mockResponses.get(method)?.get(path) ?? Promise.resolve({});
- };
-
- httpSetup.get.mockImplementation((path) => mockMethodImplementation('GET', path));
- httpSetup.delete.mockImplementation((path) => mockMethodImplementation('DELETE', path));
- httpSetup.post.mockImplementation((path) => mockMethodImplementation('POST', path));
- httpSetup.put.mockImplementation((path) => mockMethodImplementation('PUT', path));
-
- const mockResponse = (method, path, response, error) => {
- const defuse = (promise) => {
- promise.catch(() => {});
- return promise;
- };
-
- return mockResponses
- .get(method)
- .set(path, error ? defuse(Promise.reject({ body: error })) : Promise.resolve(response));
- };
-
- const setLoadFollowerIndicesResponse = (response = { indices: [] }, error) =>
- mockResponse('GET', `${API_BASE_PATH}/follower_indices`, response, error);
-
- const setLoadAutoFollowPatternsResponse = (response = { patterns: [] }, error) =>
- mockResponse('GET', `${API_BASE_PATH}/auto_follow_patterns`, response, error);
-
- const setDeleteAutoFollowPatternResponse = (autoFollowId, response, error) =>
- mockResponse(
- 'DELETE',
- `${API_BASE_PATH}/auto_follow_patterns/${autoFollowId}`,
- response,
- error
- );
-
- const setAutoFollowStatsResponse = (response, error) =>
- mockResponse('GET', `${API_BASE_PATH}/stats/auto_follow`, response, error);
-
- const setLoadRemoteClustersResponse = (response = [], error) =>
- mockResponse('GET', '/api/remote_clusters', response, error);
-
- const setGetAutoFollowPatternResponse = (patternId, response = {}, error) =>
- mockResponse('GET', `${API_BASE_PATH}/auto_follow_patterns/${patternId}`, response, error);
-
- const setGetClusterIndicesResponse = (response = [], error) =>
- mockResponse('GET', '/api/index_management/indices', response, error);
-
- const setGetFollowerIndexResponse = (patternId, response = {}, error) =>
- mockResponse('GET', `${API_BASE_PATH}/follower_indices/${patternId}`, response, error);
-
- return {
- setLoadFollowerIndicesResponse,
- setLoadAutoFollowPatternsResponse,
- setDeleteAutoFollowPatternResponse,
- setAutoFollowStatsResponse,
- setLoadRemoteClustersResponse,
- setGetAutoFollowPatternResponse,
- setGetClusterIndicesResponse,
- setGetFollowerIndexResponse,
- };
-};
-
-export const init = () => {
- const httpSetup = httpServiceMock.createSetupContract();
-
- return {
- httpSetup,
- httpRequestsMockHelpers: registerHttpRequestMockHelpers(httpSetup),
- };
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.ts
new file mode 100644
index 0000000000000..9418ae327c459
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.ts
@@ -0,0 +1,172 @@
+/*
+ * 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 type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser';
+import { httpServiceMock } from '@kbn/core/public/mocks';
+import { API_BASE_PATH } from '../../../../common/constants';
+import type { AutoFollowPattern, AutoFollowStats, FollowerIndex } from '../../../../common/types';
+import type { DeleteAutoFollowPatternResponse, RemoteClusterRow } from '../../../app/services/api';
+
+type HttpMethod = 'GET' | 'PUT' | 'DELETE' | 'POST';
+
+type HttpSetupMock = ReturnType;
+
+const resolvePath = (pathOrOptions: string | HttpFetchOptionsWithPath): string =>
+ typeof pathOrOptions === 'string' ? pathOrOptions : pathOrOptions.path;
+
+type MockHttpError =
+ | string
+ | { body: string }
+ | { message: string }
+ | { body: { message: string } };
+
+// Register helpers to mock HTTP Requests
+const registerHttpRequestMockHelpers = (httpSetup: HttpSetupMock) => {
+ const mockResponses = new Map>>(
+ ['GET', 'PUT', 'DELETE', 'POST'].map(
+ (method) => [method, new Map()] as [HttpMethod, Map>]
+ )
+ );
+
+ const mockMethodImplementation = (method: HttpMethod, path: string) => {
+ return mockResponses.get(method)?.get(path) ?? Promise.resolve({});
+ };
+
+ httpSetup.get.mockImplementation((path) => mockMethodImplementation('GET', resolvePath(path)));
+ httpSetup.delete.mockImplementation((path) =>
+ mockMethodImplementation('DELETE', resolvePath(path))
+ );
+ httpSetup.post.mockImplementation((path) => mockMethodImplementation('POST', resolvePath(path)));
+ httpSetup.put.mockImplementation((path) => mockMethodImplementation('PUT', resolvePath(path)));
+
+ const mockResponse = (
+ method: HttpMethod,
+ path: string,
+ response?: unknown,
+ error?: MockHttpError
+ ) => {
+ const defuse = (promise: Promise) => {
+ promise.catch(() => {});
+ return promise;
+ };
+
+ const createHttpFetchError = (mockError: MockHttpError) => {
+ const isObject = (value: unknown): value is Record =>
+ typeof value === 'object' && value !== null;
+
+ const getBody = (mockErr: MockHttpError): unknown => {
+ if (typeof mockErr === 'string') {
+ return mockErr;
+ }
+
+ return 'body' in mockErr ? mockErr.body : mockErr;
+ };
+
+ const body = getBody(mockError);
+
+ const message = (() => {
+ if (typeof body === 'string') {
+ return body;
+ }
+
+ if (isObject(body) && 'message' in body) {
+ return String(body.message);
+ }
+
+ return 'Request failed';
+ })();
+
+ const request: HttpFetchOptionsWithPath = { path };
+
+ const httpError = Object.assign(new Error(message), { body, request });
+
+ if (isObject(body) && 'statusCode' in body && typeof body.statusCode === 'number') {
+ Object.assign(httpError, { response: { status: body.statusCode } });
+ }
+
+ return httpError;
+ };
+
+ return mockResponses
+ .get(method)!
+ .set(
+ path,
+ error ? defuse(Promise.reject(createHttpFetchError(error))) : Promise.resolve(response)
+ );
+ };
+
+ const setLoadFollowerIndicesResponse = (
+ response: { indices: FollowerIndex[] } = { indices: [] },
+ error?: MockHttpError
+ ) => mockResponse('GET', `${API_BASE_PATH}/follower_indices`, response, error);
+
+ const setLoadAutoFollowPatternsResponse = (
+ response: { patterns: AutoFollowPattern[] } = { patterns: [] },
+ error?: MockHttpError
+ ) => mockResponse('GET', `${API_BASE_PATH}/auto_follow_patterns`, response, error);
+
+ const setDeleteAutoFollowPatternResponse = (
+ autoFollowId?: string,
+ response?: DeleteAutoFollowPatternResponse,
+ error?: MockHttpError
+ ) =>
+ mockResponse(
+ 'DELETE',
+ `${API_BASE_PATH}/auto_follow_patterns/${autoFollowId}`,
+ response,
+ error
+ );
+
+ const setAutoFollowStatsResponse = (response?: Partial, error?: MockHttpError) =>
+ mockResponse('GET', `${API_BASE_PATH}/stats/auto_follow`, response, error);
+
+ const setLoadRemoteClustersResponse = (
+ response: Array = [],
+ error?: MockHttpError
+ ) => mockResponse('GET', '/api/remote_clusters', response, error);
+
+ const setGetAutoFollowPatternResponse = (
+ patternId: string,
+ response: AutoFollowPattern,
+ error?: MockHttpError
+ ) => mockResponse('GET', `${API_BASE_PATH}/auto_follow_patterns/${patternId}`, response, error);
+
+ const setGetClusterIndicesResponse = (
+ response: Array<{ name: string }> = [],
+ error?: MockHttpError
+ ) => mockResponse('GET', '/api/index_management/indices', response, error);
+
+ const setGetFollowerIndexResponse = (
+ followerIndexId: string,
+ response: FollowerIndex,
+ error?: MockHttpError
+ ) => mockResponse('GET', `${API_BASE_PATH}/follower_indices/${followerIndexId}`, response, error);
+
+ return {
+ setLoadFollowerIndicesResponse,
+ setLoadAutoFollowPatternsResponse,
+ setDeleteAutoFollowPatternResponse,
+ setAutoFollowStatsResponse,
+ setLoadRemoteClustersResponse,
+ setGetAutoFollowPatternResponse,
+ setGetClusterIndicesResponse,
+ setGetFollowerIndexResponse,
+ };
+};
+
+export type HttpRequestsMockHelpers = ReturnType;
+
+export const init = () => {
+ const httpSetup = httpServiceMock.createSetupContract();
+
+ return {
+ httpSetup,
+ httpRequestsMockHelpers: registerHttpRequestMockHelpers(httpSetup),
+ };
+};
+
+export type InitHttpRequestsResult = ReturnType;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/index.ts
similarity index 74%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/index.ts
index 300ab0de698eb..5a87ace15820b 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/index.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { getRandomString } from '@kbn/test-jest-helpers';
import { setup as autoFollowPatternListSetup } from './auto_follow_pattern_list.helpers';
import { setup as autoFollowPatternAddSetup } from './auto_follow_pattern_add.helpers';
import { setup as autoFollowPatternEditSetup } from './auto_follow_pattern_edit.helpers';
@@ -12,7 +13,6 @@ import { setup as followerIndexListSetup } from './follower_index_list.helpers';
import { setup as followerIndexAddSetup } from './follower_index_add.helpers';
import { setup as followerIndexEditSetup } from './follower_index_edit.helpers';
import { setup as homeSetup } from './home.helpers';
-import { getRandomString } from '@kbn/test-jest-helpers';
export { getRandomString };
export { setupEnvironment } from './setup_environment';
@@ -25,4 +25,14 @@ export const pageHelpers = {
followerIndexAdd: { setup: followerIndexAddSetup },
followerIndexEdit: { setup: followerIndexEditSetup },
home: { setup: homeSetup },
+} as const satisfies {
+ autoFollowPatternList: {
+ setup: typeof autoFollowPatternListSetup;
+ };
+ autoFollowPatternAdd: { setup: typeof autoFollowPatternAddSetup };
+ autoFollowPatternEdit: { setup: typeof autoFollowPatternEditSetup };
+ followerIndexList: { setup: typeof followerIndexListSetup };
+ followerIndexAdd: { setup: typeof followerIndexAddSetup };
+ followerIndexEdit: { setup: typeof followerIndexEditSetup };
+ home: { setup: typeof homeSetup };
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/render.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/render.tsx
similarity index 60%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/render.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/render.tsx
index 2642e86237e65..99f16ea49ca93 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/render.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/render.tsx
@@ -6,12 +6,39 @@
*/
import React from 'react';
-import { render } from '@testing-library/react';
+import { render, type RenderResult } from '@testing-library/react';
import * as userEventLib from '@testing-library/user-event';
+import type { UserEvent } from '@testing-library/user-event';
import { Provider } from 'react-redux';
+import type { Store } from 'redux';
+import type { RouteComponentProps } from 'react-router-dom';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { MemoryRouter, Routes, Route } from '@kbn/shared-ux-router';
+export interface CcrRenderResult extends RenderResult {
+ user: UserEvent;
+ store?: Store;
+}
+
+type CcrRouteProps = RouteComponentProps;
+
+/** Object passed to `onRouter` from the wrapped Route (matches CCR routing mocks). */
+export interface OnRouterPayload {
+ history: RouteComponentProps['history'];
+ route: {
+ match: RouteComponentProps['match'];
+ location: RouteComponentProps['location'];
+ };
+}
+
+export interface RenderWithRouterOptions {
+ store?: Store;
+ onRouter?: (router: OnRouterPayload) => void;
+ initialEntries?: string[];
+ routePath?: string;
+ componentProps?: Partial;
+}
+
/**
* Render helper for CCR components with Redux store and React Router setup.
* Returns a user event instance configured for fake timers.
@@ -23,20 +50,22 @@ import { MemoryRouter, Routes, Route } from '@kbn/shared-ux-router';
* routePath: '/follower_indices/edit/:id',
* });
*/
-/**
- * @returns {import('@testing-library/react').RenderResult & { user: import('@testing-library/user-event').UserEvent }}
- */
-export const renderWithRouter = (
- Component,
- { store, onRouter, initialEntries = ['/'], routePath = '/', defaultProps = {}, ...props } = {}
-) => {
+export const renderWithRouter = (
+ Component: React.ComponentType>,
+ {
+ store,
+ onRouter,
+ initialEntries = ['/'],
+ routePath = '/',
+ componentProps = {},
+ }: RenderWithRouterOptions = {}
+): CcrRenderResult => {
const user = userEventLib.default.setup({
- // eslint-disable-next-line no-undef
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0, // Skip pointer-events check for EUI popovers/portals
});
- const Wrapped = (routeProps) => {
+ const Wrapped = (routeProps: CcrRouteProps) => {
// Setup routing callback if provided
if (typeof onRouter === 'function') {
const router = {
@@ -48,8 +77,7 @@ export const renderWithRouter = (
const ComponentWithProps = (
;
+
const getSelectedTabText = () => {
const container = document;
const selected =
@@ -20,8 +23,8 @@ const getSelectedTabText = () => {
};
describe('', () => {
- let httpRequestsMockHelpers;
- let user;
+ let httpRequestsMockHelpers: SetupEnvironmentReturn['httpRequestsMockHelpers'];
+ let user: UserEvent;
beforeAll(() => {
jest.useFakeTimers();
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts
index 76354e035e0f7..f201a93daaa22 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts
@@ -11,8 +11,6 @@ jest.mock('../../../app/services/track_ui_metric', () => {
return {
...original,
trackUiMetric: jest.fn(),
- trackUserRequest: (request: Promise) => {
- return request.then((response) => response);
- },
+ trackUserRequest: (request: Promise): Promise => request,
};
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/app.tsx b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/app.tsx
index 87742f2ed0ae9..e4622c2b30eee 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/app.tsx
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/app.tsx
@@ -17,11 +17,10 @@ import { EuiPageTemplate } from '@elastic/eui';
import { getFatalErrors } from './services/notifications';
import { routing } from './services/routing';
-// @ts-ignore
import { loadPermissions } from './services/api';
+import { getErrorBody, isHttpFetchError, toCcrApiError } from './services/http_error';
import { SectionLoading, PageError } from '../shared_imports';
-// @ts-ignore
import {
CrossClusterReplicationHome,
AutoFollowPatternAdd,
@@ -35,15 +34,22 @@ interface AppProps {
getUrlForApp: ApplicationStart['getUrlForApp'];
}
+interface PermissionError {
+ error: string;
+ cause?: string[];
+ message?: string;
+ statusCode?: number;
+}
+
interface AppState {
isFetchingPermissions: boolean;
- fetchPermissionError: any;
+ fetchPermissionError: PermissionError | undefined;
hasPermission: boolean;
- missingClusterPrivileges: any[];
+ missingClusterPrivileges: string[];
}
class AppComponent extends Component {
- constructor(props: any) {
+ constructor(props: AppProps) {
super(props);
this.registerRouter();
@@ -72,19 +78,24 @@ class AppComponent extends Component {
hasPermission,
missingClusterPrivileges,
});
- } catch (error) {
- // Expect an error in the shape provided by Angular's $http service.
- if (error && error.body) {
+ } catch (error: unknown) {
+ const apiError = toCcrApiError(error);
+ if (isHttpFetchError(apiError)) {
+ const body = getErrorBody(apiError);
return this.setState({
isFetchingPermissions: false,
- fetchPermissionError: error,
+ fetchPermissionError: {
+ error: body?.message ?? apiError.message,
+ message: body?.message,
+ statusCode: body?.statusCode,
+ },
});
}
// This error isn't an HTTP error, so let the fatal error screen tell the user something
// unexpected happened.
getFatalErrors().add(
- error,
+ apiError,
i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', {
defaultMessage: 'Cross-Cluster Replication app',
})
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.tsx.snap
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.tsx.snap
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts
index ef26fa3e46111..e3ecc46d72550 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts
@@ -5,25 +5,29 @@
* 2.0.
*/
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { connect } from 'react-redux';
import type { Props } from './auto_follow_pattern_action_menu';
import { AutoFollowPatternActionMenu as AutoFollowPatternActionMenuView } from './auto_follow_pattern_action_menu';
+import type { CcrState } from '../../store';
-// @ts-ignore
import { pauseAutoFollowPattern, resumeAutoFollowPattern } from '../../store/actions';
-const mapDispatchToProps = (dispatch: (action: any) => void) => {
- return {
- pauseAutoFollowPattern: (ids: string[]) => {
- dispatch(pauseAutoFollowPattern(ids));
- },
- resumeAutoFollowPattern: (ids: string[]) => {
- dispatch(resumeAutoFollowPattern(ids));
- },
- };
-};
+type OwnProps = Pick;
+type DispatchProps = Pick;
+type CcrDispatch = ThunkDispatch;
-export const AutoFollowPatternActionMenu = connect>(
+const mapDispatchToProps = (dispatch: CcrDispatch): DispatchProps => ({
+ pauseAutoFollowPattern: (ids: string[]) => {
+ dispatch(pauseAutoFollowPattern(ids));
+ },
+ resumeAutoFollowPattern: (ids: string[]) => {
+ dispatch(resumeAutoFollowPattern(ids));
+ },
+});
+
+export const AutoFollowPatternActionMenu = connect<{}, DispatchProps, OwnProps, CcrState>(
null,
mapDispatchToProps
)(AutoFollowPatternActionMenuView);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx
index 8612daec2dc5d..c1c1374331b79 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx
@@ -12,7 +12,6 @@ import { i18n } from '@kbn/i18n';
import { AutoFollowPatternDeleteProvider } from '../auto_follow_pattern_delete_provider';
-// @ts-ignore
import { routing } from '../../services/routing';
const actionsAriaLabel = i18n.translate(
@@ -121,7 +120,7 @@ const AutoFollowPatternActionMenuUI: FunctionComponent = ({
closePopoverViaAction();
},
},
- ].filter(Boolean);
+ ].filter((item): item is NonNullable => Boolean(item));
const button = (
= ({
defaultMessage: 'Pattern options',
}
),
- items: panelItems as any,
+ items: panelItems,
},
]}
/>
@@ -167,7 +166,7 @@ const AutoFollowPatternActionMenuUI: FunctionComponent = ({
);
};
-export const AutoFollowPatternActionMenu = (props: Omit) => (
+export const AutoFollowPatternActionMenu = (props: Omit) => (
{(deleteAutoFollowPattern: (ids: string[]) => void) => (
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts
deleted file mode 100644
index d6b0f9ef292a0..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * 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 type { FC, ReactNode } from 'react';
-
-declare const AutoFollowPatternDeleteProvider: FC<{
- children: (deleteAutoFollowPattern: (ids: string[]) => void) => ReactNode;
-}>;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.tsx
similarity index 74%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.tsx
index 7583bff7b6b19..45b07dc5d73f2 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.tsx
@@ -5,33 +5,51 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
+import React, { PureComponent, Fragment, type ReactNode, type SyntheticEvent } from 'react';
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiConfirmModal, htmlIdGenerator } from '@elastic/eui';
+import type { CcrState } from '../store/reducers';
import { deleteAutoFollowPattern } from '../store/actions';
import { arrify } from '../../../common/services/utils';
-class AutoFollowPatternDeleteProviderUi extends PureComponent {
- state = {
+interface Props {
+ /** Thunk is typed as `string` but the API accepts single or multiple ids. */
+ deleteAutoFollowPattern: (id: string | string[]) => void;
+ children: (deleteAutoFollowPattern: (id: string | string[]) => void) => ReactNode;
+}
+
+interface State {
+ isModalOpen: boolean;
+ ids: string[] | null;
+}
+
+class AutoFollowPatternDeleteProviderUi extends PureComponent {
+ state: State = {
isModalOpen: false,
ids: null,
};
- onMouseOverModal = (event) => {
+ stopModalEventPropagation = (event: SyntheticEvent) => {
// This component can sometimes be used inside of an EuiToolTip, in which case mousing over
// the modal can trigger the tooltip. Stopping propagation prevents this.
event.stopPropagation();
};
- deleteAutoFollowPattern = (id) => {
+ deleteAutoFollowPattern = (id: string | string[]) => {
this.setState({ isModalOpen: true, ids: arrify(id) });
};
onConfirm = () => {
- this.props.deleteAutoFollowPattern(this.state.ids);
+ const { ids } = this.state;
+ if (!ids) {
+ return;
+ }
+ this.props.deleteAutoFollowPattern(ids);
this.setState({ isModalOpen: false, ids: null });
};
@@ -43,6 +61,9 @@ class AutoFollowPatternDeleteProviderUi extends PureComponent {
renderModal = () => {
const { ids } = this.state;
+ if (!ids) {
+ return null;
+ }
const isSingle = ids.length === 1;
const title = isSingle
? i18n.translate(
@@ -63,7 +84,6 @@ class AutoFollowPatternDeleteProviderUi extends PureComponent {
const confirmModalTitleId = htmlIdGenerator()('confirmModalTitle');
return (
- // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
{!isSingle && (
@@ -118,8 +139,8 @@ class AutoFollowPatternDeleteProviderUi extends PureComponent {
}
}
-const mapDispatchToProps = (dispatch) => ({
- deleteAutoFollowPattern: (id) => dispatch(deleteAutoFollowPattern(id)),
+const mapDispatchToProps = (dispatch: ThunkDispatch) => ({
+ deleteAutoFollowPattern: (id: string | string[]) => dispatch(deleteAutoFollowPattern(id)),
});
export const AutoFollowPatternDeleteProvider = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.tsx
similarity index 73%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.tsx
index 8e5f0caaeb820..7e4c94dc217c6 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.tsx
@@ -6,6 +6,7 @@
*/
import { updateFormErrors } from './auto_follow_pattern_form';
+import type { AutoFollowPatternValidationErrors } from '../services/auto_follow_pattern_validators';
jest.mock('../services/auto_follow_pattern_validators', () => ({
validateAutoFollowPattern: jest.fn(),
@@ -15,8 +16,8 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({
describe(' {
describe('updateFormErrors()', () => {
it('should merge errors with existing fieldsErrors', () => {
- const errors = { name: 'Some error' };
- const existingErrors = { leaderIndexPatterns: null };
+ const errors: AutoFollowPatternValidationErrors = { name: 'Some error' };
+ const existingErrors: AutoFollowPatternValidationErrors = { leaderIndexPatterns: null };
const output = updateFormErrors(errors, existingErrors);
expect(output).toMatchSnapshot();
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.tsx
similarity index 78%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.tsx
index 3df55310fae3a..f6d0fd8a8fca2 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_form.tsx
@@ -5,8 +5,8 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { PureComponent, Fragment, type ReactNode } from 'react';
+import type { EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -40,42 +40,85 @@ import { SectionError } from './section_error';
import { AutoFollowPatternIndicesPreview } from './auto_follow_pattern_indices_preview';
import { RemoteClustersFormField } from './remote_clusters_form_field';
import {
+ type AutoFollowPatternValidationErrors,
+ type MessageError,
validateAutoFollowPattern,
validateLeaderIndexPattern,
} from '../services/auto_follow_pattern_validators';
import { AutoFollowPatternRequestFlyout } from './auto_follow_pattern_request_flyout';
+import type { ApiStatus } from '../../../common/types';
+import type { AutoFollowPatternConfig, AutoFollowPatternCreateConfig } from '../services/api';
+import type { CcrApiError } from '../services/http_error';
+import type { ParsedAutoFollowPattern } from '../store/reducers/auto_follow_pattern';
const indexPatternIllegalCharacters = ILLEGAL_CHARACTERS_VISIBLE.join(' ');
const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
-const getEmptyAutoFollowPattern = (remoteClusterName = '') => ({
+type AutoFollowPatternFormFields = Omit & {
+ active?: boolean;
+};
+
+const getEmptyAutoFollowPattern = (remoteClusterName = ''): AutoFollowPatternFormFields => ({
name: '',
remoteCluster: remoteClusterName,
leaderIndexPatterns: [],
+ followIndexPattern: '',
followIndexPatternPrefix: '',
followIndexPatternSuffix: '',
});
-export const updateFormErrors = (errors, existingErrors) => ({
+export const updateFormErrors = (
+ errors: AutoFollowPatternValidationErrors,
+ existingErrors: AutoFollowPatternValidationErrors
+) => ({
fieldsErrors: {
...existingErrors,
...errors,
},
});
-export class AutoFollowPatternForm extends PureComponent {
- static propTypes = {
- saveAutoFollowPattern: PropTypes.func.isRequired,
- autoFollowPattern: PropTypes.object,
- apiError: PropTypes.object,
- apiStatus: PropTypes.string.isRequired,
- currentUrl: PropTypes.string.isRequired,
- remoteClusters: PropTypes.array,
- saveButtonLabel: PropTypes.node,
- };
+interface RemoteClusterOption {
+ name: string;
+ isConnected: boolean;
+}
+
+interface BaseProps {
+ apiError?: CcrApiError | null;
+ apiStatus: ApiStatus;
+ currentUrl: string;
+ remoteClusters?: RemoteClusterOption[];
+ saveButtonLabel: ReactNode;
+}
+
+interface CreateModeProps extends BaseProps {
+ autoFollowPattern?: undefined;
+ createAutoFollowPattern: (name: string, autoFollowPattern: AutoFollowPatternCreateConfig) => void;
+ // Disallow providing the update handler in create mode; without this, callers passing a props bag
+ // (e.g. via spread) can accidentally provide both handlers when `autoFollowPattern` is omitted.
+ updateAutoFollowPattern?: never;
+}
+
+interface UpdateModeProps extends BaseProps {
+ autoFollowPattern: ParsedAutoFollowPattern;
+ updateAutoFollowPattern: (name: string, autoFollowPattern: AutoFollowPatternConfig) => void;
+ // Disallow providing the create handler in update mode; without this, callers passing a props bag
+ // (e.g. via spread) can accidentally provide both handlers.
+ createAutoFollowPattern?: never;
+}
+
+type Props = CreateModeProps | UpdateModeProps;
- constructor(props) {
+interface State {
+ autoFollowPattern: AutoFollowPatternFormFields;
+ fieldsErrors: AutoFollowPatternValidationErrors;
+ areErrorsVisible: boolean;
+ isNew: boolean;
+ isRequestVisible: boolean;
+}
+
+export class AutoFollowPatternForm extends PureComponent {
+ constructor(props: Props) {
super(props);
const isNew = this.props.autoFollowPattern === undefined;
@@ -83,9 +126,16 @@ export class AutoFollowPatternForm extends PureComponent {
route: {
location: { search },
},
- } = routing.reactRouter;
+ } = routing.reactRouterOrThrow;
const queryParams = extractQueryParams(search);
- const remoteClusterName = getRemoteClusterName(this.props.remoteClusters, queryParams.cluster);
+ const rawCluster = queryParams.cluster;
+ const clusterParam =
+ typeof rawCluster === 'string'
+ ? rawCluster
+ : Array.isArray(rawCluster)
+ ? rawCluster[0]
+ : undefined;
+ const remoteClusterName = getRemoteClusterName(this.props.remoteClusters ?? [], clusterParam);
const autoFollowPattern = isNew
? getEmptyAutoFollowPattern(remoteClusterName)
: {
@@ -94,7 +144,13 @@ export class AutoFollowPatternForm extends PureComponent {
this.state = {
autoFollowPattern,
- fieldsErrors: validateAutoFollowPattern(autoFollowPattern),
+ fieldsErrors: validateAutoFollowPattern({
+ name: autoFollowPattern.name,
+ leaderIndexPatterns: autoFollowPattern.leaderIndexPatterns,
+ followIndexPatternPrefix: autoFollowPattern.followIndexPatternPrefix,
+ followIndexPatternSuffix: autoFollowPattern.followIndexPatternSuffix,
+ remoteCluster: autoFollowPattern.remoteCluster,
+ }),
areErrorsVisible: false,
isNew,
isRequestVisible: false,
@@ -107,7 +163,7 @@ export class AutoFollowPatternForm extends PureComponent {
}));
};
- onFieldsChange = (fields) => {
+ onFieldsChange = (fields: Partial) => {
this.setState(({ autoFollowPattern }) => ({
autoFollowPattern: {
...autoFollowPattern,
@@ -115,22 +171,28 @@ export class AutoFollowPatternForm extends PureComponent {
},
}));
- const errors = validateAutoFollowPattern(fields);
+ const errors = validateAutoFollowPattern({
+ name: fields.name,
+ leaderIndexPatterns: fields.leaderIndexPatterns,
+ followIndexPatternPrefix: fields.followIndexPatternPrefix,
+ followIndexPatternSuffix: fields.followIndexPatternSuffix,
+ remoteCluster: fields.remoteCluster,
+ });
this.onFieldsErrorChange(errors);
};
- onFieldsErrorChange = (errors) =>
+ onFieldsErrorChange = (errors: AutoFollowPatternValidationErrors) =>
this.setState(({ fieldsErrors }) => updateFormErrors(errors, fieldsErrors));
- onClusterChange = (remoteCluster) => {
+ onClusterChange = (remoteCluster: string) => {
this.onFieldsChange({ remoteCluster });
};
- onCreateLeaderIndexPattern = (indexPattern) => {
+ onCreateLeaderIndexPattern = (indexPattern: string) => {
const error = validateLeaderIndexPattern(indexPattern);
if (error) {
- const errors = {
+ const errors: AutoFollowPatternValidationErrors = {
leaderIndexPatterns: {
...error,
alwaysVisible: true,
@@ -152,13 +214,13 @@ export class AutoFollowPatternForm extends PureComponent {
this.onFieldsChange({ leaderIndexPatterns: newLeaderIndexPatterns });
};
- onLeaderIndexPatternChange = (indexPatterns) => {
+ onLeaderIndexPatternChange = (indexPatterns: Array>) => {
this.onFieldsChange({
leaderIndexPatterns: indexPatterns.map(({ label }) => label),
});
};
- onLeaderIndexPatternInputChange = (leaderIndexPattern) => {
+ onLeaderIndexPatternInputChange = (leaderIndexPattern: string) => {
const isEmpty = !leaderIndexPattern || !leaderIndexPattern.trim();
const {
autoFollowPattern: { leaderIndexPatterns },
@@ -172,7 +234,7 @@ export class AutoFollowPatternForm extends PureComponent {
}
);
- const errors = {
+ const errors: AutoFollowPatternValidationErrors = {
leaderIndexPatterns: {
message: errorMsg,
alwaysVisible: true,
@@ -181,15 +243,17 @@ export class AutoFollowPatternForm extends PureComponent {
this.setState(({ fieldsErrors }) => updateFormErrors(errors, fieldsErrors));
} else {
- this.setState(({ fieldsErrors, autoFollowPattern: { leaderIndexPatterns } }) => {
- const errors = Boolean(leaderIndexPatterns.length)
- ? // Validate existing patterns, so we can surface an error if this required input is missing.
- validateAutoFollowPattern({ leaderIndexPatterns })
- : // Validate the input as the user types so they have immediate feedback about errors.
- validateAutoFollowPattern({ leaderIndexPatterns: [leaderIndexPattern] });
-
- return updateFormErrors(errors, fieldsErrors);
- });
+ this.setState(
+ ({ fieldsErrors, autoFollowPattern: { leaderIndexPatterns: existingPatterns } }) => {
+ const errors = Boolean(existingPatterns.length)
+ ? // Validate existing patterns, so we can surface an error if this required input is missing.
+ validateAutoFollowPattern({ leaderIndexPatterns: existingPatterns })
+ : // Validate the input as the user types so they have immediate feedback about errors.
+ validateAutoFollowPattern({ leaderIndexPatterns: [leaderIndexPattern] });
+
+ return updateFormErrors(errors, fieldsErrors);
+ }
+ );
}
};
@@ -204,9 +268,7 @@ export class AutoFollowPatternForm extends PureComponent {
};
isFormValid() {
- return Object.values(this.state.fieldsErrors).every(
- (error) => error === undefined || error === null
- );
+ return Object.values(this.state.fieldsErrors).every((error) => error == null);
}
sendForm = () => {
@@ -219,8 +281,16 @@ export class AutoFollowPatternForm extends PureComponent {
this.setState({ areErrorsVisible: false });
- const { name, ...autoFollowPattern } = this.getFields();
- this.props.saveAutoFollowPattern(name, autoFollowPattern);
+ const { name, active, ...autoFollowPattern } = this.getFields();
+ const props = this.props;
+ if (props.autoFollowPattern === undefined) {
+ props.createAutoFollowPattern(name, autoFollowPattern);
+ return;
+ }
+ props.updateAutoFollowPattern(name, {
+ ...autoFollowPattern,
+ active: active ?? true,
+ });
};
cancelForm = () => {
@@ -258,8 +328,8 @@ export class AutoFollowPatternForm extends PureComponent {
name,
remoteCluster,
leaderIndexPatterns,
- followIndexPatternPrefix,
- followIndexPatternSuffix,
+ followIndexPatternPrefix = '',
+ followIndexPatternSuffix = '',
},
isNew,
areErrorsVisible,
@@ -299,7 +369,7 @@ export class AutoFollowPatternForm extends PureComponent {
defaultMessage="Name"
/>
}
- error={fieldsErrors.name}
+ error={fieldsErrors.name ?? undefined}
isInvalid={isInvalid}
fullWidth
>
@@ -329,12 +399,12 @@ export class AutoFollowPatternForm extends PureComponent {
defaultMessage="Auto-follow patterns capture indices on remote clusters."
/>
),
- remoteClusterNotConnectedNotEditable: (name) => ({
+ remoteClusterNotConnectedNotEditable: (clusterName: string) => ({
title: (
),
description: (
@@ -344,12 +414,12 @@ export class AutoFollowPatternForm extends PureComponent {
/>
),
}),
- remoteClusterDoesNotExist: (name) => (
+ remoteClusterDoesNotExist: (clusterName: string) => (
),
};
@@ -376,7 +446,7 @@ export class AutoFollowPatternForm extends PureComponent {
>
{
- const hasError = !!(
- fieldsErrors.leaderIndexPatterns && fieldsErrors.leaderIndexPatterns.message
- );
+ const leaderIndexPatternsError: MessageError | null | undefined =
+ fieldsErrors.leaderIndexPatterns;
+ const hasError = Boolean(leaderIndexPatternsError?.message);
const isInvalid =
- hasError && (fieldsErrors.leaderIndexPatterns.alwaysVisible || areErrorsVisible);
+ hasError && (Boolean(leaderIndexPatternsError?.alwaysVisible) || areErrorsVisible);
const formattedLeaderIndexPatterns = leaderIndexPatterns.map((pattern) => ({
label: pattern,
}));
@@ -459,7 +529,7 @@ export class AutoFollowPatternForm extends PureComponent {
/>
}
isInvalid={isInvalid}
- error={fieldsErrors.leaderIndexPatterns && fieldsErrors.leaderIndexPatterns.message}
+ error={leaderIndexPatternsError && leaderIndexPatternsError.message}
fullWidth
>
}
- error={fieldsErrors.followIndexPatternPrefix}
+ error={fieldsErrors.followIndexPatternPrefix ?? undefined}
isInvalid={isPrefixInvalid}
fullWidth
>
@@ -545,7 +615,7 @@ export class AutoFollowPatternForm extends PureComponent {
defaultMessage="Suffix"
/>
}
- error={fieldsErrors.followIndexPatternSuffix}
+ error={fieldsErrors.followIndexPatternSuffix ?? undefined}
isInvalid={isSuffixInvalid}
fullWidth
>
@@ -588,7 +658,6 @@ export class AutoFollowPatternForm extends PureComponent {
* Form Error warning message
*/
const renderFormErrorWarning = () => {
- const { areErrorsVisible } = this.state;
const isFormValid = this.isFormValid();
if (!areErrorsVisible || isFormValid) {
@@ -619,7 +688,7 @@ export class AutoFollowPatternForm extends PureComponent {
*/
const renderActions = () => {
const { apiStatus, saveButtonLabel } = this.props;
- const { areErrorsVisible, isRequestVisible } = this.state;
+ const { isRequestVisible } = this.state;
if (apiStatus === API_STATUS.SAVING) {
return (
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.tsx
similarity index 67%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.tsx
index e235432f0a341..a9db3481ac0f5 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.tsx
@@ -5,14 +5,24 @@
* 2.0.
*/
-import React from 'react';
+import React, { type FC } from 'react';
import { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { getPreviewIndicesFromAutoFollowPattern } from '../services/auto_follow_pattern';
-export const AutoFollowPatternIndicesPreview = ({ prefix, suffix, leaderIndexPatterns }) => {
+interface Props {
+ prefix?: string;
+ suffix?: string;
+ leaderIndexPatterns: string[];
+}
+
+export const AutoFollowPatternIndicesPreview: FC = ({
+ prefix = '',
+ suffix = '',
+ leaderIndexPatterns,
+}) => {
const { indicesPreview } = getPreviewIndicesFromAutoFollowPattern({
prefix,
suffix,
@@ -33,13 +43,15 @@ export const AutoFollowPatternIndicesPreview = ({ prefix, suffix, leaderIndexPat
defaultMessage="The above settings will generate index names that look like this:"
/>
- {indicesPreview.map(({ followPattern: { prefix, suffix, template } }, i) => (
- -
- {prefix}
- {template}
- {suffix}
-
- ))}
+ {indicesPreview.map(
+ ({ followPattern: { prefix: fpPrefix, suffix: fpSuffix, template } }, i) => (
+ -
+ {fpPrefix}
+ {template}
+ {fpSuffix}
+
+ )
+ )}
);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.tsx
similarity index 84%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.tsx
index 8984fed221961..75c0102e59202 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.tsx
@@ -5,15 +5,18 @@
* 2.0.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { type ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiPageHeader, EuiButtonEmpty } from '@elastic/eui';
import { documentationLinks } from '../services/documentation_links';
-export const AutoFollowPatternPageTitle = ({ title }) => (
+interface Props {
+ title: ReactNode;
+}
+
+export const AutoFollowPatternPageTitle = ({ title }: Props) => (
<>
(
>
);
-
-AutoFollowPatternPageTitle.propTypes = {
- title: PropTypes.node.isRequired,
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.tsx
similarity index 93%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.tsx
index 52d20c910549d..2b90989700b8c 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.tsx
@@ -7,7 +7,6 @@
import React, { PureComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import PropTypes from 'prop-types';
import {
EuiButtonEmpty,
@@ -23,15 +22,16 @@ import {
} from '@elastic/eui';
import { serializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization';
+import type { AutoFollowPatternCreateConfig } from '../services/api';
-export class AutoFollowPatternRequestFlyout extends PureComponent {
- static propTypes = {
- close: PropTypes.func.isRequired,
- name: PropTypes.string.isRequired,
- autoFollowPattern: PropTypes.object.isRequired,
- isNew: PropTypes.bool,
- };
+interface Props {
+ close: () => void;
+ name: string;
+ autoFollowPattern: AutoFollowPatternCreateConfig;
+ isNew?: boolean;
+}
+export class AutoFollowPatternRequestFlyout extends PureComponent {
render() {
const { name, autoFollowPattern, close, isNew } = this.props;
const endpoint = `PUT /_ccr/auto_follow/${name ? name : ''}`;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.tsx
similarity index 68%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.tsx
index 80f4837d6c01c..64453beaeb9bf 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.tsx
@@ -5,13 +5,25 @@
* 2.0.
*/
-import React from 'react';
+import React, { type ReactNode } from 'react';
+
+import type { FollowerIndex } from '../../../../common/types';
import { FollowerIndexPauseProvider } from './follower_index_pause_provider';
import { FollowerIndexResumeProvider } from './follower_index_resume_provider';
import { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider';
-export const FollowerIndexActionsProvider = (props) => {
+export interface FollowerIndexActionsCallbacks {
+ pauseFollowerIndex: (index: FollowerIndex | FollowerIndex[]) => void;
+ resumeFollowerIndex: (id: string | string[]) => void;
+ unfollowLeaderIndex: (id: string | string[]) => void;
+}
+
+interface Props {
+ children: (getActions: () => FollowerIndexActionsCallbacks) => ReactNode;
+}
+
+export const FollowerIndexActionsProvider = (props: Props) => {
return (
{(pauseFollowerIndex) => (
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.tsx
similarity index 79%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.tsx
index 3c6a7fd4fd92c..c1e0e35bb1c1b 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.tsx
@@ -5,41 +5,51 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { PureComponent, Fragment, type ReactNode, type SyntheticEvent } from 'react';
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiConfirmModal, htmlIdGenerator } from '@elastic/eui';
+import type { CcrState } from '../../store/reducers';
+import type { FollowerIndex } from '../../../../common/types';
import { pauseFollowerIndex } from '../../store/actions';
import { arrify } from '../../../../common/services/utils';
import { areAllSettingsDefault } from '../../services/follower_index_default_settings';
-class FollowerIndexPauseProviderUi extends PureComponent {
- static propTypes = {
- onConfirm: PropTypes.func,
- };
+interface Props {
+ pauseFollowerIndex: (id: string | string[]) => void;
+ children: (pauseFollowerIndex: (index: FollowerIndex | FollowerIndex[]) => void) => ReactNode;
+ onConfirm?: () => void;
+}
+
+interface State {
+ isModalOpen: boolean;
+ indices: FollowerIndex[];
+}
- state = {
+class FollowerIndexPauseProviderUi extends PureComponent {
+ state: State = {
isModalOpen: false,
indices: [],
};
- onMouseOverModal = (event) => {
+ stopModalEventPropagation = (event: SyntheticEvent) => {
// This component can sometimes be used inside of an EuiToolTip, in which case mousing over
// the modal can trigger the tooltip. Stopping propagation prevents this.
event.stopPropagation();
};
- pauseFollowerIndex = (index) => {
+ pauseFollowerIndex = (index: FollowerIndex | FollowerIndex[]) => {
this.setState({ isModalOpen: true, indices: arrify(index) });
};
onConfirm = () => {
this.props.pauseFollowerIndex(this.state.indices.map((index) => index.name));
this.setState({ isModalOpen: false, indices: [] });
- this.props.onConfirm && this.props.onConfirm();
+ this.props.onConfirm?.();
};
closeConfirmModal = () => {
@@ -50,6 +60,9 @@ class FollowerIndexPauseProviderUi extends PureComponent {
renderModal = () => {
const { indices } = this.state;
+ if (!indices.length) {
+ return null;
+ }
const isSingle = indices.length === 1;
const title = isSingle
? i18n.translate(
@@ -71,7 +84,6 @@ class FollowerIndexPauseProviderUi extends PureComponent {
const confirmModalTitleId = htmlIdGenerator()('confirmModalTitle');
return (
- // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
{hasCustomSettings && (
@@ -144,8 +157,8 @@ class FollowerIndexPauseProviderUi extends PureComponent {
}
}
-const mapDispatchToProps = (dispatch) => ({
- pauseFollowerIndex: (id) => dispatch(pauseFollowerIndex(id)),
+const mapDispatchToProps = (dispatch: ThunkDispatch) => ({
+ pauseFollowerIndex: (id: string | string[]) => dispatch(pauseFollowerIndex(id)),
});
export const FollowerIndexPauseProvider = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.tsx
similarity index 79%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.tsx
index 1a48518ceecd5..de45363f78a20 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.tsx
@@ -5,41 +5,54 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { PureComponent, Fragment, type ReactNode, type SyntheticEvent } from 'react';
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiConfirmModal, EuiLink, htmlIdGenerator } from '@elastic/eui';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
+import type { CcrState } from '../../store/reducers';
import { routing } from '../../services/routing';
import { resumeFollowerIndex } from '../../store/actions';
import { arrify } from '../../../../common/services/utils';
-class FollowerIndexResumeProviderUi extends PureComponent {
- static propTypes = {
- onConfirm: PropTypes.func,
- };
+interface Props {
+ resumeFollowerIndex: (id: string | string[]) => void;
+ children: (resumeFollowerIndex: (id: string | string[]) => void) => ReactNode;
+ onConfirm?: () => void;
+}
- state = {
+interface State {
+ isModalOpen: boolean;
+ ids: string[] | null;
+}
+
+class FollowerIndexResumeProviderUi extends PureComponent {
+ state: State = {
isModalOpen: false,
ids: null,
};
- onMouseOverModal = (event) => {
+ stopModalEventPropagation = (event: SyntheticEvent) => {
// This component can sometimes be used inside of an EuiToolTip, in which case mousing over
// the modal can trigger the tooltip. Stopping propagation prevents this.
event.stopPropagation();
};
- resumeFollowerIndex = (id) => {
+ resumeFollowerIndex = (id: string | string[]) => {
this.setState({ isModalOpen: true, ids: arrify(id) });
};
onConfirm = () => {
- this.props.resumeFollowerIndex(this.state.ids);
+ const { ids } = this.state;
+ if (!ids) {
+ return;
+ }
+ this.props.resumeFollowerIndex(ids);
this.setState({ isModalOpen: false, ids: null });
- this.props.onConfirm && this.props.onConfirm();
+ this.props.onConfirm?.();
};
closeConfirmModal = () => {
@@ -50,6 +63,10 @@ class FollowerIndexResumeProviderUi extends PureComponent {
renderModal = () => {
const { ids } = this.state;
+ if (!ids) {
+ return null;
+ }
+ const reactRouter = routing.reactRouterOrThrow;
const isSingle = ids.length === 1;
const title = isSingle
? i18n.translate(
@@ -70,7 +87,6 @@ class FollowerIndexResumeProviderUi extends PureComponent {
const confirmModalTitleId = htmlIdGenerator()('confirmModalTitle');
return (
- // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
{isSingle ? (
@@ -103,7 +120,7 @@ class FollowerIndexResumeProviderUi extends PureComponent {
editLink: (
({
- resumeFollowerIndex: (id) => dispatch(resumeFollowerIndex(id)),
+const mapDispatchToProps = (dispatch: ThunkDispatch) => ({
+ resumeFollowerIndex: (id: string | string[]) => dispatch(resumeFollowerIndex(id)),
});
export const FollowerIndexResumeProvider = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.tsx
similarity index 77%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.tsx
index 14657d819b78b..7aa6dd49cc7f2 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.tsx
@@ -5,40 +5,53 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { PureComponent, Fragment, type ReactNode, type SyntheticEvent } from 'react';
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiConfirmModal, htmlIdGenerator } from '@elastic/eui';
+import type { CcrState } from '../../store/reducers';
import { unfollowLeaderIndex } from '../../store/actions';
import { arrify } from '../../../../common/services/utils';
-class FollowerIndexUnfollowProviderUi extends PureComponent {
- static propTypes = {
- onConfirm: PropTypes.func,
- };
+interface Props {
+ unfollowLeaderIndex: (id: string | string[]) => void;
+ children: (unfollowLeaderIndex: (id: string | string[]) => void) => ReactNode;
+ onConfirm?: () => void;
+}
- state = {
+interface State {
+ isModalOpen: boolean;
+ ids: string[] | null;
+}
+
+class FollowerIndexUnfollowProviderUi extends PureComponent {
+ state: State = {
isModalOpen: false,
ids: null,
};
- onMouseOverModal = (event) => {
+ stopModalEventPropagation = (event: SyntheticEvent) => {
// This component can sometimes be used inside of an EuiToolTip, in which case mousing over
// the modal can trigger the tooltip. Stopping propagation prevents this.
event.stopPropagation();
};
- unfollowLeaderIndex = (id) => {
+ unfollowLeaderIndex = (id: string | string[]) => {
this.setState({ isModalOpen: true, ids: arrify(id) });
};
onConfirm = () => {
- this.props.unfollowLeaderIndex(this.state.ids);
+ const { ids } = this.state;
+ if (!ids) {
+ return;
+ }
+ this.props.unfollowLeaderIndex(ids);
this.setState({ isModalOpen: false, ids: null });
- this.props.onConfirm && this.props.onConfirm();
+ this.props.onConfirm?.();
};
closeConfirmModal = () => {
@@ -49,6 +62,9 @@ class FollowerIndexUnfollowProviderUi extends PureComponent {
renderModal = () => {
const { ids } = this.state;
+ if (!ids) {
+ return null;
+ }
const isSingle = ids.length === 1;
const title = isSingle
? i18n.translate(
@@ -68,7 +84,6 @@ class FollowerIndexUnfollowProviderUi extends PureComponent {
const modalTitleId = htmlIdGenerator()('confirmModalTitle');
return (
- // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
{isSingle ? (
@@ -136,8 +152,8 @@ class FollowerIndexUnfollowProviderUi extends PureComponent {
}
}
-const mapDispatchToProps = (dispatch) => ({
- unfollowLeaderIndex: (id) => dispatch(unfollowLeaderIndex(id)),
+const mapDispatchToProps = (dispatch: ThunkDispatch) => ({
+ unfollowLeaderIndex: (id: string | string[]) => dispatch(unfollowLeaderIndex(id)),
});
export const FollowerIndexUnfollowProvider = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.tsx.snap
similarity index 94%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.tsx.snap
index 017f565ff9f48..584fb2e10e12e 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.tsx.snap
@@ -5,6 +5,7 @@ Object {
"followerIndex": Object {
"leaderIndex": "bar",
"name": "new-name",
+ "remoteCluster": "remote",
},
}
`;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.tsx
similarity index 88%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.tsx
index 92a7a3670722b..3474e50b41ce4 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.tsx
@@ -6,12 +6,39 @@
*/
import React from 'react';
+import type { ReactElement } from 'react';
+import type { DocLinksStart } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiLink } from '@elastic/eui';
+
+import type { FollowerIndexAdvancedSettings } from '../../../../common/types';
import { getSettingDefault } from '../../services/follower_index_default_settings';
-export const getAdvancedSettingsFields = (documentationLinks) => {
+type DocumentationLinks = DocLinksStart['links'];
+
+type AdvancedSettingField = keyof FollowerIndexAdvancedSettings;
+type AdvancedSettingValue = string | number;
+
+export type AdvancedSettingValidator = (
+ value: AdvancedSettingValue | undefined
+) => ReactElement[] | undefined;
+
+interface AdvancedSettingFieldDefinition {
+ readonly field: AdvancedSettingField;
+ readonly testSubject: string;
+ readonly title: string;
+ readonly description: string;
+ readonly label: string;
+ readonly defaultValue: AdvancedSettingValue;
+ readonly helpText?: React.ReactNode;
+ readonly type?: string;
+ readonly validator?: AdvancedSettingValidator;
+}
+
+export const getAdvancedSettingsFields = (
+ documentationLinks: DocumentationLinks
+): AdvancedSettingFieldDefinition[] => {
const byteUnitsHelpText = (
{
];
};
-export const getEmptyAdvancedSettings = (documentationLinks) =>
- getAdvancedSettingsFields(documentationLinks).reduce((obj, advancedSetting) => {
- const { field, defaultValue } = advancedSetting;
- return { ...obj, [field]: defaultValue };
- }, {});
+export const getEmptyAdvancedSettings = (documentationLinks: DocumentationLinks) =>
+ getAdvancedSettingsFields(documentationLinks).reduce>(
+ (obj, advancedSetting) => {
+ const { field, defaultValue } = advancedSetting;
+ return { ...obj, [field]: defaultValue };
+ },
+ {}
+ );
-export function areAdvancedSettingsEdited(followerIndex, documentationLinks) {
+export function areAdvancedSettingsEdited(
+ followerIndex: FollowerIndexAdvancedSettings,
+ documentationLinks: DocumentationLinks
+) {
+ const empty = getEmptyAdvancedSettings(documentationLinks);
return getAdvancedSettingsFields(documentationLinks).some((advancedSetting) => {
const { field } = advancedSetting;
- return followerIndex[field] !== getEmptyAdvancedSettings(documentationLinks)[field];
+ return followerIndex[field] !== empty[field];
});
}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.tsx
similarity index 65%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.tsx
index d541c43d99b5c..5b764dca61fbb 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.tsx
@@ -5,12 +5,12 @@
* 2.0.
*/
-import { updateFields, updateFormErrors } from './follower_index_form';
+import { updateFields, updateFormErrors, type FollowerIndexFormState } from './follower_index_form';
describe(' state transitions', () => {
it('updateFormErrors() should merge errors with existing fieldsErrors', () => {
- const errors = { name: 'Some error' };
- const state = {
+ const errors: Record = { name: 'Some error' };
+ const state: Pick = {
fieldsErrors: { leaderIndex: null },
};
const output = updateFormErrors(errors)(state);
@@ -19,8 +19,12 @@ describe(' state transitions', () => {
it('updateFields() should merge new fields value with existing followerIndex', () => {
const fields = { name: 'new-name' };
- const state = {
- followerIndex: { name: 'foo', leaderIndex: 'bar' },
+ const state: Pick = {
+ followerIndex: {
+ name: 'foo',
+ remoteCluster: 'remote',
+ leaderIndex: 'bar',
+ },
};
const output = updateFields(fields)(state);
expect(output).toMatchSnapshot();
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.tsx
similarity index 74%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.tsx
index 3b05a933a8724..49a12f49dee7a 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.tsx
@@ -5,9 +5,9 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { PureComponent, Fragment, type ReactElement, type ReactNode } from 'react';
import { debounce } from 'lodash';
+import type { DebouncedFunc } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -27,16 +27,19 @@ import {
EuiSwitch,
EuiText,
EuiTitle,
+ type EuiSwitchEvent,
} from '@elastic/eui';
import { extractQueryParams, indices } from '../../../shared_imports';
import { indexNameValidator, leaderIndexValidator } from '../../services/input_validation';
import { routing } from '../../services/routing';
import { getFatalErrors } from '../../services/notifications';
-import { loadIndices } from '../../services/api';
+import { loadIndices, type FollowerIndexSaveBody } from '../../services/api';
import { documentationLinks } from '../../services/documentation_links';
import { API_STATUS } from '../../constants';
import { getRemoteClusterName } from '../../services/get_remote_cluster_name';
+import type { CcrApiError } from '../../services/http_error';
+import { getErrorStatus, isHttpFetchError, toCcrApiError } from '../../services/http_error';
import { RemoteClustersFormField } from '../remote_clusters_form_field';
import { SectionError } from '../section_error';
import { FormEntryRow } from '../form_entry_row';
@@ -44,38 +47,84 @@ import {
getAdvancedSettingsFields,
getEmptyAdvancedSettings,
areAdvancedSettingsEdited,
+ type AdvancedSettingValidator,
} from './advanced_settings_fields';
import { FollowerIndexRequestFlyout } from './follower_index_request_flyout';
+import type {
+ ApiStatus,
+ FollowerIndex,
+ FollowerIndexAdvancedSettings,
+} from '../../../../common/types';
const indexNameIllegalCharacters = indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
-const getFieldToValidatorMap = (advancedSettingsFields) =>
- advancedSettingsFields.reduce(
- (map, advancedSetting) => {
- const { field, validator } = advancedSetting;
- map[field] = validator;
- return map;
- },
- {
- name: indexNameValidator,
- leaderIndex: leaderIndexValidator,
+type FieldValidator = (value: string | number | undefined) => ReactElement[] | undefined;
+type FollowerIndexFormFields = FollowerIndexSaveBody & Pick;
+
+const getFieldToValidatorMap = (
+ advancedSettingsFields: ReturnType
+): Record => {
+ const map: Record = {
+ name: (value) => indexNameValidator(String(value ?? '')),
+ leaderIndex: (value) => leaderIndexValidator(String(value ?? '')),
+ };
+ for (const advancedSetting of advancedSettingsFields) {
+ if (typeof advancedSetting.validator === 'function') {
+ const validator: AdvancedSettingValidator = advancedSetting.validator;
+ map[advancedSetting.field] = validator;
}
- );
+ }
+ return map;
+};
-const getEmptyFollowerIndex = (remoteClusterName = '') => ({
+const getEmptyFollowerIndex = (remoteClusterName = ''): FollowerIndexFormFields => ({
name: '',
remoteCluster: remoteClusterName,
leaderIndex: '',
...getEmptyAdvancedSettings(documentationLinks),
});
+type FollowerIndexFieldError =
+ | ReactElement[]
+ | { message: ReactNode; alwaysVisible?: boolean }
+ | undefined
+ | null;
+
+interface RemoteClusterOption {
+ name: string;
+ isConnected: boolean;
+}
+
+interface Props {
+ saveFollowerIndex: (name: string, followerIndex: FollowerIndexSaveBody) => void;
+ clearApiError: () => void;
+ followerIndex?: Omit;
+ apiError?: CcrApiError | null;
+ apiStatus: ApiStatus;
+ remoteClusters?: RemoteClusterOption[];
+ saveButtonLabel: ReactNode;
+ currentUrl: string;
+}
+
+interface State {
+ isNew: boolean;
+ followerIndex: FollowerIndexFormFields;
+ fieldsErrors: Record;
+ areErrorsVisible: boolean;
+ areAdvancedSettingsVisible: boolean;
+ isValidatingIndexName: boolean;
+ isRequestVisible: boolean;
+}
+
+export type FollowerIndexFormState = State;
+
/**
* State transitions: fields update
*/
export const updateFields =
- (fields) =>
- ({ followerIndex }) => ({
+ (fields: Partial) =>
+ ({ followerIndex }: Pick) => ({
followerIndex: {
...followerIndex,
...fields,
@@ -86,50 +135,53 @@ export const updateFields =
* State transitions: errors update
*/
export const updateFormErrors =
- (errors) =>
- ({ fieldsErrors }) => ({
+ (errors: Record) =>
+ ({ fieldsErrors }: Pick) => ({
fieldsErrors: {
...fieldsErrors,
...errors,
},
});
-export class FollowerIndexForm extends PureComponent {
- static propTypes = {
- saveFollowerIndex: PropTypes.func.isRequired,
- clearApiError: PropTypes.func.isRequired,
- followerIndex: PropTypes.object,
- apiError: PropTypes.object,
- apiStatus: PropTypes.string.isRequired,
- remoteClusters: PropTypes.array,
- saveButtonLabel: PropTypes.node,
- };
+export class FollowerIndexForm extends PureComponent {
+ cachedAdvancedSettings: Partial = {};
- constructor(props) {
+ validateIndexName: DebouncedFunc<(name: string) => Promise>;
+
+ constructor(props: Props) {
super(props);
const {
route: {
location: { search },
},
- } = routing.reactRouter;
+ } = routing.reactRouterOrThrow;
const queryParams = extractQueryParams(search);
+ const rawCluster = queryParams.cluster;
+ const clusterParam =
+ typeof rawCluster === 'string'
+ ? rawCluster
+ : Array.isArray(rawCluster)
+ ? rawCluster[0]
+ : undefined;
+
+ const followerIndexProp = this.props.followerIndex;
+ const isNew = followerIndexProp === undefined;
+ const remoteClusterName = getRemoteClusterName(this.props.remoteClusters ?? [], clusterParam);
+
+ let followerIndex: FollowerIndexFormFields;
+ if (followerIndexProp === undefined) {
+ followerIndex = getEmptyFollowerIndex(remoteClusterName);
+ } else {
+ const { status: _status, ...editableFollowerIndex } = followerIndexProp;
+ followerIndex = {
+ ...getEmptyFollowerIndex(),
+ ...editableFollowerIndex,
+ };
+ }
- const isNew = this.props.followerIndex === undefined;
- const remoteClusterName = getRemoteClusterName(this.props.remoteClusters, queryParams.cluster);
- const followerIndex = isNew
- ? getEmptyFollowerIndex(remoteClusterName)
- : {
- ...getEmptyFollowerIndex(),
- ...this.props.followerIndex,
- };
-
- // eslint-disable-next-line no-nested-ternary
- const areAdvancedSettingsVisible = isNew
- ? false
- : areAdvancedSettingsEdited(followerIndex, documentationLinks)
- ? true
- : false;
+ const areAdvancedSettingsVisible =
+ !isNew && areAdvancedSettingsEdited(followerIndex, documentationLinks);
const fieldsErrors = this.getFieldsErrors(followerIndex);
@@ -143,8 +195,7 @@ export class FollowerIndexForm extends PureComponent {
isRequestVisible: false,
};
- this.cachedAdvancedSettings = {};
- this.validateIndexName = debounce(this.validateIndexName, 500, { trailing: true });
+ this.validateIndexName = debounce(this.validateIndexNameImpl, 500, { trailing: true });
}
toggleRequest = () => {
@@ -153,11 +204,11 @@ export class FollowerIndexForm extends PureComponent {
}));
};
- onFieldsChange = (fields) => {
+ onFieldsChange = (fields: Partial) => {
this.setState(updateFields(fields));
const newFields = {
- ...this.state.fields,
+ ...this.state.followerIndex,
...fields,
};
@@ -168,22 +219,23 @@ export class FollowerIndexForm extends PureComponent {
}
};
- getFieldsErrors = (newFields) => {
- return Object.keys(newFields).reduce((errors, field) => {
- const advancedSettings = getAdvancedSettingsFields(documentationLinks);
- const validator = getFieldToValidatorMap(advancedSettings)[field];
- const value = newFields[field];
+ getFieldsErrors = (newFields: Partial) => {
+ const advancedSettings = getAdvancedSettingsFields(documentationLinks);
+ const validatorMap = getFieldToValidatorMap(advancedSettings);
+ const errors: Record = {};
+ for (const [field, value] of Object.entries(newFields)) {
+ const validator = validatorMap[field];
if (validator) {
- const error = validator(value);
- errors[field] = error;
+ errors[field] = validator(value);
}
+ }
- return errors;
- }, {});
+ return errors;
};
- onIndexNameChange = ({ name }) => {
+ onIndexNameChange = (value: string | number) => {
+ const name = String(value);
this.onFieldsChange({ name });
const error = indexNameValidator(name);
@@ -208,10 +260,10 @@ export class FollowerIndexForm extends PureComponent {
this.validateIndexName(name);
};
- validateIndexName = async (name) => {
+ validateIndexNameImpl = async (name: string) => {
try {
- const indices = await loadIndices();
- const doesExist = indices.some((index) => index.name === name);
+ const loadedIndices = await loadIndices();
+ const doesExist = loadedIndices.some((index) => index.name === name);
if (doesExist) {
const error = {
message: (
@@ -230,25 +282,25 @@ export class FollowerIndexForm extends PureComponent {
isValidatingIndexName: false,
});
} catch (error) {
- if (error) {
- if (error.name === 'AbortError') {
- // Ignore aborted requests
- return;
- }
- // This could be an HTTP error
- if (error.body) {
- // All validation does is check for a name collision, so we can just let the user attempt
- // to save the follower index and get an error back from the API.
- return this.setState({
- isValidatingIndexName: false,
- });
- }
+ const apiError = toCcrApiError(error);
+ if (apiError.name === 'AbortError') {
+ // Ignore aborted requests
+ return;
+ }
+
+ // This could be an HTTP error.
+ if (isHttpFetchError(apiError)) {
+ // All validation does is check for a name collision, so we can just let the user attempt
+ // to save the follower index and get an error back from the API.
+ return this.setState({
+ isValidatingIndexName: false,
+ });
}
// This error isn't an HTTP error, so let the fatal error screen tell the user something
// unexpected happened.
getFatalErrors().add(
- error,
+ apiError,
i18n.translate(
'xpack.crossClusterReplication.followerIndexForm.indexNameValidationFatalErrorTitle',
{
@@ -259,7 +311,7 @@ export class FollowerIndexForm extends PureComponent {
}
};
- onClusterChange = (remoteCluster) => {
+ onClusterChange = (remoteCluster: string) => {
this.onFieldsChange({ remoteCluster });
};
@@ -267,7 +319,7 @@ export class FollowerIndexForm extends PureComponent {
return this.state.followerIndex;
};
- toggleAdvancedSettings = (event) => {
+ toggleAdvancedSettings = (event: EuiSwitchEvent) => {
// If the user edits the advanced settings but then hides them, we need to make sure the
// edited values don't get sent to the API when the user saves, but we *do* want to restore
// these values to the form when the user re-opens the advanced settings.
@@ -289,16 +341,15 @@ export class FollowerIndexForm extends PureComponent {
// Save a cache of the advanced settings.
const fields = this.getFields();
- this.cachedAdvancedSettings = getAdvancedSettingsFields(documentationLinks).reduce(
- (cache, { field }) => {
- const value = fields[field];
- if (value !== '') {
- cache[field] = value;
- }
- return cache;
- },
- {}
- );
+ this.cachedAdvancedSettings = getAdvancedSettingsFields(documentationLinks).reduce<
+ Partial
+ >((cache, { field }) => {
+ const value = fields[field];
+ if (value !== '') {
+ Object.assign(cache, { [field]: value });
+ }
+ return cache;
+ }, {});
// Hide the advanced settings.
this.setState({
@@ -321,9 +372,8 @@ export class FollowerIndexForm extends PureComponent {
return;
}
- const { name, ...followerIndex } = this.getFields();
-
- this.props.saveFollowerIndex(name, followerIndex);
+ const { name, ...saveBody } = this.getFields();
+ this.props.saveFollowerIndex(name, saveBody);
};
cancelForm = () => {
@@ -336,38 +386,36 @@ export class FollowerIndexForm extends PureComponent {
renderApiErrors() {
const { apiError } = this.props;
- if (apiError) {
- const title = i18n.translate(
- 'xpack.crossClusterReplication.followerIndexForm.savingErrorTitle',
- {
- defaultMessage: `Can't create follower index`,
- }
- );
- const { leaderIndex } = this.state.followerIndex;
- const error =
- apiError.status === 404
- ? {
- data: {
- message: i18n.translate(
- 'xpack.crossClusterReplication.followerIndexForm.leaderIndexNotFoundError',
- {
- defaultMessage: `The leader index ''{leaderIndex}'' does not exist.`,
- values: { leaderIndex },
- }
- ),
- },
- }
- : apiError;
-
- return (
-
-
-
-
- );
+ if (!apiError) {
+ return null;
}
- return null;
+ const title = i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexForm.savingErrorTitle',
+ {
+ defaultMessage: `Can't create follower index`,
+ }
+ );
+ const { leaderIndex } = this.state.followerIndex;
+ const error =
+ getErrorStatus(apiError) === 404
+ ? {
+ message: i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexForm.leaderIndexNotFoundError',
+ {
+ defaultMessage: `The leader index ''{leaderIndex}'' does not exist.`,
+ values: { leaderIndex },
+ }
+ ),
+ }
+ : apiError;
+
+ return (
+
+
+
+
+ );
}
renderForm = () => {
@@ -450,7 +498,7 @@ export class FollowerIndexForm extends PureComponent {
defaultMessage="Replication requires a leader index on a remote cluster."
/>
),
- remoteClusterNotConnectedNotEditable: (name) => ({
+ remoteClusterNotConnectedNotEditable: (name: string) => ({
title: (
),
}),
- remoteClusterDoesNotExist: (name) => (
+ remoteClusterDoesNotExist: (name: string) => (
);
@@ -652,7 +700,7 @@ export class FollowerIndexForm extends PureComponent {
helpText={helpText}
type={type}
areErrorsVisible={areErrorsVisible}
- onValueUpdate={this.onFieldsChange}
+ onValueUpdate={(value) => this.onFieldsChange({ [field]: value })}
testSubj={testSubject}
/>
);
@@ -668,7 +716,6 @@ export class FollowerIndexForm extends PureComponent {
* Form Error warning message
*/
const renderFormErrorWarning = () => {
- const { areErrorsVisible } = this.state;
const isFormValid = this.isFormValid();
if (!areErrorsVisible || isFormValid) {
@@ -700,7 +747,7 @@ export class FollowerIndexForm extends PureComponent {
*/
const renderActions = () => {
const { apiStatus, saveButtonLabel } = this.props;
- const { areErrorsVisible, isRequestVisible } = this.state;
+ const { isRequestVisible } = this.state;
if (apiStatus === API_STATUS.SAVING) {
return (
@@ -804,6 +851,7 @@ export class FollowerIndexForm extends PureComponent {
render() {
const { followerIndex, isRequestVisible } = this.state;
+ const { name, ...requestPayload } = this.getFields();
return (
@@ -812,8 +860,8 @@ export class FollowerIndexForm extends PureComponent {
{isRequestVisible ? (
this.setState({ isRequestVisible: false })}
/>
) : null}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.tsx
similarity index 89%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.tsx
index 462ec9668d424..9776f191b4aa0 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.tsx
@@ -7,7 +7,6 @@
import React, { PureComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import PropTypes from 'prop-types';
import {
EuiButtonEmpty,
@@ -23,14 +22,15 @@ import {
} from '@elastic/eui';
import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization';
+import type { FollowerIndexSaveBody } from '../../services/api';
-export class FollowerIndexRequestFlyout extends PureComponent {
- static propTypes = {
- close: PropTypes.func.isRequired,
- name: PropTypes.string.isRequired,
- followerIndex: PropTypes.object.isRequired,
- };
+interface Props {
+ close: () => void;
+ name: string;
+ followerIndex: FollowerIndexSaveBody;
+}
+export class FollowerIndexRequestFlyout extends PureComponent {
render() {
const { name, followerIndex, close } = this.props;
const endpoint = `PUT /${name ? name : ''}/_ccr/follow`;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_form/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_page_title.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_page_title.tsx
similarity index 84%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_page_title.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_page_title.tsx
index b4b8dbf3241a8..a016853f73865 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_page_title.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/follower_index_page_title.tsx
@@ -5,15 +5,18 @@
* 2.0.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { type ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiPageHeader, EuiButtonEmpty } from '@elastic/eui';
import { documentationLinks } from '../services/documentation_links';
-export const FollowerIndexPageTitle = ({ title }) => (
+interface Props {
+ title: ReactNode;
+}
+
+export const FollowerIndexPageTitle = ({ title }: Props) => (
<>
(
>
);
-
-FollowerIndexPageTitle.propTypes = {
- title: PropTypes.node.isRequired,
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/form_entry_row.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/form_entry_row.tsx
similarity index 61%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/form_entry_row.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/form_entry_row.tsx
index a544d4ed2b2e3..290b83ec08c49 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/form_entry_row.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/form_entry_row.tsx
@@ -5,8 +5,7 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { PureComponent, Fragment, type ReactElement, type ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@@ -21,46 +20,57 @@ import {
* State transitions: fields update
*/
export const updateFields =
- (newValues) =>
- ({ fields }) => ({
+ >(newValues: Partial) =>
+ ({ fields }: { fields: T }) => ({
fields: {
...fields,
...newValues,
},
});
-export class FormEntryRow extends PureComponent {
- static propTypes = {
- title: PropTypes.node,
- description: PropTypes.node,
- label: PropTypes.node,
- helpText: PropTypes.node,
- type: PropTypes.string,
- onValueUpdate: PropTypes.func.isRequired,
- field: PropTypes.string.isRequired,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
- defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- isLoading: PropTypes.bool,
- error: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
- disabled: PropTypes.bool,
- areErrorsVisible: PropTypes.bool.isRequired,
- testSubj: PropTypes.string,
- };
+interface StructuredError {
+ message?: ReactNode;
+ alwaysVisible?: boolean;
+}
+
+type FormEntryError = ReactElement[] | StructuredError | undefined | null;
+
+const isStructuredError = (error: FormEntryError | string | undefined): error is StructuredError =>
+ !!error && typeof error === 'object' && !Array.isArray(error);
+
+interface Props {
+ /** EuiDescribedFormGroup expects a React element for the title slot. */
+ title: ReactElement;
+ description?: ReactNode;
+ label?: ReactNode;
+ helpText?: ReactNode;
+ type?: string;
+ onValueUpdate: (value: string | number) => void;
+ field: string;
+ value: string | number | undefined;
+ defaultValue?: string | number;
+ isLoading?: boolean;
+ error?: FormEntryError | string;
+ disabled?: boolean;
+ areErrorsVisible: boolean;
+ testSubj?: string;
+}
- onFieldChange = (value) => {
- const { field, onValueUpdate, type } = this.props;
+export class FormEntryRow extends PureComponent {
+ onFieldChange = (value: string | number) => {
+ const { onValueUpdate, type } = this.props;
const isNumber = type === 'number';
let valueParsed = value;
if (isNumber) {
- valueParsed = !!value ? Math.max(0, parseInt(value, 10)) : value; // make sure we don't send NaN value or a negative number
+ valueParsed = !!value ? Math.max(0, parseInt(String(value), 10)) : value; // make sure we don't send NaN value or a negative number
}
- onValueUpdate({ [field]: valueParsed });
+ onValueUpdate(valueParsed);
};
- renderField = (isInvalid) => {
+ renderField = (isInvalid: boolean) => {
const { value, type, disabled, isLoading, testSubj } = this.props;
switch (type) {
case 'number':
@@ -103,8 +113,8 @@ export class FormEntryRow extends PureComponent {
defaultValue,
} = this.props;
- const hasError = !!error;
- const isInvalid = hasError && (error.alwaysVisible || areErrorsVisible);
+ const structured = isStructuredError(error) ? error : undefined;
+ const isInvalid = !!error && ((structured && structured.alwaysVisible) || areErrorsVisible);
const canBeResetToDefault = defaultValue !== undefined;
const isResetToDefaultVisible = value !== defaultValue;
@@ -130,7 +140,7 @@ export class FormEntryRow extends PureComponent {
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.d.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.d.ts
deleted file mode 100644
index 5fe864c8198a7..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * 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 declare const SectionError: any;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_form_field.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_form_field.tsx
similarity index 83%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_form_field.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_form_field.tsx
index a83e820b3d837..24c67578ce9e8 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_form_field.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_form_field.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, PureComponent } from 'react';
+import React, { Fragment, PureComponent, type ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@@ -21,6 +21,35 @@ import {
import { routing } from '../services/routing';
+export interface RemoteClustersFormFieldErrorMessages {
+ noClusterFound: () => ReactNode;
+ remoteClusterNotConnectedEditable?: (name: string) => {
+ title: ReactNode;
+ description: ReactNode;
+ };
+ remoteClusterNotConnectedNotEditable?: (name: string) => {
+ title: ReactNode;
+ description: ReactNode;
+ };
+ remoteClusterDoesNotExist?: (name: string) => ReactNode;
+}
+
+interface RemoteClusterRow {
+ name: string;
+ isConnected: boolean;
+}
+
+interface Props {
+ selected: string | null;
+ remoteClusters: RemoteClusterRow[];
+ currentUrl: string;
+ isEditable: boolean;
+ areErrorsVisible: boolean;
+ onChange: (clusterName: string) => void;
+ onError: (error: { message: ReactNode } | null) => void;
+ errorMessages: RemoteClustersFormFieldErrorMessages;
+}
+
const errorMessages = {
noClusterFound: () => (
),
- remoteClusterNotConnectedEditable: (name) => ({
+ remoteClusterNotConnectedEditable: (name: string) => ({
title: (
{
+ errorMessages: RemoteClustersFormFieldErrorMessages = {
...errorMessages,
...this.props.errorMessages,
};
@@ -58,7 +87,7 @@ export class RemoteClustersFormField extends PureComponent {
onError(error);
}
- validateRemoteCluster(clusterName) {
+ validateRemoteCluster(clusterName: string | null) {
const { remoteClusters } = this.props;
const remoteCluster = remoteClusters.find((c) => c.name === clusterName);
@@ -76,7 +105,7 @@ export class RemoteClustersFormField extends PureComponent {
};
}
- onRemoteClusterChange = (cluster) => {
+ onRemoteClusterChange = (cluster: string) => {
const { onChange, onError } = this.props;
const { error } = this.validateRemoteCluster(cluster);
onChange(cluster);
@@ -90,7 +119,7 @@ export class RemoteClustersFormField extends PureComponent {
return (
{
this.onRemoteClusterChange(e.target.value);
}}
@@ -217,14 +246,18 @@ export class RemoteClustersFormField extends PureComponent {
);
};
- renderCurrentRemoteClusterNotConnected = (name, fatal) => {
+ renderCurrentRemoteClusterNotConnected = (name: string, fatal?: boolean) => {
const { isEditable, currentUrl } = this.props;
const { remoteClusterNotConnectedEditable, remoteClusterNotConnectedNotEditable } =
this.errorMessages;
- const { title, description } = isEditable
- ? remoteClusterNotConnectedEditable(name)
- : remoteClusterNotConnectedNotEditable(name);
+ const resolver = isEditable
+ ? remoteClusterNotConnectedEditable
+ : remoteClusterNotConnectedNotEditable;
+ if (!resolver) {
+ return null;
+ }
+ const { title, description } = resolver(name);
return (
{
+ renderRemoteClusterDoesNotExist = (name: string) => {
const { currentUrl } = this.props;
const title = i18n.translate(
'xpack.crossClusterReplication.remoteClustersFormField.remoteClusterNotFoundTitle',
@@ -265,7 +298,7 @@ export class RemoteClustersFormField extends PureComponent {
return (
- {this.errorMessages.remoteClusterDoesNotExist(name)}
+ {this.errorMessages.remoteClusterDoesNotExist?.(name)}
- {field}
+ <>{field}>
);
}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_provider.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_provider.ts
similarity index 54%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_provider.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_provider.ts
index c8993441f2672..575b9370db793 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_provider.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/remote_clusters_provider.ts
@@ -5,11 +5,27 @@
* 2.0.
*/
-import { PureComponent } from 'react'; // eslint-disable-line no-unused-vars
-import { loadRemoteClusters } from '../services/api';
+import { PureComponent, type ReactNode } from 'react';
+import { loadRemoteClusters, type RemoteClusterRow } from '../services/api';
+import type { CcrApiError } from '../services/http_error';
+import { toCcrApiError } from '../services/http_error';
-export class RemoteClustersProvider extends PureComponent {
- state = {
+interface State {
+ isLoading: boolean;
+ error: CcrApiError | null;
+ remoteClusters: RemoteClusterRow[];
+}
+
+interface Props {
+ children: (params: {
+ isLoading: boolean;
+ error: CcrApiError | null;
+ remoteClusters: RemoteClusterRow[];
+ }) => ReactNode;
+}
+
+export class RemoteClustersProvider extends PureComponent {
+ state: State = {
isLoading: true,
error: null,
remoteClusters: [],
@@ -20,8 +36,8 @@ export class RemoteClustersProvider extends PureComponent {
}
loadRemoteClusters() {
- const sortClusterByName = (remoteClusters) =>
- remoteClusters.sort((a, b) => {
+ const sortClusterByName = (remoteClusters: RemoteClusterRow[]) =>
+ remoteClusters.sort((a: RemoteClusterRow, b: RemoteClusterRow) => {
if (a.name < b.name) {
return -1;
}
@@ -31,7 +47,7 @@ export class RemoteClustersProvider extends PureComponent {
return 0;
});
loadRemoteClusters()
- .then(sortClusterByName)
+ .then((clusters) => sortClusterByName(clusters))
.then((remoteClusters) => {
this.setState({
isLoading: false,
@@ -41,7 +57,7 @@ export class RemoteClustersProvider extends PureComponent {
.catch((error) => {
this.setState({
isLoading: false,
- error,
+ error: toCcrApiError(error),
});
});
}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_error.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_error.js
deleted file mode 100644
index e646a6fc082c9..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_error.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 React, { Fragment } from 'react';
-import { EuiCallOut, EuiSpacer } from '@elastic/eui';
-
-export function SectionError(props) {
- const { title, error, ...rest } = props;
- const data = error.body ? error.body : error;
- const { error: errorString, attributes, message } = data;
-
- return (
-
- {message || errorString}
- {attributes?.error?.root_cause && (
-
-
-
- {attributes.error.root_cause.map(({ type, reason }, i) => (
- -
- {type}: {reason}
-
- ))}
-
-
- )}
-
- );
-}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_error.tsx b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_error.tsx
new file mode 100644
index 0000000000000..368d185b28524
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_error.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 React, { Fragment, type ReactNode } from 'react';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
+import type { CcrApiError } from '../services/http_error';
+import { getErrorBody, isHttpFetchError } from '../services/http_error';
+
+interface Props {
+ title: ReactNode;
+ error: CcrApiError | { message?: ReactNode; error?: string };
+ 'data-test-subj'?: string;
+}
+
+export function SectionError({ title, error, ...rest }: Props) {
+ const errorBody =
+ error instanceof Error && isHttpFetchError(error) ? getErrorBody(error) : undefined;
+ const message: ReactNode | undefined = errorBody?.message ?? error.message;
+ const errorString: string | undefined =
+ error instanceof Error || errorBody ? undefined : error.error;
+
+ const attributes = errorBody?.attributes;
+
+ const getRootCauses = (): Array<{ type: string; reason: string }> | undefined => {
+ if (attributes == null || typeof attributes !== 'object') {
+ return;
+ }
+
+ const attributesError = Reflect.get(attributes, 'error');
+ if (attributesError == null || typeof attributesError !== 'object') {
+ return;
+ }
+
+ const rootCause = Reflect.get(attributesError, 'root_cause');
+ if (!Array.isArray(rootCause)) {
+ return;
+ }
+
+ const result: Array<{ type: string; reason: string }> = [];
+ for (const item of rootCause) {
+ if (item == null || typeof item !== 'object') {
+ continue;
+ }
+
+ const type = Reflect.get(item, 'type');
+ const reason = Reflect.get(item, 'reason');
+ if (typeof type === 'string' && typeof reason === 'string') {
+ result.push({ type, reason });
+ }
+ }
+
+ return result.length ? result : undefined;
+ };
+
+ const rootCauses = getRootCauses();
+
+ return (
+
+ {message || errorString}
+ {rootCauses && (
+
+
+
+ {rootCauses.map(({ type, reason }, i) => (
+ -
+ {type}: {reason}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_loading.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_loading.tsx
similarity index 80%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_loading.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_loading.tsx
index 5f4761344f0ef..40bbb5aae1b1d 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_loading.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/components/section_loading.tsx
@@ -5,11 +5,16 @@
* 2.0.
*/
-import React from 'react';
+import React, { type ReactNode } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText, EuiTextColor } from '@elastic/eui';
-export function SectionLoading({ children, dataTestSubj = '' }) {
+interface Props {
+ children: ReactNode;
+ dataTestSubj?: string;
+}
+
+export function SectionLoading({ children, dataTestSubj = '' }: Props) {
return (
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/constants/sections.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/constants/sections.ts
index b335de451d0ab..b2fa75e112042 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/constants/sections.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/constants/sections.ts
@@ -10,4 +10,4 @@ export const SECTIONS = {
FOLLOWER_INDEX: 'followerIndex',
REMOTE_CLUSTER: 'remoteCluster',
CCR_STATS: 'ccrStats',
-};
+} as const;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.ts
similarity index 53%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.ts
index be6481be4f943..502dee7624dfe 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.ts
@@ -6,23 +6,29 @@
*/
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { SECTIONS } from '../../constants';
+import type { AutoFollowPatternCreateConfig } from '../../services/api';
+import type { CcrState } from '../../store';
import { getApiStatus, getApiError } from '../../store/selectors';
-import { saveAutoFollowPattern, clearApiError } from '../../store/actions';
+import { createAutoFollowPattern, clearApiError } from '../../store/actions';
import { AutoFollowPatternAdd as AutoFollowPatternAddView } from './auto_follow_pattern_add';
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
-const mapStateToProps = (state) => ({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
apiStatus: getApiStatus(`${scope}-save`)(state),
apiError: getApiError(`${scope}-save`)(state),
});
-const mapDispatchToProps = (dispatch) => ({
- saveAutoFollowPattern: (id, autoFollowPattern) =>
- dispatch(saveAutoFollowPattern(id, autoFollowPattern)),
- clearApiError: () => dispatch(clearApiError(scope)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ createAutoFollowPattern: (id: string, autoFollowPattern: AutoFollowPatternCreateConfig) =>
+ dispatch(createAutoFollowPattern(id, autoFollowPattern)),
+ clearApiError: () => dispatch(clearApiError(`${scope}-save`)),
});
export const AutoFollowPatternAdd = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.tsx
similarity index 77%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.tsx
index a6ab123786c8e..8df230dbb900f 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.tsx
@@ -6,11 +6,14 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import type { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPageSection } from '@elastic/eui';
+import type { ApiStatus } from '../../../../common/types';
import { listBreadcrumb, addBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs';
+import type { AutoFollowPatternCreateConfig } from '../../services/api';
+import type { CcrApiError } from '../../services/http_error';
import {
AutoFollowPatternForm,
AutoFollowPatternPageTitle,
@@ -18,14 +21,14 @@ import {
} from '../../components';
import { SectionLoading } from '../../../shared_imports';
-export class AutoFollowPatternAdd extends PureComponent {
- static propTypes = {
- saveAutoFollowPattern: PropTypes.func.isRequired,
- clearApiError: PropTypes.func.isRequired,
- apiError: PropTypes.object,
- apiStatus: PropTypes.string.isRequired,
- };
+export interface AutoFollowPatternAddProps extends RouteComponentProps {
+ createAutoFollowPattern: (id: string, autoFollowPattern: AutoFollowPatternCreateConfig) => void;
+ clearApiError: () => void;
+ apiError: CcrApiError | null;
+ apiStatus: ApiStatus;
+}
+export class AutoFollowPatternAdd extends PureComponent {
componentDidMount() {
setBreadcrumbs([listBreadcrumb('/auto_follow_patterns'), addBreadcrumb]);
}
@@ -36,7 +39,7 @@ export class AutoFollowPatternAdd extends PureComponent {
render() {
const {
- saveAutoFollowPattern,
+ createAutoFollowPattern,
apiStatus,
apiError,
match: { url: currentUrl },
@@ -72,7 +75,7 @@ export class AutoFollowPatternAdd extends PureComponent {
apiError={apiError}
currentUrl={currentUrl}
remoteClusters={error ? [] : remoteClusters}
- saveAutoFollowPattern={saveAutoFollowPattern}
+ createAutoFollowPattern={createAutoFollowPattern}
saveButtonLabel={
({
+ getAutoFollowPattern: jest.fn(() => ({ type: 'MOCK/GET_AUTO_FOLLOW_PATTERN' })),
+ updateAutoFollowPattern: jest.fn(() => ({ type: 'MOCK/UPDATE_AUTO_FOLLOW_PATTERN' })),
+ selectEditAutoFollowPattern: jest.fn(() => ({ type: 'MOCK/SELECT_EDIT_AUTO_FOLLOW_PATTERN' })),
+ clearApiError: jest.fn((scope: string) => ({ type: 'MOCK/CLEAR_API_ERROR', payload: scope })),
+}));
+
+import { mapDispatchToProps } from './auto_follow_pattern_edit.container';
+import {
+ updateAutoFollowPattern,
+ clearApiError,
+ selectEditAutoFollowPattern,
+ getAutoFollowPattern,
+} from '../../store/actions';
+
+const mockedUpdateAutoFollowPattern = jest.mocked(updateAutoFollowPattern);
+const mockedClearApiError = jest.mocked(clearApiError);
+const mockedSelectEditAutoFollowPattern = jest.mocked(selectEditAutoFollowPattern);
+const mockedGetAutoFollowPattern = jest.mocked(getAutoFollowPattern);
+
+describe('auto_follow_pattern_edit.container mapDispatchToProps', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ dispatch.mockClear();
+ mockedUpdateAutoFollowPattern.mockClear();
+ mockedClearApiError.mockClear();
+ mockedSelectEditAutoFollowPattern.mockClear();
+ mockedGetAutoFollowPattern.mockClear();
+ });
+
+ describe('updateAutoFollowPattern', () => {
+ it('should strip unknown fields from the update payload (defense-in-depth against strict backend schema)', () => {
+ const actions = mapDispatchToProps(dispatch);
+
+ // Upstream sources of the pattern object (selectors, reducers, caller
+ // code) may include extra fields such as `name` or `errors`. The backend
+ // update route rejects unknown fields via
+ // `schema.object({...}).unknowns: 'forbid'` (default), so the container
+ // must not forward anything outside the documented contract.
+ const enrichedPayload = {
+ active: true,
+ remoteCluster: 'cluster-a',
+ leaderIndexPatterns: ['logs-*'],
+ followIndexPattern: 'replica-{{leader_index}}',
+ name: 'should-not-leak',
+ errors: ['should-not-leak'],
+ unexpected: 42,
+ } as unknown as Parameters<
+ ReturnType['updateAutoFollowPattern']
+ >[1];
+
+ actions.updateAutoFollowPattern('my-pattern', enrichedPayload);
+
+ expect(mockedUpdateAutoFollowPattern).toHaveBeenCalledTimes(1);
+ expect(mockedUpdateAutoFollowPattern).toHaveBeenCalledWith('my-pattern', {
+ active: true,
+ remoteCluster: 'cluster-a',
+ leaderIndexPatterns: ['logs-*'],
+ followIndexPattern: 'replica-{{leader_index}}',
+ });
+ });
+ });
+
+ describe('clearApiError', () => {
+ it('should clear both the -get and -save scopes', () => {
+ const actions = mapDispatchToProps(dispatch);
+ actions.clearApiError();
+
+ const calledScopes = mockedClearApiError.mock.calls.map(([scope]) => scope);
+ expect(calledScopes).toEqual(
+ expect.arrayContaining(['autoFollowPattern-get', 'autoFollowPattern-save'])
+ );
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.ts
similarity index 54%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.ts
index eb22e6cc8b7c4..b2868823d8e36 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.ts
@@ -6,8 +6,12 @@
*/
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { SECTIONS } from '../../constants';
+import type { AutoFollowPatternConfig } from '../../services/api';
+import type { CcrState } from '../../store';
import {
getApiStatus,
getApiError,
@@ -16,7 +20,7 @@ import {
} from '../../store/selectors';
import {
getAutoFollowPattern,
- saveAutoFollowPattern,
+ updateAutoFollowPattern,
selectEditAutoFollowPattern,
clearApiError,
} from '../../store/actions';
@@ -24,7 +28,9 @@ import { AutoFollowPatternEdit as AutoFollowPatternEditView } from './auto_follo
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
-const mapStateToProps = (state) => ({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
apiStatus: {
get: getApiStatus(`${scope}-get`)(state),
save: getApiStatus(`${scope}-save`)(state),
@@ -37,25 +43,23 @@ const mapStateToProps = (state) => ({
autoFollowPattern: getSelectedAutoFollowPattern('edit')(state),
});
-const mapDispatchToProps = (dispatch) => ({
- getAutoFollowPattern: (id) => dispatch(getAutoFollowPattern(id)),
- selectAutoFollowPattern: (id) => dispatch(selectEditAutoFollowPattern(id)),
- saveAutoFollowPattern: (id, autoFollowPattern) => {
- // Strip out errors.
+export const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ getAutoFollowPattern: (id: string) => dispatch(getAutoFollowPattern(id)),
+ selectAutoFollowPattern: (id: string | null) => dispatch(selectEditAutoFollowPattern(id)),
+ updateAutoFollowPattern: (id: string, autoFollowPattern: AutoFollowPatternConfig) => {
+ // Only forward the fields accepted by the update route's body schema
+ // (`schema.object({...}).unknowns: 'forbid'` by default). Upstream sources
+ // of `autoFollowPattern` (selectors, reducers) may include extra fields
+ // such as `name` or `errors`; forwarding those would produce a 400 at the
+ // server boundary.
const { active, remoteCluster, leaderIndexPatterns, followIndexPattern } = autoFollowPattern;
-
- dispatch(
- saveAutoFollowPattern(
- id,
- {
- active,
- remoteCluster,
- leaderIndexPatterns,
- followIndexPattern,
- },
- true
- )
- );
+ const updatePayload: AutoFollowPatternConfig = {
+ active,
+ remoteCluster,
+ leaderIndexPatterns,
+ followIndexPattern,
+ };
+ return dispatch(updateAutoFollowPattern(id, updatePayload));
},
clearApiError: () => {
dispatch(clearApiError(`${scope}-get`));
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.tsx
similarity index 63%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.tsx
index 6c381484891ea..9a899cb48a59d 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.tsx
@@ -6,42 +6,58 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import type { ReactNode } from 'react';
+import type { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiPageSection, EuiPageTemplate } from '@elastic/eui';
-import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
+import type { ApiStatus } from '../../../../common/types';
+import type { AutoFollowPatternWithErrors } from '../../store/selectors';
+import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs';
+import type { AutoFollowPatternConfig } from '../../services/api';
import {
AutoFollowPatternForm,
AutoFollowPatternPageTitle,
RemoteClustersProvider,
} from '../../components';
+import type { CcrApiError } from '../../services/http_error';
+import { getErrorBody, getErrorStatus } from '../../services/http_error';
import { API_STATUS } from '../../constants';
import { SectionLoading } from '../../../shared_imports';
-export class AutoFollowPatternEdit extends PureComponent {
- static propTypes = {
- getAutoFollowPattern: PropTypes.func.isRequired,
- selectAutoFollowPattern: PropTypes.func.isRequired,
- saveAutoFollowPattern: PropTypes.func.isRequired,
- clearApiError: PropTypes.func.isRequired,
- apiError: PropTypes.object.isRequired,
- apiStatus: PropTypes.object.isRequired,
- autoFollowPattern: PropTypes.object,
- autoFollowPatternId: PropTypes.string,
- };
-
- static getDerivedStateFromProps({ autoFollowPatternId }, { lastAutoFollowPatternId }) {
+export interface AutoFollowPatternEditProps extends RouteComponentProps<{ id: string }> {
+ getAutoFollowPattern: (id: string) => void;
+ selectAutoFollowPattern: (id: string | null) => void;
+ updateAutoFollowPattern: (id: string, autoFollowPattern: AutoFollowPatternConfig) => void;
+ clearApiError: () => void;
+ apiError: { get: CcrApiError | null; save: CcrApiError | null };
+ apiStatus: { get: ApiStatus; save: ApiStatus };
+ autoFollowPattern: AutoFollowPatternWithErrors | null;
+ autoFollowPatternId: string | null;
+}
+
+export interface AutoFollowPatternEditState {
+ lastAutoFollowPatternId: string | undefined;
+}
+
+export class AutoFollowPatternEdit extends PureComponent<
+ AutoFollowPatternEditProps,
+ AutoFollowPatternEditState
+> {
+ static getDerivedStateFromProps(
+ { autoFollowPatternId }: Pick,
+ { lastAutoFollowPatternId }: AutoFollowPatternEditState
+ ): Partial | null {
if (lastAutoFollowPatternId !== autoFollowPatternId) {
- return { lastAutoFollowPatternId: autoFollowPatternId };
+ return { lastAutoFollowPatternId: autoFollowPatternId ?? undefined };
}
return null;
}
- state = { lastAutoFollowPatternId: undefined };
+ state: AutoFollowPatternEditState = { lastAutoFollowPatternId: undefined };
componentDidMount() {
const {
@@ -57,14 +73,17 @@ export class AutoFollowPatternEdit extends PureComponent {
setBreadcrumbs([listBreadcrumb('/auto_follow_patterns'), editBreadcrumb]);
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(prevProps: AutoFollowPatternEditProps, prevState: AutoFollowPatternEditState) {
const { autoFollowPattern, getAutoFollowPattern } = this.props;
// Fetch the auto-follow pattern on the server if we don't have it (i.e. page reload)
if (
!autoFollowPattern &&
prevState.lastAutoFollowPatternId !== this.state.lastAutoFollowPatternId
) {
- getAutoFollowPattern(this.state.lastAutoFollowPatternId);
+ const id = this.state.lastAutoFollowPatternId;
+ if (id !== undefined) {
+ getAutoFollowPattern(id);
+ }
}
}
@@ -72,25 +91,27 @@ export class AutoFollowPatternEdit extends PureComponent {
this.props.clearApiError();
}
- renderGetAutoFollowPatternError(error) {
+ renderGetAutoFollowPatternError(error: CcrApiError) {
const {
match: {
params: { id: name },
},
} = this.props;
- const errorMessage =
- error.body.statusCode === 404
- ? {
- error: i18n.translate(
- 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorMessage',
- {
- defaultMessage: `The auto-follow pattern ''{name}'' does not exist.`,
- values: { name },
- }
- ),
- }
- : error;
+ const statusCode = getErrorStatus(error);
+ const body = getErrorBody(error);
+ const errorMessage: ReactNode =
+ statusCode === 404
+ ? i18n.translate(
+ 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorMessage',
+ {
+ defaultMessage: `The auto-follow pattern ''{name}'' does not exist.`,
+ values: { name },
+ }
+ )
+ : body?.message ?? error.message;
+
+ const listNav = reactRouterNavigate(this.props.history, `/auto_follow_patterns`);
return (
{errorMessage}
}
actions={
@@ -123,13 +144,13 @@ export class AutoFollowPatternEdit extends PureComponent {
);
}
- renderLoading(loadingTitle) {
+ renderLoading(loadingTitle: ReactNode) {
return {loadingTitle};
}
render() {
const {
- saveAutoFollowPattern,
+ updateAutoFollowPattern,
apiStatus,
apiError,
autoFollowPattern,
@@ -177,7 +198,7 @@ export class AutoFollowPatternEdit extends PureComponent {
currentUrl={currentUrl}
remoteClusters={error ? [] : remoteClusters}
autoFollowPattern={autoFollowPattern}
- saveAutoFollowPattern={saveAutoFollowPattern}
+ updateAutoFollowPattern={updateAutoFollowPattern}
saveButtonLabel={
({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
apiStatus: getApiStatus(`${scope}-save`)(state),
apiError: getApiError(`${scope}-save`)(state),
});
-const mapDispatchToProps = (dispatch) => ({
- saveFollowerIndex: (id, followerIndex) => dispatch(saveFollowerIndex(id, followerIndex)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ saveFollowerIndex: (id: string, followerIndex: FollowerIndexSaveBody) =>
+ dispatch(saveFollowerIndex(id, followerIndex)),
clearApiError: () => dispatch(clearApiError(`${scope}-save`)),
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.tsx
similarity index 81%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.tsx
index 91e262fa84818..9ff98e9257b72 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.tsx
@@ -6,11 +6,14 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import type { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPageSection } from '@elastic/eui';
+import type { ApiStatus } from '../../../../common/types';
import { setBreadcrumbs, listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs';
+import type { FollowerIndexSaveBody } from '../../services/api';
+import type { CcrApiError } from '../../services/http_error';
import {
FollowerIndexForm,
FollowerIndexPageTitle,
@@ -18,14 +21,14 @@ import {
} from '../../components';
import { SectionLoading } from '../../../shared_imports';
-export class FollowerIndexAdd extends PureComponent {
- static propTypes = {
- saveFollowerIndex: PropTypes.func.isRequired,
- clearApiError: PropTypes.func.isRequired,
- apiError: PropTypes.object,
- apiStatus: PropTypes.string.isRequired,
- };
+export interface FollowerIndexAddProps extends RouteComponentProps {
+ saveFollowerIndex: (name: string, followerIndex: FollowerIndexSaveBody) => void | Promise;
+ clearApiError: () => void;
+ apiError: CcrApiError | null;
+ apiStatus: ApiStatus;
+}
+export class FollowerIndexAdd extends PureComponent {
componentDidMount() {
setBreadcrumbs([listBreadcrumb('/follower_indices'), addBreadcrumb]);
}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_add/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.ts
similarity index 66%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.ts
index 2f4d6eb489acd..a17c3b37a2a89 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.ts
@@ -6,8 +6,12 @@
*/
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { SECTIONS } from '../../constants';
+import type { FollowerIndexSaveBody } from '../../services/api';
+import type { CcrState } from '../../store';
import {
getApiStatus,
getApiError,
@@ -24,7 +28,9 @@ import { FollowerIndexEdit as FollowerIndexEditView } from './follower_index_edi
const scope = SECTIONS.FOLLOWER_INDEX;
-const mapStateToProps = (state) => ({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
apiStatus: {
get: getApiStatus(`${scope}-get`)(state),
save: getApiStatus(`${scope}-save`)(state),
@@ -37,10 +43,11 @@ const mapStateToProps = (state) => ({
followerIndex: getSelectedFollowerIndex('edit')(state),
});
-const mapDispatchToProps = (dispatch) => ({
- getFollowerIndex: (id) => dispatch(getFollowerIndex(id)),
- selectFollowerIndex: (id) => dispatch(selectEditFollowerIndex(id)),
- saveFollowerIndex: (id, followerIndex) => dispatch(saveFollowerIndex(id, followerIndex, true)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ getFollowerIndex: (id: string) => dispatch(getFollowerIndex(id)),
+ selectFollowerIndex: (id: string | null) => dispatch(selectEditFollowerIndex(id)),
+ saveFollowerIndex: (id: string, followerIndex: FollowerIndexSaveBody) =>
+ dispatch(saveFollowerIndex(id, followerIndex, true)),
clearApiError: () => {
dispatch(clearApiError(`${scope}-get`));
dispatch(clearApiError(`${scope}-save`));
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.tsx
similarity index 71%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.tsx
index ee0cf671d9390..0f4fb602bb8fd 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.tsx
@@ -6,7 +6,8 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import type { ReactNode } from 'react';
+import type { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -18,8 +19,12 @@ import {
htmlIdGenerator,
} from '@elastic/eui';
-import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
+import type { ApiStatus, FollowerIndexWithPausedStatus } from '../../../../common/types';
+import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs';
+import type { CcrApiError } from '../../services/http_error';
+import { getErrorBody, getErrorStatus } from '../../services/http_error';
+import type { FollowerIndexSaveBody } from '../../services/api';
import {
FollowerIndexForm,
FollowerIndexPageTitle,
@@ -28,26 +33,39 @@ import {
import { API_STATUS } from '../../constants';
import { SectionLoading } from '../../../shared_imports';
-export class FollowerIndexEdit extends PureComponent {
- static propTypes = {
- getFollowerIndex: PropTypes.func.isRequired,
- selectFollowerIndex: PropTypes.func.isRequired,
- saveFollowerIndex: PropTypes.func.isRequired,
- clearApiError: PropTypes.func.isRequired,
- apiError: PropTypes.object.isRequired,
- apiStatus: PropTypes.object.isRequired,
- followerIndex: PropTypes.object,
- followerIndexId: PropTypes.string,
- };
+export interface FollowerIndexEditProps extends RouteComponentProps<{ id: string }> {
+ getFollowerIndex: (id: string) => void;
+ selectFollowerIndex: (id: string | null) => void;
+ saveFollowerIndex: (name: string, followerIndex: FollowerIndexSaveBody) => void;
+ clearApiError: () => void;
+ apiError: { get: CcrApiError | null; save: CcrApiError | null };
+ apiStatus: { get: ApiStatus; save: ApiStatus };
+ followerIndex: FollowerIndexWithPausedStatus | null;
+ followerIndexId: string | null;
+}
+
+export interface FollowerIndexEditState {
+ lastFollowerIndexId: string | undefined;
+ showConfirmModal: boolean;
+}
+
+export class FollowerIndexEdit extends PureComponent<
+ FollowerIndexEditProps,
+ FollowerIndexEditState
+> {
+ private editedFollowerIndexPayload?: { name: string; followerIndex: FollowerIndexSaveBody };
- static getDerivedStateFromProps({ followerIndexId }, { lastFollowerIndexId }) {
+ static getDerivedStateFromProps(
+ { followerIndexId }: Pick,
+ { lastFollowerIndexId }: FollowerIndexEditState
+ ): Partial | null {
if (lastFollowerIndexId !== followerIndexId) {
- return { lastFollowerIndexId: followerIndexId };
+ return { lastFollowerIndexId: followerIndexId ?? undefined };
}
return null;
}
- state = {
+ state: FollowerIndexEditState = {
lastFollowerIndexId: undefined,
showConfirmModal: false,
};
@@ -59,7 +77,7 @@ export class FollowerIndexEdit extends PureComponent {
},
selectFollowerIndex,
} = this.props;
- let decodedId;
+ let decodedId: string;
try {
// When we navigate through the router (history.push) we need to decode both the uri and the id
decodedId = decodeURI(id);
@@ -75,11 +93,14 @@ export class FollowerIndexEdit extends PureComponent {
setBreadcrumbs([listBreadcrumb('/follower_indices'), editBreadcrumb]);
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(prevProps: FollowerIndexEditProps, prevState: FollowerIndexEditState) {
const { followerIndex, getFollowerIndex } = this.props;
// Fetch the follower index on the server if we don't have it (i.e. page reload)
if (!followerIndex && prevState.lastFollowerIndexId !== this.state.lastFollowerIndexId) {
- getFollowerIndex(this.state.lastFollowerIndexId);
+ const id = this.state.lastFollowerIndexId;
+ if (id !== undefined) {
+ getFollowerIndex(id);
+ }
}
}
@@ -87,13 +108,17 @@ export class FollowerIndexEdit extends PureComponent {
this.props.clearApiError();
}
- saveFollowerIndex = (name, followerIndex) => {
+ saveFollowerIndex = (name: string, followerIndex: FollowerIndexSaveBody) => {
this.editedFollowerIndexPayload = { name, followerIndex };
this.showConfirmModal();
};
confirmSaveFollowerIhdex = () => {
- const { name, followerIndex } = this.editedFollowerIndexPayload;
+ const payload = this.editedFollowerIndexPayload;
+ if (!payload) {
+ return;
+ }
+ const { name, followerIndex } = payload;
this.props.saveFollowerIndex(name, followerIndex);
this.closeConfirmModal();
};
@@ -102,29 +127,31 @@ export class FollowerIndexEdit extends PureComponent {
closeConfirmModal = () => this.setState({ showConfirmModal: false });
- renderLoading(loadingTitle) {
+ renderLoading(loadingTitle: ReactNode) {
return {loadingTitle};
}
- renderGetFollowerIndexError(error) {
+ renderGetFollowerIndexError(error: CcrApiError) {
const {
match: {
params: { id: name },
},
} = this.props;
- const errorMessage =
- error.body.statusCode === 404
- ? {
- error: i18n.translate(
- 'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
- {
- defaultMessage: `The follower index ''{name}'' does not exist.`,
- values: { name },
- }
- ),
- }
- : error;
+ const statusCode = getErrorStatus(error);
+ const body = getErrorBody(error);
+ const errorMessage: ReactNode =
+ statusCode === 404
+ ? i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorMessage',
+ {
+ defaultMessage: `The follower index ''{name}'' does not exist.`,
+ values: { name },
+ }
+ )
+ : body?.message ?? error.message;
+
+ const listNav = reactRouterNavigate(this.props.history, `/follower_indices`);
return (
{errorMessage}}
actions={
{
- const {
- followerIndexId,
- followerIndex: { isPaused },
- } = this.props;
+ const { followerIndexId, followerIndex } = this.props;
+
+ if (!followerIndex) {
+ return null;
+ }
+
+ const { isPaused } = followerIndex;
const confirmModalTitleId = htmlIdGenerator()('confirmModalTitle');
const title = i18n.translate(
'xpack.crossClusterReplication.followerIndexEditForm.confirmModal.title',
@@ -226,9 +256,6 @@ export class FollowerIndexEdit extends PureComponent {
const { showConfirmModal } = this.state;
- /* remove non-editable properties */
- const { shards, ...rest } = followerIndex || {}; // eslint-disable-line no-unused-vars
-
if (apiStatus.get === API_STATUS.LOADING || !followerIndex) {
return this.renderLoading(
i18n.translate(
@@ -242,6 +269,8 @@ export class FollowerIndexEdit extends PureComponent {
return this.renderGetFollowerIndexError(apiError.get);
}
+ const { shards: _shards, ...rest } = followerIndex;
+
return (
{({ isLoading, error, remoteClusters }) => {
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/follower_index_edit/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.ts
similarity index 70%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.ts
index 78e4ddc0f87e3..5ff701af4a950 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.ts
@@ -6,8 +6,11 @@
*/
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { SECTIONS } from '../../../constants';
+import type { CcrState } from '../../../store';
import {
getListAutoFollowPatterns,
getSelectedAutoFollowPatternId,
@@ -24,7 +27,9 @@ import { AutoFollowPatternList as AutoFollowPatternListView } from './auto_follo
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
-const mapStateToProps = (state) => ({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
autoFollowPatterns: getListAutoFollowPatterns(state),
autoFollowPatternId: getSelectedAutoFollowPatternId('detail')(state),
apiStatus: getApiStatus(scope)(state),
@@ -32,9 +37,10 @@ const mapStateToProps = (state) => ({
isAuthorized: isApiAuthorized(scope)(state),
});
-const mapDispatchToProps = (dispatch) => ({
- loadAutoFollowPatterns: (inBackground) => dispatch(loadAutoFollowPatterns(inBackground)),
- selectAutoFollowPattern: (id) => dispatch(selectDetailAutoFollowPattern(id)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ loadAutoFollowPatterns: (inBackground?: boolean) =>
+ dispatch(loadAutoFollowPatterns(inBackground)),
+ selectAutoFollowPattern: (id: string | null) => dispatch(selectDetailAutoFollowPattern(id)),
loadAutoFollowStats: () => dispatch(loadAutoFollowStats()),
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.tsx
similarity index 68%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.tsx
index bd4028d6bac0f..6969b9bccc12e 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.tsx
@@ -6,7 +6,8 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import type { History } from 'history';
+import type { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiText, EuiSpacer, EuiPageTemplate } from '@elastic/eui';
@@ -14,27 +15,50 @@ import { EuiButton, EuiText, EuiSpacer, EuiPageTemplate } from '@elastic/eui';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { extractQueryParams, PageError, PageLoading } from '../../../../shared_imports';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
+import type { CcrApiError } from '../../../services/http_error';
+import { getErrorBody } from '../../../services/http_error';
import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants';
+import type { ApiStatus } from '../../../../../common/types';
+import type { ParsedAutoFollowPattern } from '../../../store/reducers/auto_follow_pattern';
import { AutoFollowPatternTable, DetailPanel } from './components';
const REFRESH_RATE_MS = 30000;
-const getQueryParamPattern = ({ location: { search } }) => {
- const { pattern } = extractQueryParams(search);
- return pattern ? decodeURIComponent(pattern) : null;
+const getQueryParamPattern = (history: History) => {
+ const { pattern } = extractQueryParams(history.location.search);
+ if (!pattern) {
+ return null;
+ }
+ const patternStr = Array.isArray(pattern) ? pattern[0] : pattern;
+ return decodeURIComponent(String(patternStr));
};
-export class AutoFollowPatternList extends PureComponent {
- static propTypes = {
- loadAutoFollowPatterns: PropTypes.func,
- selectAutoFollowPattern: PropTypes.func,
- loadAutoFollowStats: PropTypes.func,
- autoFollowPatterns: PropTypes.array,
- apiStatus: PropTypes.string,
- apiError: PropTypes.object,
- };
+export interface AutoFollowPatternListProps extends RouteComponentProps {
+ loadAutoFollowPatterns: (inBackground?: boolean) => void;
+ selectAutoFollowPattern: (id: string | null) => void;
+ loadAutoFollowStats: () => void;
+ autoFollowPatterns: ParsedAutoFollowPattern[];
+ autoFollowPatternId: string | null;
+ apiStatus: ApiStatus;
+ apiError: CcrApiError | null;
+ isAuthorized: boolean;
+}
+
+interface AutoFollowPatternListState {
+ lastAutoFollowPatternId: string | null;
+ isDetailPanelOpen: boolean;
+}
+
+export class AutoFollowPatternList extends PureComponent<
+ AutoFollowPatternListProps,
+ AutoFollowPatternListState
+> {
+ private interval?: ReturnType;
- static getDerivedStateFromProps({ autoFollowPatternId }, { lastAutoFollowPatternId }) {
+ static getDerivedStateFromProps(
+ { autoFollowPatternId }: Pick,
+ { lastAutoFollowPatternId }: AutoFollowPatternListState
+ ): Partial | null {
if (autoFollowPatternId !== lastAutoFollowPatternId) {
return {
lastAutoFollowPatternId: autoFollowPatternId,
@@ -44,7 +68,7 @@ export class AutoFollowPatternList extends PureComponent {
return null;
}
- state = {
+ state: AutoFollowPatternListState = {
lastAutoFollowPatternId: null,
isDetailPanelOpen: false,
};
@@ -64,7 +88,7 @@ export class AutoFollowPatternList extends PureComponent {
this.interval = setInterval(() => loadAutoFollowPatterns(true), REFRESH_RATE_MS);
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(prevProps: AutoFollowPatternListProps, prevState: AutoFollowPatternListState) {
const { history, loadAutoFollowStats } = this.props;
const { lastAutoFollowPatternId } = this.state;
@@ -88,7 +112,9 @@ export class AutoFollowPatternList extends PureComponent {
}
componentWillUnmount() {
- clearInterval(this.interval);
+ if (this.interval !== undefined) {
+ clearInterval(this.interval);
+ }
}
renderEmpty() {
@@ -164,18 +190,18 @@ export class AutoFollowPatternList extends PureComponent {
if (!isAuthorized) {
return (
- }
+ title={i18n.translate(
+ 'xpack.crossClusterReplication.autoFollowPatternList.permissionErrorTitle',
+ {
+ defaultMessage: 'Permission error',
+ }
+ )}
error={{
- error: (
-
+ error: i18n.translate(
+ 'xpack.crossClusterReplication.autoFollowPatternList.noPermissionText',
+ {
+ defaultMessage: 'You do not have permission to view or add auto-follow patterns.',
+ }
),
}}
/>
@@ -190,7 +216,9 @@ export class AutoFollowPatternList extends PureComponent {
}
);
- return ;
+ const body = getErrorBody(apiError);
+ const error = { error: body?.message ?? apiError.message, statusCode: body?.statusCode };
+ return ;
}
if (isEmpty) {
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.ts
similarity index 61%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.ts
index ea15a36ee6860..bc9573ec6d0fb 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.ts
@@ -6,8 +6,11 @@
*/
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { SECTIONS } from '../../../../../constants';
+import type { CcrState } from '../../../../../store';
import {
selectDetailAutoFollowPattern,
pauseAutoFollowPattern,
@@ -18,14 +21,16 @@ import { AutoFollowPatternTable as AutoFollowPatternTableComponent } from './aut
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
-const mapStateToProps = (state) => ({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
apiStatusDelete: getApiStatus(`${scope}-delete`)(state),
});
-const mapDispatchToProps = (dispatch) => ({
- selectAutoFollowPattern: (name) => dispatch(selectDetailAutoFollowPattern(name)),
- pauseAutoFollowPattern: (name) => dispatch(pauseAutoFollowPattern(name)),
- resumeAutoFollowPattern: (name) => dispatch(resumeAutoFollowPattern(name)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ selectAutoFollowPattern: (name: string) => dispatch(selectDetailAutoFollowPattern(name)),
+ pauseAutoFollowPattern: (name: string) => dispatch(pauseAutoFollowPattern(name)),
+ resumeAutoFollowPattern: (name: string) => dispatch(resumeAutoFollowPattern(name)),
});
export const AutoFollowPatternTable = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.tsx
similarity index 75%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.tsx
index 2dfc17c2c4f95..3823cea036de9 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.tsx
@@ -6,10 +6,10 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
+import type { EuiInMemoryTableProps, EuiSearchBarOnChangeArgs } from '@elastic/eui';
import {
EuiInMemoryTable,
EuiButton,
@@ -26,6 +26,8 @@ import {
} from '../../../../../components';
import { routing } from '../../../../../services/routing';
import { trackUiMetric } from '../../../../../services/track_ui_metric';
+import type { ApiStatus } from '../../../../../../../common/types';
+import type { ParsedAutoFollowPattern } from '../../../../../store/reducers/auto_follow_pattern';
const actionI18nTexts = {
pause: i18n.translate(
@@ -54,7 +56,10 @@ const actionI18nTexts = {
),
};
-const getFilteredPatterns = (autoFollowPatterns, queryText) => {
+const getFilteredPatterns = (
+ autoFollowPatterns: ParsedAutoFollowPattern[],
+ queryText: string
+): ParsedAutoFollowPattern[] => {
if (queryText) {
const normalizedSearchText = queryText.toLowerCase();
@@ -79,15 +84,29 @@ const getFilteredPatterns = (autoFollowPatterns, queryText) => {
return autoFollowPatterns;
};
-export class AutoFollowPatternTable extends PureComponent {
- static propTypes = {
- autoFollowPatterns: PropTypes.array,
- selectAutoFollowPattern: PropTypes.func.isRequired,
- pauseAutoFollowPattern: PropTypes.func.isRequired,
- resumeAutoFollowPattern: PropTypes.func.isRequired,
- };
+export interface AutoFollowPatternTableProps {
+ autoFollowPatterns: ParsedAutoFollowPattern[];
+ selectAutoFollowPattern: (name: string) => void;
+ pauseAutoFollowPattern: (name: string) => void;
+ resumeAutoFollowPattern: (name: string) => void;
+ apiStatusDelete: ApiStatus;
+}
+
+interface AutoFollowPatternTableState {
+ prevAutoFollowPatterns: ParsedAutoFollowPattern[];
+ selectedItems: string[];
+ filteredAutoFollowPatterns: ParsedAutoFollowPattern[];
+ queryText: string;
+}
- static getDerivedStateFromProps(props, state) {
+export class AutoFollowPatternTable extends PureComponent<
+ AutoFollowPatternTableProps,
+ AutoFollowPatternTableState
+> {
+ static getDerivedStateFromProps(
+ props: AutoFollowPatternTableProps,
+ state: AutoFollowPatternTableState
+ ): Partial | null {
const { autoFollowPatterns } = props;
const { prevAutoFollowPatterns, queryText } = state;
@@ -102,7 +121,7 @@ export class AutoFollowPatternTable extends PureComponent {
return null;
}
- constructor(props) {
+ constructor(props: AutoFollowPatternTableProps) {
super(props);
this.state = {
@@ -113,9 +132,9 @@ export class AutoFollowPatternTable extends PureComponent {
};
}
- onSearch = ({ query }) => {
+ onSearch = ({ query, queryText }: EuiSearchBarOnChangeArgs) => {
const { autoFollowPatterns } = this.props;
- const { text } = query;
+ const text = query?.text ?? queryText;
// We cache the filtered indices instead of calculating them inside render() because
// of https://github.com/elastic/eui/issues/3445.
@@ -125,7 +144,9 @@ export class AutoFollowPatternTable extends PureComponent {
});
};
- getTableColumns(deleteAutoFollowPattern) {
+ getTableColumns(
+ deleteAutoFollowPattern: (name: string) => void
+ ): EuiInMemoryTableProps['columns'] {
const { selectAutoFollowPattern } = this.props;
return [
@@ -139,7 +160,7 @@ export class AutoFollowPatternTable extends PureComponent {
),
sortable: true,
truncateText: false,
- render: (name) => {
+ render: (name: string) => {
return (
{
@@ -162,7 +183,7 @@ export class AutoFollowPatternTable extends PureComponent {
defaultMessage: 'Status',
}
),
- render: (active) => {
+ render: (active: boolean) => {
const statusText = active
? i18n.translate(
'xpack.crossClusterReplication.autoFollowPatternList.table.statusTextActive',
@@ -201,7 +222,7 @@ export class AutoFollowPatternTable extends PureComponent {
defaultMessage: 'Leader patterns',
}
),
- render: (leaderIndexPatterns) => leaderIndexPatterns.join(', '),
+ render: (leaderIndexPatterns: string[]) => leaderIndexPatterns.join(', '),
},
{
field: 'followIndexPatternPrefix',
@@ -232,33 +253,40 @@ export class AutoFollowPatternTable extends PureComponent {
),
actions: [
{
+ type: 'icon',
name: actionI18nTexts.pause,
description: actionI18nTexts.pause,
icon: 'pause',
- onClick: (item) => this.props.pauseAutoFollowPattern(item.name),
- available: (item) => item.active,
+ onClick: (item: ParsedAutoFollowPattern) =>
+ this.props.pauseAutoFollowPattern(item.name),
+ available: (item: ParsedAutoFollowPattern) => item.active,
'data-test-subj': 'contextMenuPauseButton',
},
{
+ type: 'icon',
name: actionI18nTexts.resume,
description: actionI18nTexts.resume,
icon: 'play',
- onClick: (item) => this.props.resumeAutoFollowPattern(item.name),
- available: (item) => !item.active,
+ onClick: (item: ParsedAutoFollowPattern) =>
+ this.props.resumeAutoFollowPattern(item.name),
+ available: (item: ParsedAutoFollowPattern) => !item.active,
'data-test-subj': 'contextMenuResumeButton',
},
{
+ type: 'icon',
name: actionI18nTexts.edit,
description: actionI18nTexts.edit,
icon: 'pencil',
- onClick: (item) => routing.navigate(routing.getAutoFollowPatternPath(item.name)),
+ onClick: (item: ParsedAutoFollowPattern) =>
+ routing.navigate(routing.getAutoFollowPatternPath(item.name)),
'data-test-subj': 'contextMenuEditButton',
},
{
+ type: 'icon',
name: actionI18nTexts.delete,
description: actionI18nTexts.delete,
icon: 'trash',
- onClick: (item) => deleteAutoFollowPattern(item.name),
+ onClick: (item: ParsedAutoFollowPattern) => deleteAutoFollowPattern(item.name),
'data-test-subj': 'contextMenuDeleteButton',
},
],
@@ -282,11 +310,12 @@ export class AutoFollowPatternTable extends PureComponent {
render() {
const { selectedItems, filteredAutoFollowPatterns } = this.state;
+ const reactRouter = routing.reactRouterOrThrow;
const sorting = {
sort: {
field: 'name',
- direction: 'asc',
+ direction: 'asc' as const,
},
};
@@ -296,22 +325,23 @@ export class AutoFollowPatternTable extends PureComponent {
};
const selection = {
- onSelectionChange: (selectedItems) =>
- this.setState({ selectedItems: selectedItems.map(({ name }) => name) }),
+ onSelectionChange: (rows: ParsedAutoFollowPattern[]) =>
+ this.setState({ selectedItems: rows.map(({ name }) => name) }),
};
- const search = {
+ const search: EuiInMemoryTableProps['search'] = {
toolsLeft: selectedItems.length ? (
- filteredAutoFollowPatterns.find((item) => item.name === name)
- )}
+ patterns={this.state.selectedItems
+ .map((name) => filteredAutoFollowPatterns.find((item) => item.name === name))
+ .filter((p): p is ParsedAutoFollowPattern => p !== undefined)}
/>
) : undefined,
toolsRight: (
({
- 'data-test-subj': `cell_${column.field}`,
+ 'data-test-subj': `cell_${'field' in column ? column.field : ''}`,
})}
data-test-subj="autoFollowPatternListTable"
tableCaption={i18n.translate(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.ts
similarity index 89%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.ts
index 4b8689a616635..a4bd67f71ceb4 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.ts
@@ -6,18 +6,19 @@
*/
import { connect } from 'react-redux';
-import { DetailPanel as DetailPanelView } from './detail_panel';
+import type { CcrState } from '../../../../../store';
import {
getSelectedAutoFollowPattern,
getSelectedAutoFollowPatternId,
getApiStatus,
} from '../../../../../store/selectors';
import { SECTIONS } from '../../../../../constants';
+import { DetailPanel as DetailPanelView } from './detail_panel';
const scope = SECTIONS.AUTO_FOLLOW_PATTERN;
-const mapStateToProps = (state) => ({
+const mapStateToProps = (state: CcrState) => ({
autoFollowPatternId: getSelectedAutoFollowPatternId('detail')(state),
autoFollowPattern: getSelectedAutoFollowPattern('detail')(state),
apiStatus: getApiStatus(scope)(state),
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.tsx
similarity index 93%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.tsx
index fd374735604a1..af469dd7ca9dd 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.tsx
@@ -6,7 +6,6 @@
*/
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n-react';
import moment from 'moment';
@@ -38,21 +37,24 @@ import {
AutoFollowPatternActionMenu,
} from '../../../../../components';
-export class DetailPanel extends Component {
- static propTypes = {
- apiStatus: PropTypes.string,
- autoFollowPatternId: PropTypes.string,
- autoFollowPattern: PropTypes.object,
- closeDetailPanel: PropTypes.func.isRequired,
- };
+import type { ApiStatus } from '../../../../../../../common/types';
+import type { AutoFollowPatternWithErrors } from '../../../../../store/selectors';
+export interface DetailPanelProps {
+ apiStatus: ApiStatus;
+ autoFollowPatternId: string | null;
+ autoFollowPattern: AutoFollowPatternWithErrors | null;
+ closeDetailPanel: () => void;
+}
+
+export class DetailPanel extends Component {
renderAutoFollowPattern({
followIndexPatternPrefix,
followIndexPatternSuffix,
remoteCluster,
leaderIndexPatterns,
active,
- }) {
+ }: AutoFollowPatternWithErrors) {
return (
<>
@@ -194,7 +196,11 @@ export class DetailPanel extends Component {
);
}
- renderIndicesPreview(prefix, suffix, leaderIndexPatterns) {
+ renderIndicesPreview(
+ prefix: string | undefined,
+ suffix: string | undefined,
+ leaderIndexPatterns: string[]
+ ) {
return (
- {autoFollowPattern.errors.map((error, i) => (
+ {pattern.errors.map((error, i) => (
-
{moment(error.timestamp).format('MMMM Do, YYYY h:mm:ss A')}:{' '}
{error.autoFollowException.reason}
@@ -274,6 +280,8 @@ export class DetailPanel extends Component {
return this.renderAutoFollowPatternNotFound();
}
+ const reactRouter = routing.reactRouterOrThrow;
+
const { followIndexPatternPrefix, followIndexPatternSuffix, leaderIndexPatterns } =
autoFollowPattern;
@@ -302,7 +310,7 @@ export class DetailPanel extends Component {
{
+ state: ContextMenuState = {
isPopoverOpen: false,
};
@@ -49,7 +58,7 @@ export class ContextMenu extends PureComponent {
});
};
- editFollowerIndex = (id) => {
+ editFollowerIndex = (id: string) => {
const uri = routing.getFollowerIndexPath(id);
routing.navigate(uri);
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.tsx b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.ts
similarity index 67%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.tsx
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.ts
index 35545e7501ae3..d993707d7410a 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.tsx
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.ts
@@ -6,7 +6,10 @@
*/
import { connect } from 'react-redux';
-import type { ApiStatus, FollowerIndex } from '../../../../../../../common/types';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
+
+import type { ApiStatus, FollowerIndexWithPausedStatus } from '../../../../../../../common/types';
import { DetailPanel as DetailPanelView } from './detail_panel';
import {
@@ -16,12 +19,15 @@ import {
} from '../../../../../store/selectors';
import { getFollowerIndex } from '../../../../../store/actions';
import { SECTIONS } from '../../../../../constants';
+import type { CcrState } from '../../../../../store';
const scope = SECTIONS.FOLLOWER_INDEX;
+type CcrDispatch = ThunkDispatch;
+
interface StateProps {
- followerIndexId: string;
- followerIndex: FollowerIndex;
+ followerIndexId: string | null;
+ followerIndex: FollowerIndexWithPausedStatus | null;
apiStatus: ApiStatus;
}
@@ -29,13 +35,13 @@ interface DispatchProps {
getFollowerIndex: (id: string) => void;
}
-const mapStateToProps = (state: any): StateProps => ({
+const mapStateToProps = (state: CcrState): StateProps => ({
followerIndexId: getSelectedFollowerIndexId('detail')(state),
followerIndex: getSelectedFollowerIndex('detail')(state),
apiStatus: getApiStatus(scope)(state),
});
-const mapDispatchToProps = (dispatch: any): DispatchProps => ({
+const mapDispatchToProps = (dispatch: CcrDispatch): DispatchProps => ({
getFollowerIndex: (id: string) => dispatch(getFollowerIndex(id)),
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.test.tsx b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.test.tsx
index 8767dc239669c..fbd37d48b3b3c 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.test.tsx
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.test.tsx
@@ -25,7 +25,7 @@ jest.mock('../../../../../services/routing', () => {
routing: {
getFollowerIndexPath: jest.fn(() => '/follower-index-path'),
navigate: jest.fn(),
- _reactRouter: {
+ reactRouter: {
getUrlForApp: jest.fn(() => '/mock-url'),
},
},
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.tsx b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.tsx
index ab56dff20ada8..734bd70878063 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.tsx
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.tsx
@@ -405,8 +405,8 @@ const FollowerIndexDetails = ({ followerIndex, isPollingStatus }: FollowerIndexD
export interface DetailPanelProps {
apiStatus?: ApiStatus;
- followerIndexId?: string;
- followerIndex?: FollowerIndexWithPausedStatus;
+ followerIndexId?: string | null;
+ followerIndex?: FollowerIndexWithPausedStatus | null;
closeDetailPanel: () => void;
getFollowerIndex: (id: string) => void;
}
@@ -553,7 +553,7 @@ export const DetailPanel = ({
({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
apiStatusDelete: getApiStatus(`${scope}-delete`)(state),
});
-const mapDispatchToProps = (dispatch) => ({
- selectFollowerIndex: (name) => dispatch(selectDetailFollowerIndex(name)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ selectFollowerIndex: (name: string) => dispatch(selectDetailFollowerIndex(name)),
});
export const FollowerIndicesTable = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.tsx
similarity index 75%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.tsx
index f0c50937ec85a..e71142d637c79 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.tsx
@@ -6,9 +6,13 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
+import type {
+ DefaultItemAction,
+ EuiInMemoryTableProps,
+ EuiSearchBarOnChangeArgs,
+} from '@elastic/eui';
import {
EuiHealth,
EuiButton,
@@ -18,12 +22,13 @@ import {
EuiOverlayMask,
} from '@elastic/eui';
+import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { API_STATUS, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK } from '../../../../../constants';
import { FollowerIndexActionsProvider } from '../../../../../components';
import { routing } from '../../../../../services/routing';
import { trackUiMetric } from '../../../../../services/track_ui_metric';
import { ContextMenu } from '../context_menu';
-import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
+import type { ApiStatus, FollowerIndexWithPausedStatus } from '../../../../../../../common/types';
const actionI18nTexts = {
pause: i18n.translate(
@@ -52,7 +57,10 @@ const actionI18nTexts = {
),
};
-const getFilteredIndices = (followerIndices, queryText) => {
+const getFilteredIndices = (
+ followerIndices: FollowerIndexWithPausedStatus[],
+ queryText: string
+): FollowerIndexWithPausedStatus[] => {
if (queryText) {
const normalizedSearchText = queryText.toLowerCase();
@@ -79,13 +87,27 @@ const getFilteredIndices = (followerIndices, queryText) => {
return followerIndices;
};
-export class FollowerIndicesTable extends PureComponent {
- static propTypes = {
- followerIndices: PropTypes.array,
- selectFollowerIndex: PropTypes.func.isRequired,
- };
+export interface FollowerIndicesTableProps {
+ followerIndices: FollowerIndexWithPausedStatus[];
+ selectFollowerIndex: (name: string) => void;
+ apiStatusDelete: ApiStatus;
+}
+
+interface FollowerIndicesTableState {
+ prevFollowerIndices: FollowerIndexWithPausedStatus[];
+ selectedItems: FollowerIndexWithPausedStatus[];
+ filteredIndices: FollowerIndexWithPausedStatus[];
+ queryText: string;
+}
- static getDerivedStateFromProps(props, state) {
+export class FollowerIndicesTable extends PureComponent<
+ FollowerIndicesTableProps,
+ FollowerIndicesTableState
+> {
+ static getDerivedStateFromProps(
+ props: FollowerIndicesTableProps,
+ state: FollowerIndicesTableState
+ ): Partial | null {
const { followerIndices } = props;
const { prevFollowerIndices, queryText } = state;
@@ -100,7 +122,7 @@ export class FollowerIndicesTable extends PureComponent {
return null;
}
- constructor(props) {
+ constructor(props: FollowerIndicesTableProps) {
super(props);
this.state = {
@@ -111,9 +133,9 @@ export class FollowerIndicesTable extends PureComponent {
};
}
- onSearch = ({ query }) => {
+ onSearch = ({ query, queryText }: EuiSearchBarOnChangeArgs) => {
const { followerIndices } = this.props;
- const { text } = query;
+ const text = query?.text ?? queryText;
// We cache the filtered indices instead of calculating them inside render() because
// of https://github.com/elastic/eui/issues/3445.
@@ -123,46 +145,56 @@ export class FollowerIndicesTable extends PureComponent {
});
};
- editFollowerIndex = (id) => {
+ editFollowerIndex = (id: string) => {
const uri = routing.getFollowerIndexPath(id);
routing.navigate(uri);
};
- getTableColumns(actionHandlers) {
+ getTableColumns(actionHandlers: {
+ pauseFollowerIndex: (item: FollowerIndexWithPausedStatus) => void;
+ resumeFollowerIndex: (name: string) => void;
+ unfollowLeaderIndex: (name: string) => void;
+ }): EuiInMemoryTableProps['columns'] {
const { selectFollowerIndex } = this.props;
- const actions = [
+ const actions: Array> = [
/* Pause follower index */
{
+ type: 'icon',
name: actionI18nTexts.pause,
description: actionI18nTexts.pause,
icon: 'pause',
- onClick: (item) => actionHandlers.pauseFollowerIndex(item),
- available: (item) => !item.isPaused,
+ onClick: (item: FollowerIndexWithPausedStatus) => actionHandlers.pauseFollowerIndex(item),
+ available: (item: FollowerIndexWithPausedStatus) => !item.isPaused,
'data-test-subj': 'pauseButton',
},
/* Resume follower index */
{
+ type: 'icon',
name: actionI18nTexts.resume,
description: actionI18nTexts.resume,
icon: 'play',
- onClick: (item) => actionHandlers.resumeFollowerIndex(item.name),
- available: (item) => item.isPaused,
+ onClick: (item: FollowerIndexWithPausedStatus) =>
+ actionHandlers.resumeFollowerIndex(item.name),
+ available: (item: FollowerIndexWithPausedStatus) => !!item.isPaused,
'data-test-subj': 'resumeButton',
},
/* Edit follower index */
{
+ type: 'icon',
name: actionI18nTexts.edit,
description: actionI18nTexts.edit,
- onClick: (item) => this.editFollowerIndex(item.name),
+ onClick: (item: FollowerIndexWithPausedStatus) => this.editFollowerIndex(item.name),
icon: 'pencil',
'data-test-subj': 'editButton',
},
/* Unfollow leader index */
{
+ type: 'icon',
name: actionI18nTexts.unfollow,
description: actionI18nTexts.unfollow,
- onClick: (item) => actionHandlers.unfollowLeaderIndex(item.name),
+ onClick: (item: FollowerIndexWithPausedStatus) =>
+ actionHandlers.unfollowLeaderIndex(item.name),
icon: 'chartThreshold',
'data-test-subj': 'unfollowButton',
},
@@ -179,7 +211,7 @@ export class FollowerIndicesTable extends PureComponent {
),
sortable: true,
truncateText: false,
- render: (name) => {
+ render: (name: string) => {
return (
{
@@ -203,7 +235,7 @@ export class FollowerIndicesTable extends PureComponent {
),
truncateText: true,
sortable: true,
- render: (isPaused) => {
+ render: (isPaused: boolean) => {
return isPaused ? (
this.setState({ selectedItems: newSelectedItems }),
+ onSelectionChange: (newSelectedItems: FollowerIndexWithPausedStatus[]) =>
+ this.setState({ selectedItems: newSelectedItems }),
};
- const search = {
+ const search: EuiInMemoryTableProps['search'] = {
toolsLeft: selectedItems.length ? (
) : undefined,
toolsRight: (
({
- 'data-test-subj': `cell-${column.field}`,
+ 'data-test-subj': `cell-${'field' in column ? column.field : ''}`,
})}
data-test-subj="followerIndexListTable"
/>
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.ts
similarity index 68%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.ts
index 087fbc938fbc1..6db7e1edd9baa 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.ts
@@ -6,8 +6,11 @@
*/
import { connect } from 'react-redux';
+import type { AnyAction } from 'redux';
+import type { ThunkDispatch } from 'redux-thunk';
import { SECTIONS } from '../../../constants';
+import type { CcrState } from '../../../store';
import {
getListFollowerIndices,
getSelectedFollowerIndexId,
@@ -20,7 +23,9 @@ import { FollowerIndicesList as FollowerIndicesListView } from './follower_indic
const scope = SECTIONS.FOLLOWER_INDEX;
-const mapStateToProps = (state) => ({
+type CcrDispatch = ThunkDispatch;
+
+const mapStateToProps = (state: CcrState) => ({
followerIndices: getListFollowerIndices(state),
followerIndexId: getSelectedFollowerIndexId('detail')(state),
apiStatus: getApiStatus(scope)(state),
@@ -28,9 +33,9 @@ const mapStateToProps = (state) => ({
isAuthorized: isApiAuthorized(scope)(state),
});
-const mapDispatchToProps = (dispatch) => ({
- loadFollowerIndices: (inBackground) => dispatch(loadFollowerIndices(inBackground)),
- selectFollowerIndex: (id) => dispatch(selectDetailFollowerIndex(id)),
+const mapDispatchToProps = (dispatch: CcrDispatch) => ({
+ loadFollowerIndices: (inBackground?: boolean) => dispatch(loadFollowerIndices(inBackground)),
+ selectFollowerIndex: (id: string | null) => dispatch(selectDetailFollowerIndex(id)),
});
export const FollowerIndicesList = connect(
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.tsx
similarity index 69%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.tsx
index c5502199118f2..4992d43caadc1 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.tsx
@@ -6,7 +6,8 @@
*/
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import type { History } from 'history';
+import type { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiText, EuiSpacer, EuiPageTemplate } from '@elastic/eui';
@@ -14,26 +15,48 @@ import { EuiButton, EuiText, EuiSpacer, EuiPageTemplate } from '@elastic/eui';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { extractQueryParams, PageLoading, PageError } from '../../../../shared_imports';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
+import type { CcrApiError } from '../../../services/http_error';
+import { getErrorBody } from '../../../services/http_error';
import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants';
+import type { ApiStatus, FollowerIndexWithPausedStatus } from '../../../../../common/types';
import { FollowerIndicesTable, DetailPanel } from './components';
const REFRESH_RATE_MS = 30000;
-const getQueryParamName = ({ location: { search } }) => {
- const { name } = extractQueryParams(search);
- return name ? decodeURIComponent(name) : null;
+const getQueryParamName = (history: History) => {
+ const { name } = extractQueryParams(history.location.search);
+ if (!name) {
+ return null;
+ }
+ const nameStr = Array.isArray(name) ? name[0] : name;
+ return decodeURIComponent(String(nameStr));
};
-export class FollowerIndicesList extends PureComponent {
- static propTypes = {
- loadFollowerIndices: PropTypes.func,
- selectFollowerIndex: PropTypes.func,
- followerIndices: PropTypes.array,
- apiStatus: PropTypes.string,
- apiError: PropTypes.object,
- };
+export interface FollowerIndicesListProps extends RouteComponentProps {
+ loadFollowerIndices: (inBackground?: boolean) => void;
+ selectFollowerIndex: (id: string | null) => void;
+ followerIndices: FollowerIndexWithPausedStatus[];
+ followerIndexId: string | null;
+ apiStatus: ApiStatus;
+ apiError: CcrApiError | null;
+ isAuthorized: boolean;
+}
+
+interface FollowerIndicesListState {
+ lastFollowerIndexId: string | null;
+ isDetailPanelOpen: boolean;
+}
+
+export class FollowerIndicesList extends PureComponent<
+ FollowerIndicesListProps,
+ FollowerIndicesListState
+> {
+ private interval?: ReturnType;
- static getDerivedStateFromProps({ followerIndexId }, { lastFollowerIndexId }) {
+ static getDerivedStateFromProps(
+ { followerIndexId }: Pick,
+ { lastFollowerIndexId }: FollowerIndicesListState
+ ): Partial | null {
if (followerIndexId !== lastFollowerIndexId) {
return {
lastFollowerIndexId: followerIndexId,
@@ -43,7 +66,7 @@ export class FollowerIndicesList extends PureComponent {
return null;
}
- state = {
+ state: FollowerIndicesListState = {
lastFollowerIndexId: null,
isDetailPanelOpen: false,
};
@@ -61,7 +84,7 @@ export class FollowerIndicesList extends PureComponent {
this.interval = setInterval(() => loadFollowerIndices(true), REFRESH_RATE_MS);
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(prevProps: FollowerIndicesListProps, prevState: FollowerIndicesListState) {
const { history } = this.props;
const { lastFollowerIndexId } = this.state;
@@ -86,7 +109,9 @@ export class FollowerIndicesList extends PureComponent {
}
componentWillUnmount() {
- clearInterval(this.interval);
+ if (this.interval !== undefined) {
+ clearInterval(this.interval);
+ }
}
renderEmpty() {
@@ -170,18 +195,18 @@ export class FollowerIndicesList extends PureComponent {
if (!isAuthorized) {
return (
- }
+ title={i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexList.permissionErrorTitle',
+ {
+ defaultMessage: 'Permission error',
+ }
+ )}
error={{
- error: (
-
+ error: i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexList.noPermissionText',
+ {
+ defaultMessage: 'You do not have permission to view or add follower indices.',
+ }
),
}}
/>
@@ -196,7 +221,9 @@ export class FollowerIndicesList extends PureComponent {
}
);
- return ;
+ const body = getErrorBody(apiError);
+ const error = { error: body?.message ?? apiError.message, statusCode: body?.statusCode };
+ return ;
}
if (isEmpty) {
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.container.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.container.ts
similarity index 60%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.container.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.container.ts
index 7739937021af9..6ff3a90d5c0b1 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.container.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.container.ts
@@ -6,8 +6,10 @@
*/
import { connect } from 'react-redux';
+import type { RouteComponentProps } from 'react-router-dom';
import { SECTIONS } from '../../constants';
+import type { CcrState } from '../../store';
import {
getListAutoFollowPatterns,
getListFollowerIndices,
@@ -15,14 +17,19 @@ import {
} from '../../store/selectors';
import { CrossClusterReplicationHome as CrossClusterReplicationHomeView } from './home';
-const mapStateToProps = (state) => ({
+const mapStateToProps = (state: CcrState) => ({
autoFollowPatterns: getListAutoFollowPatterns(state),
isAutoFollowApiAuthorized: isApiAuthorized(SECTIONS.AUTO_FOLLOW_PATTERN)(state),
followerIndices: getListFollowerIndices(state),
isFollowerIndexApiAuthorized: isApiAuthorized(SECTIONS.FOLLOWER_INDEX)(state),
});
-export const CrossClusterReplicationHome = connect(
- mapStateToProps,
- null
-)(CrossClusterReplicationHomeView);
+type CrossClusterReplicationHomeOwnProps = RouteComponentProps<{ section: string }>;
+type CrossClusterReplicationHomeStateProps = ReturnType;
+
+export const CrossClusterReplicationHome = connect<
+ CrossClusterReplicationHomeStateProps,
+ {},
+ CrossClusterReplicationHomeOwnProps,
+ CcrState
+>(mapStateToProps)(CrossClusterReplicationHomeView);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.tsx
similarity index 71%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.tsx
index 68bb384aa218c..b556c7773d25a 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/home.tsx
@@ -6,22 +6,46 @@
*/
import React, { PureComponent } from 'react';
+import type { ReactNode } from 'react';
+import type { RouteComponentProps } from 'react-router-dom';
import { Route, Routes } from '@kbn/shared-ux-router';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiPageHeader } from '@elastic/eui';
+import type { FollowerIndexWithPausedStatus } from '../../../../common/types';
+import type { ParsedAutoFollowPattern } from '../../store/reducers/auto_follow_pattern';
import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs';
import { routing } from '../../services/routing';
import { AutoFollowPatternList } from './auto_follow_pattern_list';
import { FollowerIndicesList } from './follower_indices_list';
-export class CrossClusterReplicationHome extends PureComponent {
- state = {
+export interface CrossClusterReplicationHomeProps extends RouteComponentProps<{ section: string }> {
+ autoFollowPatterns: ParsedAutoFollowPattern[];
+ isAutoFollowApiAuthorized: boolean;
+ followerIndices: FollowerIndexWithPausedStatus[];
+ isFollowerIndexApiAuthorized: boolean;
+}
+
+interface CrossClusterReplicationHomeState {
+ activeSection: string;
+}
+
+interface HomeTab {
+ id: string;
+ name: ReactNode;
+ testSubj: string;
+}
+
+export class CrossClusterReplicationHome extends PureComponent<
+ CrossClusterReplicationHomeProps,
+ CrossClusterReplicationHomeState
+> {
+ state: CrossClusterReplicationHomeState = {
activeSection: 'follower_indices',
};
- tabs = [
+ readonly tabs: HomeTab[] = [
{
id: 'follower_indices',
name: (
@@ -48,7 +72,7 @@ export class CrossClusterReplicationHome extends PureComponent {
setBreadcrumbs([listBreadcrumb()]);
}
- static getDerivedStateFromProps(props) {
+ static getDerivedStateFromProps(props: CrossClusterReplicationHomeProps) {
const {
match: {
params: { section },
@@ -59,7 +83,7 @@ export class CrossClusterReplicationHome extends PureComponent {
};
}
- onSectionChange = (section) => {
+ onSectionChange = (section: string) => {
setBreadcrumbs([listBreadcrumb(`/${section}`)]);
routing.navigate(`/${section}`);
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/home/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.d.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.d.ts
deleted file mode 100644
index 93d441399b722..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * 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 declare const CrossClusterReplicationHome: any;
-export declare const AutoFollowPatternAdd: any;
-export declare const AutoFollowPatternEdit: any;
-export declare const FollowerIndexAdd: any;
-export declare const FollowerIndexEdit: any;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/sections/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.ts.snap
similarity index 98%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.ts.snap
index 46659f167a919..335d81b3acf23 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.ts.snap
@@ -30,6 +30,5 @@ Object {
/>,
"leaderIndexPatterns": null,
"name": "Name can't begin with an underscore.",
- "otherProp": null,
}
`;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/api.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/api.js
deleted file mode 100644
index 8067b2cc11b9a..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/api.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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 {
- API_BASE_PATH,
- API_REMOTE_CLUSTERS_BASE_PATH,
- API_INDEX_MANAGEMENT_BASE_PATH,
-} from '../../../common/constants';
-import { arrify } from '../../../common/services/utils';
-import {
- UIM_FOLLOWER_INDEX_CREATE,
- UIM_FOLLOWER_INDEX_UPDATE,
- UIM_FOLLOWER_INDEX_PAUSE,
- UIM_FOLLOWER_INDEX_PAUSE_MANY,
- UIM_FOLLOWER_INDEX_RESUME,
- UIM_FOLLOWER_INDEX_RESUME_MANY,
- UIM_FOLLOWER_INDEX_UNFOLLOW,
- UIM_FOLLOWER_INDEX_UNFOLLOW_MANY,
- UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS,
- UIM_AUTO_FOLLOW_PATTERN_CREATE,
- UIM_AUTO_FOLLOW_PATTERN_UPDATE,
- UIM_AUTO_FOLLOW_PATTERN_DELETE,
- UIM_AUTO_FOLLOW_PATTERN_DELETE_MANY,
- UIM_AUTO_FOLLOW_PATTERN_PAUSE,
- UIM_AUTO_FOLLOW_PATTERN_PAUSE_MANY,
- UIM_AUTO_FOLLOW_PATTERN_RESUME,
- UIM_AUTO_FOLLOW_PATTERN_RESUME_MANY,
-} from '../constants';
-import { trackUserRequest } from './track_ui_metric';
-import { areAllSettingsDefault } from './follower_index_default_settings';
-
-let httpClient;
-
-export function setHttpClient(client) {
- httpClient = client;
-}
-
-export const getHttpClient = () => {
- return httpClient;
-};
-
-// ---
-
-const createIdString = (ids) => ids.map((id) => encodeURIComponent(id)).join(',');
-
-/* Auto Follow Pattern */
-export const loadAutoFollowPatterns = (asSystemRequest) =>
- httpClient.get(`${API_BASE_PATH}/auto_follow_patterns`, { asSystemRequest });
-
-export const getAutoFollowPattern = (id) =>
- httpClient.get(`${API_BASE_PATH}/auto_follow_patterns/${encodeURIComponent(id)}`);
-
-export const loadRemoteClusters = () => httpClient.get(API_REMOTE_CLUSTERS_BASE_PATH);
-
-export const createAutoFollowPattern = (autoFollowPattern) => {
- const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns`, {
- body: JSON.stringify(autoFollowPattern),
- });
- return trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_CREATE);
-};
-
-export const updateAutoFollowPattern = (id, autoFollowPattern) => {
- const request = httpClient.put(
- `${API_BASE_PATH}/auto_follow_patterns/${encodeURIComponent(id)}`,
- { body: JSON.stringify(autoFollowPattern) }
- );
- return trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_UPDATE);
-};
-
-export const deleteAutoFollowPattern = (id) => {
- const ids = arrify(id);
- const idString = ids.map((_id) => encodeURIComponent(_id)).join(',');
- const request = httpClient.delete(`${API_BASE_PATH}/auto_follow_patterns/${idString}`);
- const uiMetric =
- ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_DELETE_MANY : UIM_AUTO_FOLLOW_PATTERN_DELETE;
- return trackUserRequest(request, uiMetric);
-};
-
-export const pauseAutoFollowPattern = (id) => {
- const ids = arrify(id);
- const idString = ids.map(encodeURIComponent).join(',');
- const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns/${idString}/pause`);
-
- const uiMetric =
- ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_PAUSE_MANY : UIM_AUTO_FOLLOW_PATTERN_PAUSE;
- return trackUserRequest(request, uiMetric);
-};
-
-export const resumeAutoFollowPattern = (id) => {
- const ids = arrify(id);
- const idString = ids.map(encodeURIComponent).join(',');
- const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns/${idString}/resume`);
-
- const uiMetric =
- ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_RESUME_MANY : UIM_AUTO_FOLLOW_PATTERN_RESUME;
- return trackUserRequest(request, uiMetric);
-};
-
-/* Follower Index */
-export const loadFollowerIndices = (asSystemRequest) =>
- httpClient.get(`${API_BASE_PATH}/follower_indices`, { asSystemRequest });
-
-export const getFollowerIndex = (id) =>
- httpClient.get(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`);
-
-export const createFollowerIndex = (followerIndex) => {
- const uiMetrics = [UIM_FOLLOWER_INDEX_CREATE];
- const isUsingAdvancedSettings = !areAllSettingsDefault(followerIndex);
- if (isUsingAdvancedSettings) {
- uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS);
- }
- const request = httpClient.post(`${API_BASE_PATH}/follower_indices`, {
- body: JSON.stringify(followerIndex),
- });
- return trackUserRequest(request, uiMetrics);
-};
-
-export const pauseFollowerIndex = (id) => {
- const ids = arrify(id);
- const idString = createIdString(ids);
- const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${idString}/pause`);
- const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_PAUSE_MANY : UIM_FOLLOWER_INDEX_PAUSE;
- return trackUserRequest(request, uiMetric);
-};
-
-export const resumeFollowerIndex = (id) => {
- const ids = arrify(id);
- const idString = createIdString(ids);
- const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${idString}/resume`);
- const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_RESUME_MANY : UIM_FOLLOWER_INDEX_RESUME;
- return trackUserRequest(request, uiMetric);
-};
-
-export const unfollowLeaderIndex = (id) => {
- const ids = arrify(id);
- const idString = createIdString(ids);
- const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${idString}/unfollow`);
- const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_UNFOLLOW_MANY : UIM_FOLLOWER_INDEX_UNFOLLOW;
- return trackUserRequest(request, uiMetric);
-};
-
-export const updateFollowerIndex = (id, followerIndex) => {
- const uiMetrics = [UIM_FOLLOWER_INDEX_UPDATE];
- const isUsingAdvancedSettings = !areAllSettingsDefault(followerIndex);
- if (isUsingAdvancedSettings) {
- uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS);
- }
-
- const {
- maxReadRequestOperationCount,
- maxOutstandingReadRequests,
- maxReadRequestSize,
- maxWriteRequestOperationCount,
- maxWriteRequestSize,
- maxOutstandingWriteRequests,
- maxWriteBufferCount,
- maxWriteBufferSize,
- maxRetryDelay,
- readPollTimeout,
- } = followerIndex;
-
- const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`, {
- body: JSON.stringify({
- maxReadRequestOperationCount,
- maxOutstandingReadRequests,
- maxReadRequestSize,
- maxWriteRequestOperationCount,
- maxWriteRequestSize,
- maxOutstandingWriteRequests,
- maxWriteBufferCount,
- maxWriteBufferSize,
- maxRetryDelay,
- readPollTimeout,
- }),
- });
-
- return trackUserRequest(request, uiMetrics);
-};
-
-/* Stats */
-export const loadAutoFollowStats = () => httpClient.get(`${API_BASE_PATH}/stats/auto_follow`);
-
-/* Indices */
-let abortController = null;
-export const loadIndices = () => {
- if (abortController) {
- abortController.abort();
- abortController = null;
- }
- abortController = new AbortController();
- const { signal } = abortController;
- return httpClient
- .get(`${API_INDEX_MANAGEMENT_BASE_PATH}/indices`, { signal })
- .then((response) => {
- abortController = null;
- return response;
- });
-};
-
-export const loadPermissions = () => httpClient.get(`${API_BASE_PATH}/permissions`);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/api.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/api.ts
new file mode 100644
index 0000000000000..ebe5f178ff33f
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/api.ts
@@ -0,0 +1,310 @@
+/*
+ * 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 type { HttpSetup } from '@kbn/core/public';
+import type { AutoFollowPattern, FollowerIndex } from '../../../common/types';
+import {
+ API_BASE_PATH,
+ API_REMOTE_CLUSTERS_BASE_PATH,
+ API_INDEX_MANAGEMENT_BASE_PATH,
+} from '../../../common/constants';
+import { arrify } from '../../../common/services/utils';
+import {
+ UIM_FOLLOWER_INDEX_CREATE,
+ UIM_FOLLOWER_INDEX_UPDATE,
+ UIM_FOLLOWER_INDEX_PAUSE,
+ UIM_FOLLOWER_INDEX_PAUSE_MANY,
+ UIM_FOLLOWER_INDEX_RESUME,
+ UIM_FOLLOWER_INDEX_RESUME_MANY,
+ UIM_FOLLOWER_INDEX_UNFOLLOW,
+ UIM_FOLLOWER_INDEX_UNFOLLOW_MANY,
+ UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS,
+ UIM_AUTO_FOLLOW_PATTERN_CREATE,
+ UIM_AUTO_FOLLOW_PATTERN_UPDATE,
+ UIM_AUTO_FOLLOW_PATTERN_DELETE,
+ UIM_AUTO_FOLLOW_PATTERN_DELETE_MANY,
+ UIM_AUTO_FOLLOW_PATTERN_PAUSE,
+ UIM_AUTO_FOLLOW_PATTERN_PAUSE_MANY,
+ UIM_AUTO_FOLLOW_PATTERN_RESUME,
+ UIM_AUTO_FOLLOW_PATTERN_RESUME_MANY,
+} from '../constants';
+import { trackUserRequest } from './track_ui_metric';
+import { areAllSettingsDefault } from './follower_index_default_settings';
+
+export type AutoFollowPatternCreateConfig = Pick<
+ AutoFollowPattern,
+ 'remoteCluster' | 'leaderIndexPatterns' | 'followIndexPattern'
+>;
+export type AutoFollowPatternConfig = AutoFollowPatternCreateConfig &
+ Pick;
+
+export interface CreateAutoFollowPatternRequest extends AutoFollowPatternCreateConfig {
+ id: string;
+}
+
+export interface BulkIdError {
+ id: string;
+}
+
+export interface DeleteAutoFollowPatternResponse {
+ errors: BulkIdError[];
+ itemsDeleted: string[];
+}
+
+export interface PauseAutoFollowPatternResponse {
+ errors: BulkIdError[];
+ itemsPaused: string[];
+}
+
+export interface ResumeAutoFollowPatternResponse {
+ errors: BulkIdError[];
+ itemsResumed: string[];
+}
+
+export interface RemoteClusterRow {
+ name: string;
+ isConnected: boolean;
+}
+
+export type FollowerIndexSaveBody = Omit;
+
+export interface PauseFollowerIndexResponse {
+ errors: BulkIdError[];
+ itemsPaused: string[];
+}
+
+export interface ResumeFollowerIndexResponse {
+ errors: BulkIdError[];
+ itemsResumed: string[];
+}
+
+export interface UnfollowLeaderIndexResponse {
+ errors: BulkIdError[];
+ itemsUnfollowed: string[];
+ itemsNotOpen: string[];
+}
+
+let httpClient: HttpSetup;
+
+export function setHttpClient(client: HttpSetup): void {
+ httpClient = client;
+}
+
+export const getHttpClient = (): HttpSetup => {
+ return httpClient;
+};
+
+// ---
+
+const createIdString = (ids: string[]): string => ids.map((id) => encodeURIComponent(id)).join(',');
+
+/* Auto Follow Pattern */
+export const loadAutoFollowPatterns = (
+ asSystemRequest: boolean
+): Promise<{ patterns: AutoFollowPattern[] }> =>
+ httpClient.get<{ patterns: AutoFollowPattern[] }>(`${API_BASE_PATH}/auto_follow_patterns`, {
+ asSystemRequest,
+ });
+
+export const getAutoFollowPattern = (id: string): Promise =>
+ httpClient.get(
+ `${API_BASE_PATH}/auto_follow_patterns/${encodeURIComponent(id)}`
+ );
+
+export const loadRemoteClusters = (): Promise =>
+ httpClient.get(API_REMOTE_CLUSTERS_BASE_PATH);
+
+export const createAutoFollowPattern = async (
+ autoFollowPattern: CreateAutoFollowPatternRequest
+): Promise => {
+ const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns`, {
+ body: JSON.stringify(autoFollowPattern),
+ });
+ await trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_CREATE);
+};
+
+export const updateAutoFollowPattern = async (
+ id: string,
+ autoFollowPattern: AutoFollowPatternConfig
+): Promise => {
+ const request = httpClient.put(
+ `${API_BASE_PATH}/auto_follow_patterns/${encodeURIComponent(id)}`,
+ {
+ body: JSON.stringify(autoFollowPattern),
+ }
+ );
+ await trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_UPDATE);
+};
+
+export const deleteAutoFollowPattern = (
+ id: string | string[]
+): Promise => {
+ const ids = arrify(id);
+ const idString = ids.map((_id) => encodeURIComponent(_id)).join(',');
+ const request = httpClient.delete(
+ `${API_BASE_PATH}/auto_follow_patterns/${idString}`
+ );
+ const uiMetric =
+ ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_DELETE_MANY : UIM_AUTO_FOLLOW_PATTERN_DELETE;
+ return trackUserRequest(request, uiMetric);
+};
+
+export const pauseAutoFollowPattern = (
+ id: string | string[]
+): Promise => {
+ const ids = arrify(id);
+ const idString = ids.map(encodeURIComponent).join(',');
+ const request = httpClient.post(
+ `${API_BASE_PATH}/auto_follow_patterns/${idString}/pause`
+ );
+
+ const uiMetric =
+ ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_PAUSE_MANY : UIM_AUTO_FOLLOW_PATTERN_PAUSE;
+ return trackUserRequest(request, uiMetric);
+};
+
+export const resumeAutoFollowPattern = (
+ id: string | string[]
+): Promise => {
+ const ids = arrify(id);
+ const idString = ids.map(encodeURIComponent).join(',');
+ const request = httpClient.post(
+ `${API_BASE_PATH}/auto_follow_patterns/${idString}/resume`
+ );
+
+ const uiMetric =
+ ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_RESUME_MANY : UIM_AUTO_FOLLOW_PATTERN_RESUME;
+ return trackUserRequest(request, uiMetric);
+};
+
+/* Follower Index */
+export const loadFollowerIndices = (
+ asSystemRequest: boolean
+): Promise<{ indices: FollowerIndex[] }> =>
+ httpClient.get<{ indices: FollowerIndex[] }>(`${API_BASE_PATH}/follower_indices`, {
+ asSystemRequest,
+ });
+
+export const getFollowerIndex = (id: string): Promise =>
+ httpClient.get(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`);
+
+export const createFollowerIndex = async (
+ followerIndex: FollowerIndexSaveBody & { name: string }
+): Promise => {
+ const uiMetrics = [UIM_FOLLOWER_INDEX_CREATE];
+ const isUsingAdvancedSettings = !areAllSettingsDefault(followerIndex);
+ if (isUsingAdvancedSettings) {
+ uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS);
+ }
+ const request = httpClient.post(`${API_BASE_PATH}/follower_indices`, {
+ body: JSON.stringify(followerIndex),
+ });
+ await trackUserRequest(request, uiMetrics);
+};
+
+export const pauseFollowerIndex = (id: string | string[]): Promise => {
+ const ids = arrify(id);
+ const idString = createIdString(ids);
+ const request = httpClient.put(
+ `${API_BASE_PATH}/follower_indices/${idString}/pause`
+ );
+ const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_PAUSE_MANY : UIM_FOLLOWER_INDEX_PAUSE;
+ return trackUserRequest(request, uiMetric);
+};
+
+export const resumeFollowerIndex = (
+ id: string | string[]
+): Promise => {
+ const ids = arrify(id);
+ const idString = createIdString(ids);
+ const request = httpClient.put(
+ `${API_BASE_PATH}/follower_indices/${idString}/resume`
+ );
+ const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_RESUME_MANY : UIM_FOLLOWER_INDEX_RESUME;
+ return trackUserRequest(request, uiMetric);
+};
+
+export const unfollowLeaderIndex = (
+ id: string | string[]
+): Promise => {
+ const ids = arrify(id);
+ const idString = createIdString(ids);
+ const request = httpClient.put(
+ `${API_BASE_PATH}/follower_indices/${idString}/unfollow`
+ );
+ const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_UNFOLLOW_MANY : UIM_FOLLOWER_INDEX_UNFOLLOW;
+ return trackUserRequest(request, uiMetric);
+};
+
+export const updateFollowerIndex = async (
+ id: string,
+ followerIndex: FollowerIndexSaveBody
+): Promise => {
+ const uiMetrics = [UIM_FOLLOWER_INDEX_UPDATE];
+ const isUsingAdvancedSettings = !areAllSettingsDefault(followerIndex);
+ if (isUsingAdvancedSettings) {
+ uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS);
+ }
+
+ const {
+ maxReadRequestOperationCount,
+ maxOutstandingReadRequests,
+ maxReadRequestSize,
+ maxWriteRequestOperationCount,
+ maxWriteRequestSize,
+ maxOutstandingWriteRequests,
+ maxWriteBufferCount,
+ maxWriteBufferSize,
+ maxRetryDelay,
+ readPollTimeout,
+ } = followerIndex;
+
+ const request = httpClient.put(
+ `${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`,
+ {
+ body: JSON.stringify({
+ maxReadRequestOperationCount,
+ maxOutstandingReadRequests,
+ maxReadRequestSize,
+ maxWriteRequestOperationCount,
+ maxWriteRequestSize,
+ maxOutstandingWriteRequests,
+ maxWriteBufferCount,
+ maxWriteBufferSize,
+ maxRetryDelay,
+ readPollTimeout,
+ }),
+ }
+ );
+
+ await trackUserRequest(request, uiMetrics);
+};
+
+/* Stats */
+export const loadAutoFollowStats = () => httpClient.get(`${API_BASE_PATH}/stats/auto_follow`);
+
+/* Indices */
+let abortController: AbortController | null = null;
+export const loadIndices = async (): Promise> => {
+ if (abortController) {
+ abortController.abort();
+ abortController = null;
+ }
+ abortController = new AbortController();
+ const { signal } = abortController;
+ const response = await httpClient.get>(
+ `${API_INDEX_MANAGEMENT_BASE_PATH}/indices`,
+ { signal }
+ );
+ abortController = null;
+ return response;
+};
+
+export const loadPermissions = (): Promise<{
+ hasPermission: boolean;
+ missingClusterPrivileges: string[];
+}> => httpClient.get(`${API_BASE_PATH}/permissions`);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.test.ts
similarity index 57%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.test.ts
index 488fbd399dba6..9041a8499d95d 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.test.ts
@@ -5,20 +5,24 @@
* 2.0.
*/
+import type { RecentAutoFollowError } from '../../../common/types';
import { parseAutoFollowErrors } from './auto_follow_errors';
describe('Auto-follow pattern errors service', () => {
it('should convert an array of error to an object where each key is an auto-follow pattern id', () => {
- const esErrors = [
+ const esErrors: RecentAutoFollowError[] = [
{
+ timestamp: 1,
leaderIndex: 'some:id::kibana_sample_4',
autoFollowException: { type: 'exception', reason: 'Error 1' },
},
{
+ timestamp: 2,
leaderIndex: 'another-id:mock:kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 2' },
},
{
+ timestamp: 3,
leaderIndex: 'some:id::kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 3' },
},
@@ -28,6 +32,7 @@ describe('Auto-follow pattern errors service', () => {
'another-id:mock': [
{
id: 'another-id:mock',
+ timestamp: 2,
leaderIndex: 'another-id:mock:kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 2' },
},
@@ -35,11 +40,13 @@ describe('Auto-follow pattern errors service', () => {
'some:id:': [
{
id: 'some:id:',
+ timestamp: 1,
leaderIndex: 'some:id::kibana_sample_4',
autoFollowException: { type: 'exception', reason: 'Error 1' },
},
{
id: 'some:id:',
+ timestamp: 3,
leaderIndex: 'some:id::kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 3' },
},
@@ -50,14 +57,42 @@ describe('Auto-follow pattern errors service', () => {
});
it('should limit the number of error to show for each pattern', () => {
- const esErrors = [
- { leaderIndex: 'my-id:kibana-1' },
- { leaderIndex: 'my-id:kibana-2' },
- { leaderIndex: 'my-id:kibana-3' },
- { leaderIndex: 'my-id:kibana-4' },
- { leaderIndex: 'my-id:kibana-5' },
- { leaderIndex: 'my-id:kibana-6' },
- { leaderIndex: 'my-id:kibana-7' },
+ const esErrors: RecentAutoFollowError[] = [
+ {
+ timestamp: 1,
+ leaderIndex: 'my-id:kibana-1',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
+ {
+ timestamp: 2,
+ leaderIndex: 'my-id:kibana-2',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
+ {
+ timestamp: 3,
+ leaderIndex: 'my-id:kibana-3',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
+ {
+ timestamp: 4,
+ leaderIndex: 'my-id:kibana-4',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
+ {
+ timestamp: 5,
+ leaderIndex: 'my-id:kibana-5',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
+ {
+ timestamp: 6,
+ leaderIndex: 'my-id:kibana-6',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
+ {
+ timestamp: 7,
+ leaderIndex: 'my-id:kibana-7',
+ autoFollowException: { type: 'exception', reason: 'e' },
+ },
];
expect(parseAutoFollowErrors(esErrors)['my-id'].length).toEqual(5);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.ts
similarity index 58%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.ts
index 4458ef7f3a66b..8e45e26f8de2c 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_errors.ts
@@ -5,7 +5,21 @@
* 2.0.
*/
-export const parseAutoFollowError = (error) => {
+import type { RecentAutoFollowError } from '../../../common/types';
+
+export interface ParsedAutoFollowError {
+ id: string;
+ timestamp: number;
+ leaderIndex: string;
+ autoFollowException: {
+ type: string;
+ reason: string;
+ };
+}
+
+export const parseAutoFollowError = (
+ error: RecentAutoFollowError
+): ParsedAutoFollowError | null => {
if (!error.leaderIndex) {
return null;
}
@@ -25,11 +39,14 @@ export const parseAutoFollowError = (error) => {
* Parse an array of auto-follow pattern errors and returns
* an object where each key is an auto-follow pattern id
*/
-export const parseAutoFollowErrors = (recentAutoFollowErrors, maxErrorsToShow = 5) =>
+export const parseAutoFollowErrors = (
+ recentAutoFollowErrors: RecentAutoFollowError[],
+ maxErrorsToShow = 5
+): Record =>
recentAutoFollowErrors
.map(parseAutoFollowError)
- .filter((error) => error !== null)
- .reduce((byId, error) => {
+ .filter((error): error is ParsedAutoFollowError => error !== null)
+ .reduce>((byId, error) => {
if (!byId[error.id]) {
byId[error.id] = [];
}
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.test.ts
similarity index 97%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.test.ts
index b77aa8924f8b4..e42fe620fab43 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.test.ts
@@ -13,9 +13,9 @@ import {
describe('Auto-follo pattern service', () => {
describe('getPreviewIndicesFromAutoFollowPattern()', () => {
- let prefix;
- let suffix;
- let leaderIndexPatterns;
+ let prefix: string;
+ let suffix: string;
+ let leaderIndexPatterns: string[];
beforeEach(() => {
prefix = 'prefix_';
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.ts
similarity index 69%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.ts
index b91ca413e02fd..a0f50d56f9f5f 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern.ts
@@ -7,7 +7,20 @@
import moment from 'moment';
-const getFollowPattern = (prefix = '', suffix = '', template = '{{leader_index}}') => ({
+export interface FollowPattern {
+ followPattern: {
+ prefix: string;
+ suffix: string;
+ template: string;
+ };
+ toString: string;
+}
+
+const getFollowPattern = (
+ prefix = '',
+ suffix = '',
+ template = '{{leader_index}}'
+): FollowPattern => ({
followPattern: {
prefix,
suffix,
@@ -16,6 +29,14 @@ const getFollowPattern = (prefix = '', suffix = '', template = '{{leader_index}}
toString: prefix + template + suffix,
});
+interface GetPreviewIndicesFromAutoFollowPatternParams {
+ prefix: string;
+ suffix: string;
+ leaderIndexPatterns: string[];
+ limit?: number;
+ wildcardPlaceHolders?: string[];
+}
+
/**
* Generate an array of indices preview that would be generated for an auto-follow pattern.
* It concatenates the prefix + the leader index pattern populated with values + the suffix
@@ -33,10 +54,13 @@ export const getPreviewIndicesFromAutoFollowPattern = ({
moment().add(1, 'days').format('YYYY-MM-DD'),
moment().add(2, 'days').format('YYYY-MM-DD'),
],
-}) => {
- const indicesPreview = [];
- let indexPreview;
- let leaderIndexTemplate;
+}: GetPreviewIndicesFromAutoFollowPatternParams): {
+ indicesPreview: FollowPattern[];
+ hasMore: boolean;
+} => {
+ const indicesPreview: FollowPattern[] = [];
+ let indexPreview: FollowPattern;
+ let leaderIndexTemplate: string;
leaderIndexPatterns.forEach((leaderIndexPattern) => {
wildcardPlaceHolders.forEach((placeHolder) => {
@@ -57,7 +81,12 @@ export const getPreviewIndicesFromAutoFollowPattern = ({
};
};
-export const getPrefixSuffixFromFollowPattern = (followPattern) => {
+export const getPrefixSuffixFromFollowPattern = (
+ followPattern: string
+): {
+ followIndexPatternPrefix: string | undefined;
+ followIndexPatternSuffix: string | undefined;
+} => {
let followIndexPatternPrefix;
let followIndexPatternSuffix;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.ts
similarity index 75%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.ts
index 721fde01aa2b5..bd9c348d49a6b 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.ts
@@ -14,6 +14,17 @@ describe('Auto-follow pattern validators', () => {
expect(errors).toMatchSnapshot();
});
+ it('does not validate fields whose values are undefined', () => {
+ const errors = validateAutoFollowPattern({
+ name: undefined,
+ leaderIndexPatterns: undefined,
+ followIndexPatternPrefix: undefined,
+ followIndexPatternSuffix: undefined,
+ });
+
+ expect(errors).toEqual({});
+ });
+
it('should validate all props from auto-follow patten', () => {
const autoFollowPattern = {
name: '_wrong-name',
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.tsx
similarity index 75%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.tsx
index 17911c8ac960f..a8d4fe54376c6 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import type { ReactElement, ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@@ -18,7 +19,7 @@ import { indices } from '../../shared_imports';
const { indexNameBeginsWithPeriod, findIllegalCharactersInIndexName, indexNameContainsSpaces } =
indices;
-export const validateName = (name = '') => {
+export const validateName = (name = ''): string | null => {
let errorMsg = null;
if (!name || !name.trim()) {
@@ -53,7 +54,9 @@ export const validateName = (name = '') => {
return errorMsg;
};
-export const validateLeaderIndexPattern = (indexPattern) => {
+export const validateLeaderIndexPattern = (
+ indexPattern: string | undefined
+): { message: ReactNode } | null => {
if (indexPattern) {
const errors = validateDataView(indexPattern);
@@ -99,7 +102,9 @@ export const validateLeaderIndexPattern = (indexPattern) => {
return null;
};
-export const validateLeaderIndexPatterns = (indexPatterns) => {
+export const validateLeaderIndexPatterns = (
+ indexPatterns: string[]
+): { message: ReactNode } | null => {
// We only need to check if a value has been provided, because validation for this field
// has already been executed as the user has entered input into it.
if (!indexPatterns.length) {
@@ -116,7 +121,7 @@ export const validateLeaderIndexPatterns = (indexPatterns) => {
return null;
};
-export const validatePrefix = (prefix) => {
+export const validatePrefix = (prefix: string): ReactElement | null => {
// If it's empty, it is valid
if (!prefix || !prefix.trim()) {
return null;
@@ -160,7 +165,7 @@ export const validatePrefix = (prefix) => {
return null;
};
-export const validateSuffix = (suffix) => {
+export const validateSuffix = (suffix: string): ReactElement | null => {
// If it's empty, it is valid
if (!suffix || !suffix.trim()) {
return null;
@@ -194,35 +199,47 @@ export const validateSuffix = (suffix) => {
return null;
};
-export const validateAutoFollowPattern = (autoFollowPattern = {}) => {
- const errors = {};
- let error = null;
- let fieldValue;
-
- Object.keys(autoFollowPattern).forEach((fieldName) => {
- fieldValue = autoFollowPattern[fieldName];
- error = null;
-
- switch (fieldName) {
- case 'name':
- error = validateName(fieldValue);
- break;
-
- case 'leaderIndexPatterns':
- error = validateLeaderIndexPatterns(fieldValue);
- break;
+export interface AutoFollowPatternValidationFields {
+ name?: string;
+ leaderIndexPatterns?: string[];
+ followIndexPatternPrefix?: string;
+ followIndexPatternSuffix?: string;
+ remoteCluster?: string;
+}
+
+export interface MessageError {
+ message: ReactNode;
+ alwaysVisible?: boolean;
+}
+
+export interface AutoFollowPatternValidationErrors {
+ name?: string | null;
+ leaderIndexPatterns?: MessageError | null;
+ followIndexPatternPrefix?: ReactElement | null;
+ followIndexPatternSuffix?: ReactElement | null;
+ remoteCluster?: MessageError | null;
+}
+
+export const validateAutoFollowPattern = (
+ autoFollowPattern: AutoFollowPatternValidationFields = {}
+): AutoFollowPatternValidationErrors => {
+ const errors: AutoFollowPatternValidationErrors = {};
+
+ if (autoFollowPattern.name !== undefined) {
+ errors.name = validateName(autoFollowPattern.name);
+ }
- case 'followIndexPatternPrefix':
- error = validatePrefix(fieldValue);
- break;
+ if (autoFollowPattern.leaderIndexPatterns !== undefined) {
+ errors.leaderIndexPatterns = validateLeaderIndexPatterns(autoFollowPattern.leaderIndexPatterns);
+ }
- case 'followIndexPatternSuffix':
- error = validateSuffix(fieldValue);
- break;
- }
+ if (autoFollowPattern.followIndexPatternPrefix !== undefined) {
+ errors.followIndexPatternPrefix = validatePrefix(autoFollowPattern.followIndexPatternPrefix);
+ }
- errors[fieldName] = error;
- });
+ if (autoFollowPattern.followIndexPatternSuffix !== undefined) {
+ errors.followIndexPatternSuffix = validateSuffix(autoFollowPattern.followIndexPatternSuffix);
+ }
return errors;
};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/follower_index_default_settings.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/follower_index_default_settings.js
deleted file mode 100644
index e571d2c6ef35d..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/follower_index_default_settings.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants';
-
-export const getSettingDefault = (name) => {
- if (!FOLLOWER_INDEX_ADVANCED_SETTINGS[name]) {
- throw new Error(`Unknown setting ${name}`);
- }
-
- return FOLLOWER_INDEX_ADVANCED_SETTINGS[name];
-};
-
-export const isSettingDefault = (name, value) => {
- return getSettingDefault(name) === value;
-};
-
-export const areAllSettingsDefault = (settings) => {
- return Object.keys(FOLLOWER_INDEX_ADVANCED_SETTINGS).every((name) =>
- isSettingDefault(name, settings[name])
- );
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/follower_index_default_settings.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/follower_index_default_settings.ts
new file mode 100644
index 0000000000000..f5ac41cdbb82e
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/follower_index_default_settings.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 type { FollowerIndexAdvancedSettings } from '../../../common/types';
+import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants';
+
+type FollowerIndexAdvancedSettingName = keyof typeof FOLLOWER_INDEX_ADVANCED_SETTINGS;
+type SettingDefaultValue =
+ (typeof FOLLOWER_INDEX_ADVANCED_SETTINGS)[FollowerIndexAdvancedSettingName];
+
+export const getSettingDefault = (name: FollowerIndexAdvancedSettingName): SettingDefaultValue => {
+ if (!Object.prototype.hasOwnProperty.call(FOLLOWER_INDEX_ADVANCED_SETTINGS, name)) {
+ throw new Error(`Unknown setting ${name}`);
+ }
+
+ return FOLLOWER_INDEX_ADVANCED_SETTINGS[name];
+};
+
+/**
+ * A setting is considered default when it is `undefined` (the user has not
+ * entered a value) or when the entered value equals the documented default.
+ */
+export const isSettingDefault = (
+ name: FollowerIndexAdvancedSettingName,
+ value: SettingDefaultValue | undefined
+): boolean => {
+ return value === undefined || getSettingDefault(name) === value;
+};
+
+export const areAllSettingsDefault = (settings: FollowerIndexAdvancedSettings): boolean => {
+ return (
+ Object.keys(FOLLOWER_INDEX_ADVANCED_SETTINGS) as FollowerIndexAdvancedSettingName[]
+ ).every((name) => isSettingDefault(name, settings[name]));
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/get_remote_cluster_name.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/get_remote_cluster_name.js
deleted file mode 100644
index 187811bf166d1..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/get_remote_cluster_name.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.
- */
-
-const getFirstConnectedCluster = (clusters) => {
- for (let i = 0; i < clusters.length; i++) {
- if (clusters[i].isConnected) {
- return clusters[i];
- }
- }
-
- // No cluster connected, we return the first one in the list
- return clusters.length ? clusters[0] : {};
-};
-
-export const getRemoteClusterName = (remoteClusters, selected) => {
- return selected && remoteClusters.some((c) => c.name === selected)
- ? selected
- : getFirstConnectedCluster(remoteClusters).name;
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/get_remote_cluster_name.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/get_remote_cluster_name.ts
new file mode 100644
index 0000000000000..e85d63718679f
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/get_remote_cluster_name.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 type { RemoteClusterRow } from './api';
+
+const getFirstConnectedCluster = (
+ clusters: RemoteClusterRow[]
+): RemoteClusterRow | Record => {
+ for (let i = 0; i < clusters.length; i++) {
+ if (clusters[i].isConnected) {
+ return clusters[i];
+ }
+ }
+
+ // No cluster connected, we return the first one in the list
+ return clusters.length ? clusters[0] : {};
+};
+
+export const getRemoteClusterName = (
+ remoteClusters: RemoteClusterRow[],
+ selected: string | undefined
+): string | undefined => {
+ if (selected && remoteClusters.some((c) => c.name === selected)) {
+ return selected;
+ }
+ const first = getFirstConnectedCluster(remoteClusters);
+ return 'name' in first ? first.name : undefined;
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/http_error.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/http_error.ts
new file mode 100644
index 0000000000000..8c15891feb1bc
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/http_error.ts
@@ -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 type { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
+
+export type CcrApiError = Error | IHttpFetchError;
+
+export const toCcrApiError = (error: unknown): CcrApiError => {
+ if (error instanceof Error) {
+ return error;
+ }
+
+ return new Error(String(error));
+};
+
+export const isHttpFetchError = (
+ error: CcrApiError
+): error is IHttpFetchError => {
+ return 'request' in error;
+};
+
+export const getErrorStatus = (error: CcrApiError | null | undefined): number | undefined => {
+ if (!error) {
+ return;
+ }
+
+ if (!isHttpFetchError(error)) {
+ return;
+ }
+
+ return error.response?.status ?? error.body?.statusCode;
+};
+
+export const getErrorBody = (
+ error: CcrApiError | null | undefined
+): ResponseErrorBody | undefined => {
+ if (!error) {
+ return;
+ }
+
+ return isHttpFetchError(error) ? error.body : undefined;
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/input_validation.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/input_validation.tsx
similarity index 81%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/input_validation.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/input_validation.tsx
index 143542fb2d59e..c0030821c9fc1 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/input_validation.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/input_validation.tsx
@@ -6,22 +6,24 @@
*/
import React from 'react';
+import type { ReactElement } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { indices } from '../../shared_imports';
-const isEmpty = (value) => {
+const isEmpty = (value: string | undefined): boolean => {
return !value || !value.trim().length;
};
-const hasSpaces = (value) => (typeof value === 'string' ? value.includes(' ') : false);
+const hasSpaces = (value: string): boolean =>
+ typeof value === 'string' ? value.includes(' ') : false;
-const beginsWithPeriod = (value) => {
+const beginsWithPeriod = (value: string): boolean => {
return value[0] === '.';
};
-const findIllegalCharacters = (value) => {
- return indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => {
+const findIllegalCharacters = (value: string): string[] => {
+ return indices.INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => {
if (value.includes(char)) {
chars.push(char);
}
@@ -30,7 +32,7 @@ const findIllegalCharacters = (value) => {
}, []);
};
-export const indexNameValidator = (value) => {
+export const indexNameValidator = (value: string): ReactElement[] | undefined => {
if (isEmpty(value)) {
return [
{
return undefined;
};
-export const leaderIndexValidator = (value) => {
+export const leaderIndexValidator = (value: string): ReactElement[] | undefined => {
if (isEmpty(value)) {
return [
{
+export const init = (toasts: IToasts, fatalErrors: FatalErrorsSetup): void => {
_toasts = toasts;
_fatalErrors = fatalErrors;
};
-export const getToasts = () => _toasts;
-export const getFatalErrors = () => _fatalErrors;
+export const getToasts = (): IToasts => {
+ if (_toasts === undefined) {
+ throw new Error('CCR notifications were used before init() was called');
+ }
+ return _toasts;
+};
+
+export const getFatalErrors = (): FatalErrorsSetup => {
+ if (_fatalErrors === undefined) {
+ throw new Error('CCR notifications were used before init() was called');
+ }
+ return _fatalErrors;
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.d.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.d.ts
deleted file mode 100644
index 54ed44629ed78..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * 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 declare const routing: any;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.js
deleted file mode 100644
index 77b8ed6c58123..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * This file based on guidance from https://github.com/elastic/eui/blob/main/wiki/consuming-eui/react-router.md
- */
-
-import { stringify } from 'query-string';
-import { BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants';
-
-const queryParamsFromObject = (params, encodeParams = false) => {
- if (!params) {
- return;
- }
-
- const paramsStr = stringify(params, { sort: false, encode: encodeParams });
- return `?${paramsStr}`;
-};
-
-class Routing {
- _reactRouter = null;
-
- getHrefToRemoteClusters(route = '/', params, encodeParams = false) {
- const search = queryParamsFromObject(params, encodeParams) || '';
- return this._reactRouter.getUrlForApp('management', {
- path: `${BASE_PATH_REMOTE_CLUSTERS}${route}${search}`,
- });
- }
-
- navigate(route = '/home', params, encodeParams = false) {
- const search = queryParamsFromObject(params, encodeParams);
-
- this._reactRouter.history.push({
- pathname: encodeURI(route),
- search,
- });
- }
-
- getAutoFollowPatternPath = (name, section = '/edit') => {
- return encodeURI(`/auto_follow_patterns${section}/${encodeURIComponent(name)}`);
- };
-
- getFollowerIndexPath = (name, section = '/edit') => {
- return encodeURI(`/follower_indices${section}/${encodeURIComponent(name)}`);
- };
-
- get reactRouter() {
- return this._reactRouter;
- }
-
- set reactRouter(router) {
- this._reactRouter = router;
- }
-}
-
-export const routing = new Routing();
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.ts
new file mode 100644
index 0000000000000..0325d87117c09
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/routing.ts
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+/**
+ * This file based on guidance from https://github.com/elastic/eui/blob/main/wiki/consuming-eui/react-router.md
+ */
+
+import type { History } from 'history';
+import type { ApplicationStart } from '@kbn/core/public';
+import { stringify } from 'query-string';
+import { BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants';
+
+export type QueryParamValue = string | number | boolean | null;
+export type QueryParams = Record;
+
+const queryParamsFromObject = (
+ params: QueryParams | undefined | null,
+ encodeParams = false
+): string | undefined => {
+ if (!params) {
+ return;
+ }
+
+ const paramsStr = stringify(params, { sort: false, encode: encodeParams });
+ return `?${paramsStr}`;
+};
+
+export interface CcrReactRouter {
+ history: History;
+ route: {
+ location: History['location'];
+ };
+ getUrlForApp: ApplicationStart['getUrlForApp'];
+}
+
+class Routing {
+ #reactRouter: CcrReactRouter | null = null;
+
+ getHrefToRemoteClusters(route = '/', params?: QueryParams | null, encodeParams = false): string {
+ const search = queryParamsFromObject(params, encodeParams) || '';
+ return this.reactRouterOrThrow.getUrlForApp('management', {
+ path: `${BASE_PATH_REMOTE_CLUSTERS}${route}${search}`,
+ });
+ }
+
+ navigate(route = '/home', params?: QueryParams | null, encodeParams = false): void {
+ const search = queryParamsFromObject(params, encodeParams);
+
+ this.reactRouterOrThrow.history.push({
+ pathname: encodeURI(route),
+ search,
+ });
+ }
+
+ getAutoFollowPatternPath = (name: string, section = '/edit'): string => {
+ return encodeURI(`/auto_follow_patterns${section}/${encodeURIComponent(name)}`);
+ };
+
+ getFollowerIndexPath = (name: string, section = '/edit'): string => {
+ return encodeURI(`/follower_indices${section}/${encodeURIComponent(name)}`);
+ };
+
+ public get reactRouter(): CcrReactRouter | null {
+ return this.#reactRouter;
+ }
+
+ public set reactRouter(router: CcrReactRouter) {
+ this.#reactRouter = router;
+ }
+
+ public get reactRouterOrThrow(): CcrReactRouter {
+ if (!this.#reactRouter) {
+ throw new Error('CCR routing was used before reactRouter was set');
+ }
+ return this.#reactRouter;
+ }
+}
+
+export const routing = new Routing();
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/track_ui_metric.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/track_ui_metric.ts
index 2fd2b23f045c2..3de9a5f8f2677 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/track_ui_metric.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/track_ui_metric.ts
@@ -14,7 +14,7 @@ import { UIM_APP_NAME } from '../constants';
export { METRIC_TYPE };
// usageCollection is an optional dependency, so we default to a no-op.
-export let trackUiMetric = (metricType: UiCounterMetricType, eventName: string) => {};
+export let trackUiMetric = (metricType: UiCounterMetricType, eventName: string | string[]) => {};
export function init(usageCollection: UsageCollectionSetup): void {
trackUiMetric = usageCollection.reportUiCounter.bind(usageCollection, UIM_APP_NAME);
@@ -24,7 +24,10 @@ export function init(usageCollection: UsageCollectionSetup): void {
* Transparently return provided request Promise, while allowing us to track
* a successful completion of the request.
*/
-export function trackUserRequest(request: Promise, actionType: string) {
+export function trackUserRequest(
+ request: Promise,
+ actionType: string | string[]
+): Promise {
// Only track successful actions.
return request.then((response) => {
// It looks like we're using the wrong type here, added via
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.js
deleted file mode 100644
index 1d24b68713dc0..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * 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 const objectToArray = (obj) => Object.keys(obj).map((k) => ({ ...obj[k], __id__: k }));
-
-export const arrayToObject = (array, keyProp = 'id') =>
- array.reduce((acc, item) => {
- acc[item[keyProp]] = item;
- return acc;
- }, {});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.test.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.test.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.ts
new file mode 100644
index 0000000000000..1be7c85af3097
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/services/utils.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 const objectToArray = (
+ obj: Record
+): Array => Object.keys(obj).map((k) => ({ ...obj[k], __id__: k }));
+
+export const arrayToObject = (array: T[], keyProp: K): Record =>
+ array.reduce>((acc, item) => {
+ acc[String(item[keyProp])] = item;
+ return acc;
+ }, {});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/action_types.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/action_types.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/action_types.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/action_types.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/api.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/api.js
deleted file mode 100644
index 32379cb9a2cc8..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/api.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 * as t from '../action_types';
-import { API_STATUS } from '../../constants';
-
-export const apiRequestStart = ({ label, scope, status = API_STATUS.LOADING }) => ({
- type: t.API_REQUEST_START,
- payload: { label, scope, status },
-});
-
-export const apiRequestEnd = ({ label, scope }) => ({
- type: t.API_REQUEST_END,
- payload: { label, scope },
-});
-
-export const setApiError = ({ error, scope }) => ({
- type: t.API_ERROR_SET,
- payload: { error, scope },
-});
-
-export const clearApiError = (scope) => ({
- type: t.API_ERROR_SET,
- payload: { error: null, scope },
-});
-
-export const sendApiRequest =
- ({ label, scope, status, handler, onSuccess = () => undefined, onError = () => undefined }) =>
- async (dispatch, getState) => {
- dispatch(clearApiError(scope));
- dispatch(apiRequestStart({ label, scope, status }));
-
- try {
- const response = await handler(dispatch);
-
- dispatch(apiRequestEnd({ label, scope }));
- dispatch({ type: `${label}_SUCCESS`, payload: response });
-
- onSuccess(response, dispatch, getState);
- } catch (error) {
- dispatch(apiRequestEnd({ label, scope }));
- dispatch(setApiError({ error, scope }));
- dispatch({ type: `${label}_FAILURE`, payload: error });
-
- onError(error, dispatch, getState);
- }
- };
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/api.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/api.ts
new file mode 100644
index 0000000000000..552418ca487e4
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/api.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 type { AnyAction } from 'redux';
+import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
+import type { ApiStatus } from '../../../../common/types';
+import * as t from '../action_types';
+import { API_STATUS } from '../../constants';
+import type { CcrState } from '../reducers';
+import type { CcrApiError } from '../../services/http_error';
+import { toCcrApiError } from '../../services/http_error';
+
+export interface ApiRequestStartParams {
+ label: string;
+ scope: string;
+ status?: ApiStatus;
+}
+
+export const apiRequestStart = ({
+ label,
+ scope,
+ status = API_STATUS.LOADING,
+}: ApiRequestStartParams) => ({
+ type: t.API_REQUEST_START,
+ payload: { label, scope, status },
+});
+
+export interface ApiRequestEndParams {
+ label: string;
+ scope: string;
+}
+
+export const apiRequestEnd = ({ label, scope }: ApiRequestEndParams) => ({
+ type: t.API_REQUEST_END,
+ payload: { label, scope },
+});
+
+export interface SetApiErrorParams {
+ error: CcrApiError;
+ scope: string;
+}
+
+export const setApiError = ({ error, scope }: SetApiErrorParams) => ({
+ type: t.API_ERROR_SET,
+ payload: { error, scope },
+});
+
+export const clearApiError = (scope: string) => ({
+ type: t.API_ERROR_SET,
+ payload: { error: null, scope },
+});
+
+type AppDispatch = ThunkDispatch;
+
+export interface SendApiRequestParams {
+ label: string;
+ scope: string;
+ status?: ApiStatus;
+ handler: (dispatch: AppDispatch) => Promise;
+ onSuccess?: (response: TResponse, dispatch: AppDispatch, getState: () => CcrState) => void;
+ onError?: (error: CcrApiError, dispatch: AppDispatch, getState: () => CcrState) => void;
+}
+
+export const sendApiRequest =
+ ({
+ label,
+ scope,
+ status,
+ handler,
+ onSuccess = () => undefined,
+ onError = () => undefined,
+ }: SendApiRequestParams): ThunkAction, CcrState, undefined, AnyAction> =>
+ async (dispatch, getState) => {
+ dispatch(clearApiError(scope));
+ dispatch(apiRequestStart({ label, scope, status }));
+
+ try {
+ const response = await handler(dispatch);
+
+ dispatch(apiRequestEnd({ label, scope }));
+ dispatch({ type: `${label}_SUCCESS`, payload: response });
+
+ onSuccess(response, dispatch, getState);
+ } catch (error) {
+ const apiError = toCcrApiError(error);
+ dispatch(apiRequestEnd({ label, scope }));
+ dispatch(setApiError({ error: apiError, scope }));
+ dispatch({ type: `${label}_FAILURE`, payload: apiError });
+
+ onError(apiError, dispatch, getState);
+ }
+ };
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.ts
similarity index 72%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.ts
index 9ebe1d57a1826..80e04e99b5177 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.ts
@@ -5,10 +5,17 @@
* 2.0.
*/
+import type { AnyAction } from 'redux';
+import type { ThunkAction } from 'redux-thunk';
import { i18n } from '@kbn/i18n';
import { getToasts } from '../../services/notifications';
import { SECTIONS, API_STATUS } from '../../constants';
import {
+ type AutoFollowPatternCreateConfig,
+ type AutoFollowPatternConfig,
+ type DeleteAutoFollowPatternResponse,
+ type PauseAutoFollowPatternResponse,
+ type ResumeAutoFollowPatternResponse,
loadAutoFollowPatterns as loadAutoFollowPatternsRequest,
getAutoFollowPattern as getAutoFollowPatternRequest,
createAutoFollowPattern as createAutoFollowPatternRequest,
@@ -20,21 +27,24 @@ import {
import { routing } from '../../services/routing';
import * as t from '../action_types';
import { sendApiRequest } from './api';
+import type { CcrState } from '../reducers';
import { getSelectedAutoFollowPatternId } from '../selectors';
const { AUTO_FOLLOW_PATTERN: scope } = SECTIONS;
-export const selectDetailAutoFollowPattern = (id) => ({
+export const selectDetailAutoFollowPattern = (id: string | null) => ({
type: t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL,
payload: id,
});
-export const selectEditAutoFollowPattern = (id) => ({
+export const selectEditAutoFollowPattern = (id: string | null) => ({
type: t.AUTO_FOLLOW_PATTERN_SELECT_EDIT,
payload: id,
});
-export const loadAutoFollowPatterns = (isUpdating = false) =>
+export const loadAutoFollowPatterns = (
+ isUpdating = false
+): ThunkAction, CcrState, undefined, AnyAction> =>
sendApiRequest({
label: t.AUTO_FOLLOW_PATTERN_LOAD,
scope,
@@ -42,54 +52,77 @@ export const loadAutoFollowPatterns = (isUpdating = false) =>
handler: async () => await loadAutoFollowPatternsRequest(isUpdating),
});
-export const getAutoFollowPattern = (id) =>
+export const getAutoFollowPattern = (
+ id: string
+): ThunkAction, CcrState, undefined, AnyAction> =>
sendApiRequest({
label: t.AUTO_FOLLOW_PATTERN_GET,
scope: `${scope}-get`,
handler: async () => await getAutoFollowPatternRequest(id),
});
-export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) =>
- sendApiRequest({
- label: isUpdating ? t.AUTO_FOLLOW_PATTERN_UPDATE : t.AUTO_FOLLOW_PATTERN_CREATE,
+export const createAutoFollowPattern = (
+ id: string,
+ autoFollowPattern: AutoFollowPatternCreateConfig
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
+ label: t.AUTO_FOLLOW_PATTERN_CREATE,
status: API_STATUS.SAVING,
scope: `${scope}-save`,
- handler: async () => {
- if (isUpdating) {
- return await updateAutoFollowPatternRequest(id, autoFollowPattern);
- }
- return await createAutoFollowPatternRequest({ id, ...autoFollowPattern });
- },
+ handler: async () =>
+ await createAutoFollowPatternRequest({
+ id,
+ ...autoFollowPattern,
+ }),
onSuccess() {
- const successMessage = isUpdating
- ? i18n.translate(
- 'xpack.crossClusterReplication.autoFollowPattern.updateAction.successNotificationTitle',
- {
- defaultMessage: `Auto-follow pattern ''{name}'' updated successfully`,
- values: { name: id },
- }
- )
- : i18n.translate(
- 'xpack.crossClusterReplication.autoFollowPattern.addAction.successNotificationTitle',
- {
- defaultMessage: `Added auto-follow pattern ''{name}''`,
- values: { name: id },
- }
- );
+ getToasts().addSuccess(
+ i18n.translate(
+ 'xpack.crossClusterReplication.autoFollowPattern.addAction.successNotificationTitle',
+ {
+ defaultMessage: `Added auto-follow pattern ''{name}''`,
+ values: { name: id },
+ }
+ )
+ );
+ routing.navigate(`/auto_follow_patterns`, {
+ pattern: encodeURIComponent(id),
+ });
+ },
+ });
- getToasts().addSuccess(successMessage);
+export const updateAutoFollowPattern = (
+ id: string,
+ autoFollowPattern: AutoFollowPatternConfig
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
+ label: t.AUTO_FOLLOW_PATTERN_UPDATE,
+ status: API_STATUS.SAVING,
+ scope: `${scope}-save`,
+ handler: async () => await updateAutoFollowPatternRequest(id, autoFollowPattern),
+ onSuccess() {
+ getToasts().addSuccess(
+ i18n.translate(
+ 'xpack.crossClusterReplication.autoFollowPattern.updateAction.successNotificationTitle',
+ {
+ defaultMessage: `Auto-follow pattern ''{name}'' updated successfully`,
+ values: { name: id },
+ }
+ )
+ );
routing.navigate(`/auto_follow_patterns`, {
pattern: encodeURIComponent(id),
});
},
});
-export const deleteAutoFollowPattern = (id) =>
- sendApiRequest({
+export const deleteAutoFollowPattern = (
+ id: string | string[]
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.AUTO_FOLLOW_PATTERN_DELETE,
scope: `${scope}-delete`,
status: API_STATUS.DELETING,
- handler: async () => deleteAutoFollowPatternRequest(id),
+ handler: async () => await deleteAutoFollowPatternRequest(id),
onSuccess(response, dispatch, getState) {
/**
* We can have 1 or more auto-follow pattern delete operation
@@ -139,19 +172,21 @@ export const deleteAutoFollowPattern = (id) =>
// If we've just deleted a pattern we were looking at, we need to close the panel.
const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState());
- if (response.itemsDeleted.includes(autoFollowPatternId)) {
+ if (autoFollowPatternId != null && response.itemsDeleted.includes(autoFollowPatternId)) {
dispatch(selectDetailAutoFollowPattern(null));
}
}
},
});
-export const pauseAutoFollowPattern = (id) =>
- sendApiRequest({
+export const pauseAutoFollowPattern = (
+ id: string | string[]
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.AUTO_FOLLOW_PATTERN_PAUSE,
scope: `${scope}-pause`,
status: API_STATUS.UPDATING,
- handler: () => pauseAutoFollowPatternRequest(id),
+ handler: async () => await pauseAutoFollowPatternRequest(id),
onSuccess: (response) => {
/**
* We can have 1 or more auto-follow pattern pause operations
@@ -202,12 +237,14 @@ export const pauseAutoFollowPattern = (id) =>
},
});
-export const resumeAutoFollowPattern = (id) =>
- sendApiRequest({
+export const resumeAutoFollowPattern = (
+ id: string | string[]
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.AUTO_FOLLOW_PATTERN_RESUME,
scope: `${scope}-resume`,
status: API_STATUS.UPDATING,
- handler: () => resumeAutoFollowPatternRequest(id),
+ handler: async () => await resumeAutoFollowPatternRequest(id),
onSuccess: (response) => {
/**
* We can have 1 or more auto-follow pattern resume operations
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/ccr.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/ccr.ts
similarity index 73%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/ccr.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/ccr.ts
index 39cf8345315f0..fbd827bef4b7e 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/ccr.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/ccr.ts
@@ -5,14 +5,17 @@
* 2.0.
*/
+import type { AnyAction } from 'redux';
+import type { ThunkAction } from 'redux-thunk';
import { SECTIONS } from '../../constants';
import { loadAutoFollowStats as loadAutoFollowStatsRequest } from '../../services/api';
import * as t from '../action_types';
import { sendApiRequest } from './api';
+import type { CcrState } from '../reducers';
const { CCR_STATS: scope } = SECTIONS;
-export const loadAutoFollowStats = () =>
+export const loadAutoFollowStats = (): ThunkAction, CcrState, undefined, AnyAction> =>
sendApiRequest({
label: t.AUTO_FOLLOW_STATS_LOAD,
scope,
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/follower_index.ts
similarity index 85%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/follower_index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/follower_index.ts
index 0242874ec026b..74a0ce40941e0 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/follower_index.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/follower_index.ts
@@ -5,12 +5,17 @@
* 2.0.
*/
+import type { AnyAction } from 'redux';
+import type { ThunkAction } from 'redux-thunk';
import { i18n } from '@kbn/i18n';
-
import { routing } from '../../services/routing';
import { getToasts } from '../../services/notifications';
import { SECTIONS, API_STATUS } from '../../constants';
import {
+ type FollowerIndexSaveBody,
+ type PauseFollowerIndexResponse,
+ type ResumeFollowerIndexResponse,
+ type UnfollowLeaderIndexResponse,
loadFollowerIndices as loadFollowerIndicesRequest,
getFollowerIndex as getFollowerIndexRequest,
createFollowerIndex as createFollowerIndexRequest,
@@ -21,21 +26,24 @@ import {
} from '../../services/api';
import * as t from '../action_types';
import { sendApiRequest } from './api';
+import type { CcrState } from '../reducers';
import { getSelectedFollowerIndexId } from '../selectors';
const { FOLLOWER_INDEX: scope } = SECTIONS;
-export const selectDetailFollowerIndex = (id) => ({
+export const selectDetailFollowerIndex = (id: string | null) => ({
type: t.FOLLOWER_INDEX_SELECT_DETAIL,
payload: id,
});
-export const selectEditFollowerIndex = (id) => ({
+export const selectEditFollowerIndex = (id: string | null) => ({
type: t.FOLLOWER_INDEX_SELECT_EDIT,
payload: id,
});
-export const loadFollowerIndices = (isUpdating = false) =>
+export const loadFollowerIndices = (
+ isUpdating = false
+): ThunkAction, CcrState, undefined, AnyAction> =>
sendApiRequest({
label: t.FOLLOWER_INDEX_LOAD,
scope,
@@ -43,15 +51,21 @@ export const loadFollowerIndices = (isUpdating = false) =>
handler: async () => await loadFollowerIndicesRequest(isUpdating),
});
-export const getFollowerIndex = (id) =>
+export const getFollowerIndex = (
+ id: string
+): ThunkAction, CcrState, undefined, AnyAction> =>
sendApiRequest({
label: t.FOLLOWER_INDEX_GET,
scope: `${scope}-get`,
handler: async () => await getFollowerIndexRequest(id),
});
-export const saveFollowerIndex = (name, followerIndex, isUpdating = false) =>
- sendApiRequest({
+export const saveFollowerIndex = (
+ name: string,
+ followerIndex: FollowerIndexSaveBody,
+ isUpdating = false
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.FOLLOWER_INDEX_CREATE,
status: API_STATUS.SAVING,
scope: `${scope}-save`,
@@ -86,12 +100,14 @@ export const saveFollowerIndex = (name, followerIndex, isUpdating = false) =>
},
});
-export const pauseFollowerIndex = (id) =>
- sendApiRequest({
+export const pauseFollowerIndex = (
+ id: string | string[]
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.FOLLOWER_INDEX_PAUSE,
status: API_STATUS.SAVING,
scope,
- handler: async () => pauseFollowerIndexRequest(id),
+ handler: async () => await pauseFollowerIndexRequest(id),
onSuccess(response, dispatch) {
/**
* We can have 1 or more follower index pause operation
@@ -145,12 +161,14 @@ export const pauseFollowerIndex = (id) =>
},
});
-export const resumeFollowerIndex = (id) =>
- sendApiRequest({
+export const resumeFollowerIndex = (
+ id: string | string[]
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.FOLLOWER_INDEX_RESUME,
status: API_STATUS.SAVING,
scope,
- handler: async () => resumeFollowerIndexRequest(id),
+ handler: async () => await resumeFollowerIndexRequest(id),
onSuccess(response, dispatch) {
/**
* We can have 1 or more follower index resume operation
@@ -204,12 +222,14 @@ export const resumeFollowerIndex = (id) =>
},
});
-export const unfollowLeaderIndex = (id) =>
- sendApiRequest({
+export const unfollowLeaderIndex = (
+ id: string | string[]
+): ThunkAction, CcrState, undefined, AnyAction> =>
+ sendApiRequest({
label: t.FOLLOWER_INDEX_UNFOLLOW,
status: API_STATUS.DELETING,
scope: `${scope}-delete`,
- handler: async () => unfollowLeaderIndexRequest(id),
+ handler: async () => await unfollowLeaderIndexRequest(id),
onSuccess(response, dispatch, getState) {
/**
* We can have 1 or more follower index unfollow operation
@@ -282,7 +302,7 @@ export const unfollowLeaderIndex = (id) =>
// If we've just unfollowed a follower index we were looking at, we need to close the panel.
const followerIndexId = getSelectedFollowerIndexId('detail')(getState());
- if (response.itemsUnfollowed.includes(followerIndexId)) {
+ if (followerIndexId != null && response.itemsUnfollowed.includes(followerIndexId)) {
dispatch(selectDetailFollowerIndex(null));
}
},
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/index.ts
similarity index 100%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/actions/index.ts
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.d.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.d.ts
deleted file mode 100644
index 2e4db093be0f2..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * 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 declare const ccrStore: any;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.ts
similarity index 88%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.ts
index 2e8f132f4034f..889811e761916 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/index.ts
@@ -5,4 +5,5 @@
* 2.0.
*/
+export type { CcrState } from './reducers';
export { ccrStore, createCrossClusterReplicationStore } from './store';
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.js
deleted file mode 100644
index 71a32be25c1fa..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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 { SECTIONS, API_STATUS } from '../../constants';
-import * as t from '../action_types';
-
-export const initialState = {
- status: {
- [SECTIONS.AUTO_FOLLOW_PATTERN]: API_STATUS.IDLE,
- [SECTIONS.FOLLOWER_INDEX]: API_STATUS.IDLE,
- },
- error: {
- [SECTIONS.AUTO_FOLLOW_PATTERN]: null,
- [SECTIONS.FOLLOWER_INDEX]: null,
- },
-};
-
-export const reducer = (state = initialState, action) => {
- const payload = action.payload || {};
- const { scope, status, error } = payload;
-
- switch (action.type) {
- case t.API_REQUEST_START: {
- return { ...state, status: { ...state.status, [scope]: status } };
- }
- case t.API_REQUEST_END: {
- return { ...state, status: { ...state.status, [scope]: API_STATUS.IDLE } };
- }
- case t.API_ERROR_SET: {
- return { ...state, error: { ...state.error, [scope]: error } };
- }
- default:
- return state;
- }
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.test.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.test.ts
similarity index 84%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.test.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.test.ts
index 0dce96e21ccb0..b512abcada539 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.test.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.test.ts
@@ -24,20 +24,20 @@ describe('CCR Api reducers', () => {
const scope = 'testSection';
it('API_REQUEST_START should set the Api status to "loading" on scope', () => {
- const result = reducer(initialState, apiRequestStart({ scope }));
+ const result = reducer(initialState, apiRequestStart({ label: 'test', scope }));
expect(result.status[scope]).toEqual(API_STATUS.LOADING);
});
it('API_END should set the Api status to "idle" on scope', () => {
- const updatedState = reducer(initialState, apiRequestStart({ scope }));
- const result = reducer(updatedState, apiRequestEnd({ scope }));
+ const updatedState = reducer(initialState, apiRequestStart({ label: 'test', scope }));
+ const result = reducer(updatedState, apiRequestEnd({ label: 'test', scope }));
expect(result.status[scope]).toEqual(API_STATUS.IDLE);
});
it('API_ERROR_SET should set the Api error on scope', () => {
- const error = { foo: 'bar' };
+ const error = new Error('boom');
const result = reducer(initialState, setApiError({ error, scope }));
expect(result.error[scope]).toBe(error);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.ts
new file mode 100644
index 0000000000000..38c7f2cfb441d
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/api.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 type { ApiStatus } from '../../../../common/types';
+import { SECTIONS, API_STATUS } from '../../constants';
+import * as t from '../action_types';
+import type { CcrApiError } from '../../services/http_error';
+
+export interface ApiState {
+ status: Record;
+ error: Record;
+}
+
+export interface ApiReducerAction {
+ type: string;
+ payload?: {
+ scope?: string;
+ status?: ApiStatus;
+ error?: CcrApiError | null;
+ label?: string;
+ };
+}
+
+export const initialState = {
+ status: {
+ [SECTIONS.AUTO_FOLLOW_PATTERN]: API_STATUS.IDLE,
+ [SECTIONS.FOLLOWER_INDEX]: API_STATUS.IDLE,
+ },
+ error: {
+ [SECTIONS.AUTO_FOLLOW_PATTERN]: null,
+ [SECTIONS.FOLLOWER_INDEX]: null,
+ },
+} satisfies ApiState;
+
+export const reducer = (state: ApiState = initialState, action: ApiReducerAction): ApiState => {
+ const payload = action.payload;
+ const scope = payload?.scope;
+
+ switch (action.type) {
+ case t.API_REQUEST_START: {
+ if (!scope) {
+ return state;
+ }
+ const status = payload?.status ?? API_STATUS.LOADING;
+ return {
+ ...state,
+ status: { ...state.status, [scope]: status },
+ };
+ }
+ case t.API_REQUEST_END: {
+ if (!scope) {
+ return state;
+ }
+ return { ...state, status: { ...state.status, [scope]: API_STATUS.IDLE } };
+ }
+ case t.API_ERROR_SET: {
+ if (!scope) {
+ return state;
+ }
+ const error = payload?.error ?? null;
+ return { ...state, error: { ...state.error, [scope]: error } };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js
deleted file mode 100644
index 60e811d424cc8..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 * as t from '../action_types';
-import { arrayToObject } from '../../services/utils';
-import { getPrefixSuffixFromFollowPattern } from '../../services/auto_follow_pattern';
-
-const initialState = {
- byId: {},
- selectedDetailId: null,
- selectedEditId: null,
-};
-
-const success = (action) => `${action}_SUCCESS`;
-
-const setActiveForIds = (ids, byId, active) => {
- const shallowCopyByIds = { ...byId };
- ids.forEach((id) => {
- shallowCopyByIds[id].active = active;
- });
- return shallowCopyByIds;
-};
-
-const parseAutoFollowPattern = (autoFollowPattern) => {
- // Extract prefix and suffix from follow index pattern
- const { followIndexPatternPrefix, followIndexPatternSuffix } = getPrefixSuffixFromFollowPattern(
- autoFollowPattern.followIndexPattern
- );
-
- return { ...autoFollowPattern, followIndexPatternPrefix, followIndexPatternSuffix };
-};
-
-export const reducer = (state = initialState, action) => {
- switch (action.type) {
- case success(t.AUTO_FOLLOW_PATTERN_LOAD): {
- return {
- ...state,
- byId: arrayToObject(action.payload.patterns.map(parseAutoFollowPattern), 'name'),
- };
- }
- case success(t.AUTO_FOLLOW_PATTERN_GET): {
- return {
- ...state,
- byId: { ...state.byId, [action.payload.name]: parseAutoFollowPattern(action.payload) },
- };
- }
- case t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL: {
- return { ...state, selectedDetailId: action.payload };
- }
- case t.AUTO_FOLLOW_PATTERN_SELECT_EDIT: {
- return { ...state, selectedEditId: action.payload };
- }
- case success(t.AUTO_FOLLOW_PATTERN_DELETE): {
- const byId = { ...state.byId };
- const { itemsDeleted } = action.payload;
- itemsDeleted.forEach((id) => delete byId[id]);
- return { ...state, byId };
- }
- case success(t.AUTO_FOLLOW_PATTERN_PAUSE): {
- const { itemsPaused } = action.payload;
- return { ...state, byId: setActiveForIds(itemsPaused, state.byId, false) };
- }
- case success(t.AUTO_FOLLOW_PATTERN_RESUME): {
- const { itemsResumed } = action.payload;
- return { ...state, byId: setActiveForIds(itemsResumed, state.byId, true) };
- }
- default:
- return state;
- }
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.ts
new file mode 100644
index 0000000000000..236c3658333b8
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.ts
@@ -0,0 +1,152 @@
+/*
+ * 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 type { AutoFollowPattern } from '../../../../common/types';
+import * as t from '../action_types';
+import { arrayToObject } from '../../services/utils';
+import { getPrefixSuffixFromFollowPattern } from '../../services/auto_follow_pattern';
+import type {
+ DeleteAutoFollowPatternResponse,
+ PauseAutoFollowPatternResponse,
+ ResumeAutoFollowPatternResponse,
+} from '../../services/api';
+
+export interface ParsedAutoFollowPattern extends AutoFollowPattern {
+ followIndexPatternPrefix?: string;
+ followIndexPatternSuffix?: string;
+}
+
+export interface AutoFollowPatternState {
+ byId: Record;
+ selectedDetailId: string | null;
+ selectedEditId: string | null;
+}
+
+const initialState: AutoFollowPatternState = {
+ byId: {},
+ selectedDetailId: null,
+ selectedEditId: null,
+};
+
+const AUTO_FOLLOW_PATTERN_LOAD_SUCCESS: `${typeof t.AUTO_FOLLOW_PATTERN_LOAD}_SUCCESS` = `${t.AUTO_FOLLOW_PATTERN_LOAD}_SUCCESS`;
+const AUTO_FOLLOW_PATTERN_GET_SUCCESS: `${typeof t.AUTO_FOLLOW_PATTERN_GET}_SUCCESS` = `${t.AUTO_FOLLOW_PATTERN_GET}_SUCCESS`;
+const AUTO_FOLLOW_PATTERN_DELETE_SUCCESS: `${typeof t.AUTO_FOLLOW_PATTERN_DELETE}_SUCCESS` = `${t.AUTO_FOLLOW_PATTERN_DELETE}_SUCCESS`;
+const AUTO_FOLLOW_PATTERN_PAUSE_SUCCESS: `${typeof t.AUTO_FOLLOW_PATTERN_PAUSE}_SUCCESS` = `${t.AUTO_FOLLOW_PATTERN_PAUSE}_SUCCESS`;
+const AUTO_FOLLOW_PATTERN_RESUME_SUCCESS: `${typeof t.AUTO_FOLLOW_PATTERN_RESUME}_SUCCESS` = `${t.AUTO_FOLLOW_PATTERN_RESUME}_SUCCESS`;
+
+interface LoadAutoFollowPatternsSuccessAction {
+ type: typeof AUTO_FOLLOW_PATTERN_LOAD_SUCCESS;
+ payload: { patterns: AutoFollowPattern[] };
+}
+
+interface GetAutoFollowPatternSuccessAction {
+ type: typeof AUTO_FOLLOW_PATTERN_GET_SUCCESS;
+ payload: AutoFollowPattern;
+}
+
+interface SelectAutoFollowPatternDetailAction {
+ type: typeof t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL;
+ payload: string | null;
+}
+
+interface SelectAutoFollowPatternEditAction {
+ type: typeof t.AUTO_FOLLOW_PATTERN_SELECT_EDIT;
+ payload: string | null;
+}
+
+interface DeleteAutoFollowPatternSuccessAction {
+ type: typeof AUTO_FOLLOW_PATTERN_DELETE_SUCCESS;
+ payload: DeleteAutoFollowPatternResponse;
+}
+
+interface PauseAutoFollowPatternSuccessAction {
+ type: typeof AUTO_FOLLOW_PATTERN_PAUSE_SUCCESS;
+ payload: PauseAutoFollowPatternResponse;
+}
+
+interface ResumeAutoFollowPatternSuccessAction {
+ type: typeof AUTO_FOLLOW_PATTERN_RESUME_SUCCESS;
+ payload: ResumeAutoFollowPatternResponse;
+}
+
+export type AutoFollowPatternReducerAction =
+ | LoadAutoFollowPatternsSuccessAction
+ | GetAutoFollowPatternSuccessAction
+ | SelectAutoFollowPatternDetailAction
+ | SelectAutoFollowPatternEditAction
+ | DeleteAutoFollowPatternSuccessAction
+ | PauseAutoFollowPatternSuccessAction
+ | ResumeAutoFollowPatternSuccessAction;
+
+const setActiveForIds = (
+ ids: string[],
+ byId: Record,
+ active: boolean
+): Record => {
+ const shallowCopyByIds = { ...byId };
+ ids.forEach((id) => {
+ const pattern = byId[id];
+ if (!pattern) {
+ return;
+ }
+ shallowCopyByIds[id] = { ...pattern, active };
+ });
+ return shallowCopyByIds;
+};
+
+const parseAutoFollowPattern = (autoFollowPattern: AutoFollowPattern): ParsedAutoFollowPattern => {
+ // Extract prefix and suffix from follow index pattern
+ const { followIndexPatternPrefix, followIndexPatternSuffix } = getPrefixSuffixFromFollowPattern(
+ autoFollowPattern.followIndexPattern
+ );
+
+ return { ...autoFollowPattern, followIndexPatternPrefix, followIndexPatternSuffix };
+};
+
+export const reducer = (
+ state: AutoFollowPatternState = initialState,
+ action: AutoFollowPatternReducerAction
+): AutoFollowPatternState => {
+ switch (action.type) {
+ case AUTO_FOLLOW_PATTERN_LOAD_SUCCESS: {
+ const payload = action.payload;
+ return {
+ ...state,
+ byId: arrayToObject(payload.patterns.map(parseAutoFollowPattern), 'name'),
+ };
+ }
+ case AUTO_FOLLOW_PATTERN_GET_SUCCESS: {
+ const payload = action.payload;
+ return {
+ ...state,
+ byId: { ...state.byId, [payload.name]: parseAutoFollowPattern(payload) },
+ };
+ }
+ case t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL: {
+ return { ...state, selectedDetailId: action.payload };
+ }
+ case t.AUTO_FOLLOW_PATTERN_SELECT_EDIT: {
+ return { ...state, selectedEditId: action.payload };
+ }
+ case AUTO_FOLLOW_PATTERN_DELETE_SUCCESS: {
+ const payload = action.payload;
+ const byId = { ...state.byId };
+ payload.itemsDeleted.forEach((id) => delete byId[id]);
+ return { ...state, byId };
+ }
+ case AUTO_FOLLOW_PATTERN_PAUSE_SUCCESS: {
+ const payload = action.payload;
+ return { ...state, byId: setActiveForIds(payload.itemsPaused, state.byId, false) };
+ }
+ case AUTO_FOLLOW_PATTERN_RESUME_SUCCESS: {
+ const payload = action.payload;
+ return { ...state, byId: setActiveForIds(payload.itemsResumed, state.byId, true) };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/follower_index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/follower_index.js
deleted file mode 100644
index b60ebffb34b35..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/follower_index.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 * as t from '../action_types';
-import { arrayToObject } from '../../services/utils';
-
-const initialState = {
- byId: {},
- selectedDetailId: null,
- selectedEditId: null,
-};
-
-const success = (action) => `${action}_SUCCESS`;
-
-const parseFollowerIndex = (followerIndex) => {
- // Extract status into boolean
- return { ...followerIndex, isPaused: followerIndex.status === 'paused' };
-};
-export const reducer = (state = initialState, action) => {
- switch (action.type) {
- case success(t.FOLLOWER_INDEX_LOAD): {
- return {
- ...state,
- byId: arrayToObject(action.payload.indices.map(parseFollowerIndex), 'name'),
- };
- }
- case success(t.FOLLOWER_INDEX_GET): {
- return {
- ...state,
- byId: { ...state.byId, [action.payload.name]: parseFollowerIndex(action.payload) },
- };
- }
- case t.FOLLOWER_INDEX_SELECT_DETAIL: {
- return { ...state, selectedDetailId: action.payload };
- }
- case t.FOLLOWER_INDEX_SELECT_EDIT: {
- return { ...state, selectedEditId: action.payload };
- }
- case success(t.FOLLOWER_INDEX_UNFOLLOW): {
- const byId = { ...state.byId };
- const { itemsUnfollowed } = action.payload;
- itemsUnfollowed.forEach((id) => delete byId[id]);
- return { ...state, byId };
- }
- default:
- return state;
- }
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/follower_index.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/follower_index.ts
new file mode 100644
index 0000000000000..a161f96c6b8bd
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/follower_index.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 type { FollowerIndex, FollowerIndexWithPausedStatus } from '../../../../common/types';
+import * as t from '../action_types';
+import { arrayToObject } from '../../services/utils';
+import type { UnfollowLeaderIndexResponse } from '../../services/api';
+
+export interface FollowerIndexState {
+ byId: Record;
+ selectedDetailId: string | null;
+ selectedEditId: string | null;
+}
+
+const initialState: FollowerIndexState = {
+ byId: {},
+ selectedDetailId: null,
+ selectedEditId: null,
+};
+
+const FOLLOWER_INDEX_LOAD_SUCCESS: `${typeof t.FOLLOWER_INDEX_LOAD}_SUCCESS` = `${t.FOLLOWER_INDEX_LOAD}_SUCCESS`;
+const FOLLOWER_INDEX_GET_SUCCESS: `${typeof t.FOLLOWER_INDEX_GET}_SUCCESS` = `${t.FOLLOWER_INDEX_GET}_SUCCESS`;
+const FOLLOWER_INDEX_UNFOLLOW_SUCCESS: `${typeof t.FOLLOWER_INDEX_UNFOLLOW}_SUCCESS` = `${t.FOLLOWER_INDEX_UNFOLLOW}_SUCCESS`;
+
+interface LoadFollowerIndicesSuccessAction {
+ type: typeof FOLLOWER_INDEX_LOAD_SUCCESS;
+ payload: { indices: FollowerIndex[] };
+}
+
+interface GetFollowerIndexSuccessAction {
+ type: typeof FOLLOWER_INDEX_GET_SUCCESS;
+ payload: FollowerIndex;
+}
+
+interface SelectFollowerIndexDetailAction {
+ type: typeof t.FOLLOWER_INDEX_SELECT_DETAIL;
+ payload: string | null;
+}
+
+interface SelectFollowerIndexEditAction {
+ type: typeof t.FOLLOWER_INDEX_SELECT_EDIT;
+ payload: string | null;
+}
+
+interface UnfollowLeaderIndexSuccessAction {
+ type: typeof FOLLOWER_INDEX_UNFOLLOW_SUCCESS;
+ payload: UnfollowLeaderIndexResponse;
+}
+
+export type FollowerIndexReducerAction =
+ | LoadFollowerIndicesSuccessAction
+ | GetFollowerIndexSuccessAction
+ | SelectFollowerIndexDetailAction
+ | SelectFollowerIndexEditAction
+ | UnfollowLeaderIndexSuccessAction;
+
+const parseFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexWithPausedStatus => {
+ // Extract status into boolean
+ return { ...followerIndex, isPaused: followerIndex.status === 'paused' };
+};
+
+export const reducer = (
+ state: FollowerIndexState = initialState,
+ action: FollowerIndexReducerAction
+): FollowerIndexState => {
+ switch (action.type) {
+ case FOLLOWER_INDEX_LOAD_SUCCESS: {
+ const payload = action.payload;
+ return {
+ ...state,
+ byId: arrayToObject(payload.indices.map(parseFollowerIndex), 'name'),
+ };
+ }
+ case FOLLOWER_INDEX_GET_SUCCESS: {
+ const payload = action.payload;
+ return {
+ ...state,
+ byId: { ...state.byId, [payload.name]: parseFollowerIndex(payload) },
+ };
+ }
+ case t.FOLLOWER_INDEX_SELECT_DETAIL: {
+ return { ...state, selectedDetailId: action.payload };
+ }
+ case t.FOLLOWER_INDEX_SELECT_EDIT: {
+ return { ...state, selectedEditId: action.payload };
+ }
+ case FOLLOWER_INDEX_UNFOLLOW_SUCCESS: {
+ const payload = action.payload;
+ const byId = { ...state.byId };
+ payload.itemsUnfollowed.forEach((id) => delete byId[id]);
+ return { ...state, byId };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/index.ts
similarity index 92%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/index.ts
index 206c19564d0ba..d688e61ed10c2 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/index.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/index.ts
@@ -17,3 +17,5 @@ export const ccr = combineReducers({
api,
stats,
});
+
+export type CcrState = ReturnType;
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/stats.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/stats.js
deleted file mode 100644
index 41d6fb2823ff3..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/stats.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 * as t from '../action_types';
-import { parseAutoFollowErrors } from '../../services/auto_follow_errors';
-
-const initialState = {
- autoFollow: null,
-};
-
-const success = (action) => `${action}_SUCCESS`;
-
-export const reducer = (state = initialState, action) => {
- switch (action.type) {
- case success(t.AUTO_FOLLOW_STATS_LOAD): {
- const { recentAutoFollowErrors, ...rest } = action.payload;
- return {
- ...state,
- autoFollow: {
- ...rest,
- recentAutoFollowErrors: parseAutoFollowErrors(recentAutoFollowErrors),
- },
- };
- }
- default:
- return state;
- }
-};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/stats.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/stats.ts
new file mode 100644
index 0000000000000..c72ca86534864
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/reducers/stats.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 type { AutoFollowStats } from '../../../../common/types';
+import * as t from '../action_types';
+import type { ParsedAutoFollowError } from '../../services/auto_follow_errors';
+import { parseAutoFollowErrors } from '../../services/auto_follow_errors';
+
+export type AutoFollowStatsWithParsedErrors = Omit & {
+ recentAutoFollowErrors: Record;
+};
+
+export interface StatsState {
+ autoFollow: AutoFollowStatsWithParsedErrors | null;
+}
+
+const initialState: StatsState = {
+ autoFollow: null,
+};
+
+const AUTO_FOLLOW_STATS_LOAD_SUCCESS: `${typeof t.AUTO_FOLLOW_STATS_LOAD}_SUCCESS` = `${t.AUTO_FOLLOW_STATS_LOAD}_SUCCESS`;
+
+export interface LoadAutoFollowStatsSuccessAction {
+ type: typeof AUTO_FOLLOW_STATS_LOAD_SUCCESS;
+ payload: AutoFollowStats;
+}
+
+export type StatsReducerAction = LoadAutoFollowStatsSuccessAction;
+
+export const reducer = (
+ state: StatsState = initialState,
+ action: StatsReducerAction
+): StatsState => {
+ switch (action.type) {
+ case AUTO_FOLLOW_STATS_LOAD_SUCCESS: {
+ const { recentAutoFollowErrors, ...rest } = action.payload;
+ return {
+ ...state,
+ autoFollow: {
+ ...rest,
+ recentAutoFollowErrors: parseAutoFollowErrors(recentAutoFollowErrors),
+ },
+ };
+ }
+ default:
+ return state;
+ }
+};
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.test.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.test.ts
new file mode 100644
index 0000000000000..5b66ea31d20f4
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.test.ts
@@ -0,0 +1,110 @@
+/*
+ * 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 type { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
+import { isApiAuthorized, getApiError } from '.';
+import { API_STATUS } from '../../constants';
+import type { CcrState } from '../reducers';
+import type { CcrApiError } from '../../services/http_error';
+
+const makeHttpFetchError = ({
+ statusCode,
+ responseStatus,
+ message = 'boom',
+}: {
+ statusCode?: number;
+ responseStatus?: number;
+ message?: string;
+}): IHttpFetchError => {
+ const body = {
+ message,
+ ...(statusCode !== undefined ? { statusCode } : {}),
+ } as unknown as ResponseErrorBody;
+ const error = Object.assign(new Error(message), {
+ body,
+ request: { path: '/fake' } as unknown as IHttpFetchError['request'],
+ ...(responseStatus !== undefined
+ ? {
+ response: {
+ status: responseStatus,
+ } as unknown as IHttpFetchError['response'],
+ }
+ : {}),
+ });
+ return error as unknown as IHttpFetchError;
+};
+
+const makeState = (scope: string, error: CcrApiError | null): CcrState =>
+ ({
+ api: {
+ status: {},
+ error: error ? { [scope]: error } : {},
+ },
+ } as unknown as CcrState);
+
+describe('store/selectors/isApiAuthorized', () => {
+ const scope = 'autoFollowPattern';
+
+ it('returns true when there is no error for the scope', () => {
+ const state = makeState(scope, null);
+ expect(isApiAuthorized(scope)(state)).toBe(true);
+ });
+
+ it('returns false for a 403 HttpFetchError exposed via response.status', () => {
+ const state = makeState(scope, makeHttpFetchError({ responseStatus: 403 }));
+ expect(isApiAuthorized(scope)(state)).toBe(false);
+ });
+
+ it('returns false for a 403 HttpFetchError exposed via body.statusCode', () => {
+ const state = makeState(scope, makeHttpFetchError({ statusCode: 403 }));
+ expect(isApiAuthorized(scope)(state)).toBe(false);
+ });
+
+ it('returns true for non-403 HttpFetchErrors (e.g. 404, 500)', () => {
+ const state404 = makeState(scope, makeHttpFetchError({ statusCode: 404 }));
+ const state500 = makeState(scope, makeHttpFetchError({ responseStatus: 500 }));
+ expect(isApiAuthorized(scope)(state404)).toBe(true);
+ expect(isApiAuthorized(scope)(state500)).toBe(true);
+ });
+
+ it('returns true for a plain Error without a status (treats as non-auth error)', () => {
+ const state = makeState(scope, new Error('unexpected'));
+ expect(isApiAuthorized(scope)(state)).toBe(true);
+ });
+
+ // `HttpFetchError` exposes status via `error.response.status` or
+ // `error.body.statusCode`, not via a top-level `error.status`. The selector
+ // must read from those fields — reading a top-level `error.status` would
+ // always be `undefined`, leaving the unauthorized UI branch unreachable.
+ it('does not read a non-existent top-level error.status', () => {
+ const error = makeHttpFetchError({ responseStatus: 403 });
+ // Even if future code accidentally attaches a misleading top-level `status`,
+ // the selector must keep using the real HttpFetchError fields.
+ Object.assign(error, { status: 200 });
+ const state = makeState(scope, error as unknown as CcrApiError);
+ expect(isApiAuthorized(scope)(state)).toBe(false);
+ });
+});
+
+describe('store/selectors/getApiError', () => {
+ it('returns null when there is no error for the scope', () => {
+ const state = makeState('other', null);
+ expect(getApiError('absent')(state)).toBeNull();
+ });
+
+ it('returns the stored error for the scope', () => {
+ const error = new Error('boom');
+ const state = makeState('autoFollowPattern', error);
+ expect(getApiError('autoFollowPattern')(state)).toBe(error);
+ });
+});
+
+describe('store/selectors smoke', () => {
+ it('API_STATUS constants are importable (sanity check for reducer wiring)', () => {
+ expect(API_STATUS.IDLE).toBeDefined();
+ });
+});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.ts
similarity index 51%
rename from x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.js
rename to x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.ts
index b907999f7be56..4d82f74507132 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.js
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/selectors/index.ts
@@ -6,53 +6,62 @@
*/
import { createSelector } from 'reselect';
+import type { FollowerIndexWithPausedStatus } from '../../../../common/types';
import { objectToArray } from '../../services/utils';
import { API_STATUS } from '../../constants';
+import type { ParsedAutoFollowError } from '../../services/auto_follow_errors';
+import type { CcrApiError } from '../../services/http_error';
+import { getErrorStatus } from '../../services/http_error';
+import type { CcrState } from '../reducers';
+import type { ParsedAutoFollowPattern } from '../reducers/auto_follow_pattern';
+import type { AutoFollowStatsWithParsedErrors } from '../reducers/stats';
+
+export type AutoFollowPatternWithErrors = ParsedAutoFollowPattern & {
+ errors: ParsedAutoFollowError[];
+};
// Api
-export const getApiState = (state) => state.api;
-export const getApiStatus = (scope) =>
- createSelector(getApiState, (apiState) => apiState.status[scope] || API_STATUS.IDLE);
-export const getApiError = (scope) =>
- createSelector(getApiState, (apiState) => apiState.error[scope]);
-export const isApiAuthorized = (scope) =>
- createSelector(getApiError(scope), (error) => {
- if (!error) {
- return true;
- }
- return error.status !== 403;
+export const getApiState = (state: CcrState) => state.api;
+export const getApiStatus = (scope: string) =>
+ createSelector(getApiState, (apiState) => apiState.status[scope] ?? API_STATUS.IDLE);
+export const getApiError = (scope: string) =>
+ createSelector(getApiState, (apiState) => apiState.error[scope] ?? null);
+export const isApiAuthorized = (scope: string) =>
+ createSelector(getApiError(scope), (error: CcrApiError | null | undefined) => {
+ const status = getErrorStatus(error);
+ return status !== 403;
});
// Stats
-export const getStatsState = (state) => state.stats;
+export const getStatsState = (state: CcrState) => state.stats;
export const getAutoFollowStats = createSelector(
getStatsState,
- (statsState) => statsState.autoFollow
+ (statsState): AutoFollowStatsWithParsedErrors | null => statsState.autoFollow
);
// Auto-follow pattern
-export const getAutoFollowPatternState = (state) => state.autoFollowPattern;
+export const getAutoFollowPatternState = (state: CcrState) => state.autoFollowPattern;
export const getAutoFollowPatterns = createSelector(
getAutoFollowPatternState,
(autoFollowPatternsState) => autoFollowPatternsState.byId
);
-export const getSelectedAutoFollowPatternId = (view = 'detail') =>
+export const getSelectedAutoFollowPatternId = (view: 'detail' | 'edit' = 'detail') =>
createSelector(getAutoFollowPatternState, (autoFollowPatternsState) =>
view === 'detail'
? autoFollowPatternsState.selectedDetailId
: autoFollowPatternsState.selectedEditId
);
-export const getSelectedAutoFollowPattern = (view = 'detail') =>
+export const getSelectedAutoFollowPattern = (view: 'detail' | 'edit' = 'detail') =>
createSelector(
getAutoFollowPatternState,
getAutoFollowStats,
- (autoFollowPatternsState, autoFollowStatsState) => {
+ (autoFollowPatternsState, autoFollowStatsState): AutoFollowPatternWithErrors | null => {
const propId = view === 'detail' ? 'selectedDetailId' : 'selectedEditId';
- if (!autoFollowPatternsState[propId]) {
+ const id = autoFollowPatternsState[propId];
+ if (!id) {
return null;
}
- const id = autoFollowPatternsState[propId];
const autoFollowPattern = autoFollowPatternsState.byId[id];
// Check if any error and merge them on the auto-follow pattern
@@ -67,24 +76,28 @@ export const getListAutoFollowPatterns = createSelector(
);
// Follower index
-export const getFollowerIndexState = (state) => state.followerIndex;
+export const getFollowerIndexState = (state: CcrState) => state.followerIndex;
export const getFollowerIndices = createSelector(
getFollowerIndexState,
(followerIndexState) => followerIndexState.byId
);
-export const getSelectedFollowerIndexId = (view = 'detail') =>
+export const getSelectedFollowerIndexId = (view: 'detail' | 'edit' = 'detail') =>
createSelector(getFollowerIndexState, (followerIndexState) =>
view === 'detail' ? followerIndexState.selectedDetailId : followerIndexState.selectedEditId
);
-export const getSelectedFollowerIndex = (view = 'detail') =>
- createSelector(getFollowerIndexState, (followerIndexState) => {
- const propId = view === 'detail' ? 'selectedDetailId' : 'selectedEditId';
+export const getSelectedFollowerIndex = (view: 'detail' | 'edit' = 'detail') =>
+ createSelector(
+ getFollowerIndexState,
+ (followerIndexState): FollowerIndexWithPausedStatus | null => {
+ const propId = view === 'detail' ? 'selectedDetailId' : 'selectedEditId';
- if (!followerIndexState[propId]) {
- return null;
+ const id = followerIndexState[propId];
+ if (!id) {
+ return null;
+ }
+ return followerIndexState.byId[id];
}
- return followerIndexState.byId[followerIndexState[propId]];
- });
+ );
export const getListFollowerIndices = createSelector(getFollowerIndices, (followerIndices) =>
objectToArray(followerIndices)
);
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/store.js b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/store.js
deleted file mode 100644
index 2467aec43318a..0000000000000
--- a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/store.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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 { applyMiddleware, compose, createStore } from 'redux';
-import thunk from 'redux-thunk';
-
-import { ccr } from './reducers';
-
-export function createCrossClusterReplicationStore(initialState = {}) {
- const enhancers = [applyMiddleware(thunk)];
-
- if (window.__REDUX_DEVTOOLS_EXTENSION__) {
- enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__());
- }
- return createStore(ccr, initialState, compose(...enhancers));
-}
-
-// Singleton for production use
-export const ccrStore = createCrossClusterReplicationStore();
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/store.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/store.ts
new file mode 100644
index 0000000000000..f4c92daa552bc
--- /dev/null
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/app/store/store.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 type { Store, StoreEnhancer } from 'redux';
+import { applyMiddleware, compose, createStore } from 'redux';
+import thunk from 'redux-thunk';
+
+import { ccr, type CcrState } from './reducers';
+
+declare global {
+ interface Window {
+ __REDUX_DEVTOOLS_EXTENSION__?: () => StoreEnhancer;
+ }
+}
+
+export function createCrossClusterReplicationStore(
+ initialState: Partial = {}
+): Store {
+ const middleware = applyMiddleware(thunk);
+ const devtools =
+ typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__
+ ? window.__REDUX_DEVTOOLS_EXTENSION__()
+ : undefined;
+ const enhancer = devtools ? compose(middleware, devtools) : middleware;
+
+ return createStore(ccr, initialState, enhancer);
+}
+
+// Singleton for production use
+export const ccrStore = createCrossClusterReplicationStore();
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts
index caf8408d5b0e0..c88642a9b631a 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts
@@ -8,7 +8,6 @@
import { schema } from '@kbn/config-schema';
import type { CcrFollowRequest } from '@elastic/elasticsearch/lib/api/types';
import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization';
-import type { FollowerIndex } from '../../../../common/types';
import { addBasePath } from '../../../services';
import { removeEmptyFields } from '../../../../common/services/utils';
import type { RouteDependencies } from '../../../types';
@@ -52,8 +51,8 @@ export const registerCreateRoute = ({
},
license.guardApiRoute(async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
- const { name, ...rest } = request.body;
- const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex));
+ const { name, ...followerIndex } = request.body;
+ const body = removeEmptyFields(serializeFollowerIndex(followerIndex));
try {
const responseBody = await client.asCurrentUser.ccr.follow({
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.test.ts b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.test.ts
index ffb7890cb4493..26981f5dd3f96 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.test.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.test.ts
@@ -61,18 +61,16 @@ describe('[CCR API] Update follower index', () => {
expect(response.payload).toEqual({
index: 'foo',
- body: {
- max_outstanding_read_requests: 1,
- max_outstanding_write_requests: 1,
- max_read_request_operation_count: 1,
- max_read_request_size: '1b',
- max_retry_delay: '1s',
- max_write_buffer_count: 1,
- max_write_buffer_size: '1b',
- max_write_request_operation_count: 1,
- max_write_request_size: '1b',
- read_poll_timeout: '1s',
- },
+ max_outstanding_read_requests: 1,
+ max_outstanding_write_requests: 1,
+ max_read_request_operation_count: 1,
+ max_read_request_size: '1b',
+ max_retry_delay: '1s',
+ max_write_buffer_count: 1,
+ max_write_buffer_size: '1b',
+ max_write_request_operation_count: 1,
+ max_write_request_size: '1b',
+ read_poll_timeout: '1s',
});
});
});
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts
index b488d760ede3c..62e5d64d3b8ac 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts
@@ -74,13 +74,13 @@ export const registerUpdateRoute = ({
}
// Resume follower
- const body = removeEmptyFields(
+ const resumeParams = removeEmptyFields(
serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings)
);
const responseBody = await client.asCurrentUser.ccr.resumeFollow({
index: id,
- body,
+ ...resumeParams,
});
return response.ok({
diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json b/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json
index 383c6df2a9f6f..d8f484ed21302 100644
--- a/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json
+++ b/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json
@@ -33,6 +33,7 @@
"@kbn/licensing-types",
"@kbn/data-views-plugin",
"@kbn/index-management-shared-types",
+ "@kbn/core-http-browser",
],
"exclude": [
"target/**/*",
diff --git a/x-pack/platform/test/accessibility/apps/group3/cross_cluster_replication.ts b/x-pack/platform/test/accessibility/apps/group3/cross_cluster_replication.ts
index 9e41487c8d189..6ea06c4249cdb 100644
--- a/x-pack/platform/test/accessibility/apps/group3/cross_cluster_replication.ts
+++ b/x-pack/platform/test/accessibility/apps/group3/cross_cluster_replication.ts
@@ -82,10 +82,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('auto follower index page ', async () => {
await PageObjects.crossClusterReplication.clickAutoFollowerPatternButton();
await a11y.testAppSnapshot();
- await PageObjects.crossClusterReplication.createAutoFollowerPattern(
- autoFollower,
- 'logstash*'
- );
+ await es.transport.request({
+ method: 'PUT',
+ path: `/_ccr/auto_follow/${encodeURIComponent(autoFollower)}`,
+ body: {
+ remote_cluster: remoteName,
+ leader_index_patterns: ['logstash*'],
+ follow_index_pattern: '{{leader_index}}',
+ },
+ });
+ await PageObjects.common.navigateToApp('crossClusterReplication');
+ await PageObjects.crossClusterReplication.clickAutoFollowerTab();
+ await PageObjects.crossClusterReplication.openAutoFollowerPatternDetails(autoFollower);
});
it('auto follower index flyout', async () => {
await a11y.testAppSnapshot();
diff --git a/x-pack/platform/test/functional/page_objects/cross_cluster_replication_page.ts b/x-pack/platform/test/functional/page_objects/cross_cluster_replication_page.ts
index 8daf4d0ac5398..cfe2be55d4c40 100644
--- a/x-pack/platform/test/functional/page_objects/cross_cluster_replication_page.ts
+++ b/x-pack/platform/test/functional/page_objects/cross_cluster_replication_page.ts
@@ -11,6 +11,7 @@ export function CrossClusterReplicationPageProvider({ getService }: FtrProviderC
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const comboBox = getService('comboBox');
+ const find = getService('find');
return {
async appTitleText() {
@@ -69,6 +70,22 @@ export function CrossClusterReplicationPageProvider({ getService }: FtrProviderC
return await testSubjects.isDisplayed('settingsValues');
});
},
+ async openAutoFollowerPatternDetails(name: string) {
+ await retry.waitForWithTimeout('auto-follow pattern to be listed', 20000, async () => {
+ const links = await find.allByCssSelector('[data-test-subj="autoFollowPatternLink"]');
+ for (const link of links) {
+ if ((await link.getVisibleText()) === name) {
+ await link.click();
+ return true;
+ }
+ }
+ return false;
+ });
+
+ await retry.waitForWithTimeout('auto-follow pattern details to show up', 20000, async () => {
+ return await testSubjects.isDisplayed('settingsValues');
+ });
+ },
async clickAdvancedSettingsToggle() {
await testSubjects.click('advancedSettingsToggle');
},