Skip to content

Commit 290aed0

Browse files
[Index Management] Migrate JS to TS (elastic#260603)
Closes elastic#202868. > Review tip: this PR is intentionally split into 2 commits (rename-only, then code changes). If GitHub shows some `.js → .ts` moves as delete+add, it's due to rename-detection heuristics when files changed substantially—reviewing commit-by-commit is recommended. ## Summary - Migrates the Index Management plugin public application layer (store, actions, reducers, selectors, containers, lib) to TypeScript while keeping runtime behavior stable. - Removes implicit-`any` boundaries by typing store/actions/components and aligning Redux thunk dispatch types with actual runtime contracts. - Keeps changes scoped to the migration path; no server route changes are included in this PR. ## What changed (high level) ### Runtime code - Migrated 26 JS files to TS/TSX across store (actions, reducers, selectors), containers, and lib utilities. - Added `store/types.ts` centralizing Redux state interfaces (`IndicesState`, `RowStatusState`, `TableState`, `IndexManagementState`) and `AppDispatch` (`ThunkDispatch`) for type-safe async actions. - Typed all action creators with `AppDispatch` for thunk support. - Typed reducers with `handleActions<State, any>` to work within `redux-actions` type constraints (where `createAction` returns `Action<any>`). - Typed selectors with `IndexManagementState`; fixed argument count mismatches in `getPageOfIndices`/`getPager`. - Typed container components handling `connect` + `withRouter` HOC composition. - Typed `IndexTable` class component props and state. - Typed lib utilities (`flattenPanelTree`, `indexStatusLabels`). - Removed `.d.ts` stubs that are no longer needed after full TS conversion. ### Tests - Removed `@ts-ignore` from `indices_filter.test.ts`; provided full mock state matching `IndexManagementState`. ## Parity notes - No intentional behavior changes. ## Test plan - `node scripts/type_check --project x-pack/platform/plugins/shared/index_management/tsconfig.json` — 0 errors - `node scripts/eslint $(git diff --name-only HEAD~2..HEAD --diff-filter=d)` — no errors found - `node scripts/jest x-pack/platform/plugins/shared/index_management/public/application/store/selectors/indices_filter.test.ts` — 2/2 passed - `node scripts/check_changes.ts` — all pre-commit checks passed Assisted with Cursor using Claude 4.6 Opus Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9ebd9bb commit 290aed0

50 files changed

Lines changed: 1380 additions & 563 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

x-pack/platform/packages/shared/index-management/index_management_shared_types/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import type {
99
IlmExplainLifecycleLifecycleExplain,
1010
HealthStatus,
11-
IndicesStatsIndexMetadataState,
1211
Uuid,
1312
} from '@elastic/elasticsearch/lib/api/types';
1413
import type { ScopedHistory } from '@kbn/core-application-browser';
@@ -147,7 +146,8 @@ export interface IndexAttributes {
147146
// The types from here below represent information returned from the index stats API;
148147
// treated optional as the stats API is not available on serverless
149148
health?: HealthStatus;
150-
status?: IndicesStatsIndexMetadataState;
149+
// Some consumers (e.g. the indices list UI) display user-friendly status text.
150+
status?: string;
151151
uuid?: Uuid;
152152
documents?: number;
153153
size?: number;

x-pack/platform/plugins/shared/index_management/common/types/data_streams.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
ByteSize,
1010
IndicesDataStream,
1111
IndicesDataStreamsStatsDataStreamsStatsItem,
12+
HealthStatus,
1213
Metadata,
1314
IndicesDataStreamIndex,
1415
IndicesDataStreamLifecycleWithRollover,
@@ -34,7 +35,7 @@ export type HealthFromEs = 'GREEN' | 'YELLOW' | 'RED';
3435

3536
export type DataStreamIndexFromEs = IndicesDataStreamIndex;
3637

37-
export type Health = 'green' | 'yellow' | 'red';
38+
export type Health = Lowercase<HealthStatus>;
3839

3940
export type IndexMode = (typeof IndexMode)[keyof typeof IndexMode];
4041

x-pack/platform/plugins/shared/index_management/public/application/components/data_health.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
import React from 'react';
99
import { EuiHealth } from '@elastic/eui';
10-
import type { Health } from '../../../common/types';
10+
import type { HealthStatus } from '@elastic/elasticsearch/lib/api/types';
1111

1212
interface Props {
13-
health: Health;
13+
health: HealthStatus;
1414
}
1515

16-
const healthToColor = (health: Health) => {
16+
const healthToColor = (health: HealthStatus) => {
1717
switch (health) {
1818
case 'green':
1919
return 'success';

x-pack/platform/plugins/shared/index_management/public/application/lib/flatten_panel_tree.js

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { flattenPanelTree } from './flatten_panel_tree';
9+
10+
type PanelTree = Parameters<typeof flattenPanelTree>[0];
11+
12+
const findPanel = (panels: ReturnType<typeof flattenPanelTree>, id: number) => {
13+
const panel = panels.find((p) => p.id === id);
14+
if (!panel) {
15+
throw new Error(`Missing panel with id ${id}`);
16+
}
17+
return panel;
18+
};
19+
20+
describe('flatten_panel_tree', () => {
21+
test('flattens a single panel with no items', () => {
22+
const tree: PanelTree = { id: 0 };
23+
const panels = flattenPanelTree(tree);
24+
25+
expect(panels).toHaveLength(1);
26+
expect(panels[0]).toEqual({ id: 0 });
27+
});
28+
29+
test('flattens nested panels and rewrites item.panel objects to ids', () => {
30+
const tree: PanelTree = {
31+
id: 0,
32+
title: 'root',
33+
items: [
34+
{
35+
name: 'Go to child',
36+
panel: {
37+
id: 1,
38+
title: 'child',
39+
items: [{ name: 'Leaf item' }],
40+
},
41+
},
42+
],
43+
};
44+
45+
const panels = flattenPanelTree(tree);
46+
47+
const root = findPanel(panels, 0);
48+
const child = findPanel(panels, 1);
49+
50+
expect(root.items?.[0]).toMatchObject({ name: 'Go to child', panel: 1 });
51+
expect(child.items?.[0]).toMatchObject({ name: 'Leaf item' });
52+
53+
// The input tree is not mutated
54+
const originalPanel =
55+
tree.items?.[0] && 'panel' in tree.items[0] ? tree.items[0].panel : undefined;
56+
expect(typeof originalPanel).toBe('object');
57+
expect(originalPanel).toMatchObject({ id: 1 });
58+
});
59+
60+
test('preserves separators and custom render items', () => {
61+
const tree: PanelTree = {
62+
id: 0,
63+
items: [
64+
{ isSeparator: true },
65+
{ renderItem: () => 'custom' },
66+
{ name: 'Child', panel: { id: 2, items: [{ name: 'Nested' }] } },
67+
],
68+
};
69+
70+
const panels = flattenPanelTree(tree);
71+
const root = findPanel(panels, 0);
72+
73+
expect(root.items?.[0]).toMatchObject({ isSeparator: true });
74+
expect(root.items?.[1]).toMatchObject({ renderItem: expect.any(Function) });
75+
expect(root.items?.[2]).toMatchObject({ name: 'Child', panel: 2 });
76+
expect(findPanel(panels, 2).items?.[0]).toMatchObject({ name: 'Nested' });
77+
});
78+
79+
test('does not rewrite numeric panel references', () => {
80+
const tree: PanelTree = {
81+
id: 0,
82+
items: [{ name: 'Go', panel: 2 }],
83+
};
84+
85+
const panels = flattenPanelTree(tree);
86+
const root = findPanel(panels, 0);
87+
expect(root.items?.[0]).toMatchObject({ panel: 2 });
88+
});
89+
90+
test('flattens multiple nested branches', () => {
91+
const tree: PanelTree = {
92+
id: 0,
93+
items: [
94+
{ name: 'A', panel: { id: 1, items: [{ name: 'A1' }] } },
95+
{
96+
name: 'B',
97+
panel: {
98+
id: 2,
99+
items: [{ name: 'B1', panel: { id: 3, items: [{ name: 'B2' }] } }],
100+
},
101+
},
102+
],
103+
};
104+
105+
const panels = flattenPanelTree(tree);
106+
107+
expect(findPanel(panels, 0).items).toEqual(
108+
expect.arrayContaining([
109+
expect.objectContaining({ name: 'A', panel: 1 }),
110+
expect.objectContaining({ name: 'B', panel: 2 }),
111+
])
112+
);
113+
expect(findPanel(panels, 1).items?.[0]).toMatchObject({ name: 'A1' });
114+
expect(findPanel(panels, 2).items?.[0]).toMatchObject({ name: 'B1', panel: 3 });
115+
expect(findPanel(panels, 3).items?.[0]).toMatchObject({ name: 'B2' });
116+
});
117+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { EuiContextMenuPanelDescriptor } from '@elastic/eui';
9+
import type {
10+
EuiContextMenuPanelItemDescriptorEntry,
11+
EuiContextMenuPanelItemSeparator,
12+
EuiContextMenuPanelItemRenderCustom,
13+
} from '@elastic/eui/src/components/context_menu/context_menu';
14+
15+
type PanelTreeItem =
16+
| (Omit<EuiContextMenuPanelItemDescriptorEntry, 'panel'> & {
17+
panel?: PanelTree | number;
18+
})
19+
| EuiContextMenuPanelItemSeparator
20+
| EuiContextMenuPanelItemRenderCustom;
21+
22+
type PanelTree = Omit<EuiContextMenuPanelDescriptor, 'items'> & { items?: PanelTreeItem[] };
23+
type FlattenedPanelTreeItem =
24+
| EuiContextMenuPanelItemDescriptorEntry
25+
| EuiContextMenuPanelItemSeparator
26+
| EuiContextMenuPanelItemRenderCustom;
27+
28+
const isEntryItem = (
29+
item: PanelTreeItem
30+
): item is Omit<EuiContextMenuPanelItemDescriptorEntry, 'panel'> & {
31+
panel?: PanelTree | number;
32+
} => {
33+
return 'name' in item && !('renderItem' in item);
34+
};
35+
36+
export const flattenPanelTree = (
37+
tree: PanelTree,
38+
array: EuiContextMenuPanelDescriptor[] = []
39+
): EuiContextMenuPanelDescriptor[] => {
40+
const items = tree.items?.map((item): FlattenedPanelTreeItem => {
41+
if (isEntryItem(item)) {
42+
if (item.panel && typeof item.panel !== 'number') {
43+
flattenPanelTree(item.panel, array);
44+
}
45+
46+
const panel = typeof item.panel === 'number' ? item.panel : item.panel?.id;
47+
return { ...item, panel };
48+
}
49+
return item;
50+
});
51+
52+
const { items: _items, ...panel } = tree;
53+
array.push(items ? { ...panel, items } : panel);
54+
55+
return array;
56+
};

x-pack/platform/plugins/shared/index_management/public/application/lib/index_status_labels.js renamed to x-pack/platform/plugins/shared/index_management/public/application/lib/index_status_labels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
INDEX_FORCEMERGING,
1818
} from '../../../common/constants';
1919

20-
export const indexStatusLabels = {
20+
export const indexStatusLabels: Record<string, string> = {
2121
[INDEX_CLEARING_CACHE]: i18n.translate(
2222
'xpack.idxMgmt.indexStatusLabels.clearingCacheStatusLabel',
2323
{

x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.js renamed to x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.ts

File renamed without changes.

x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.container.js

Lines changed: 0 additions & 73 deletions
This file was deleted.

0 commit comments

Comments
 (0)