[Core][UA] Forward-port 8.18 changes into main (#214996)

## Summary

We've done a lot of UA changes against 8.18 and 8.x but we did not port
them to `main`. The reason we did not do the active development against
`main` is because UA is disabled on main and we need those features in
8.last not on `9.0` initially. This port is to keep the codebase
consistent and to be able to use these new UA features in the future
post `9.0`.

Any issues that that are caused from this port we should wrap them in a
config and disabled them on main and enable this new flag on `8.x`.

What is being ported?
- [x] Upgrade assistant plugin
(`x-pack/platform/plugins/private/upgrade_assistant`)
- [x] Integration test changes
(`x-pack/test/upgrade_assistant_integration`)
- [x] new UA doc links
(`src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts`)
- [x] localization files sync
(`x-pack/platform/plugins/private/translations/translations/*.json`)
- [x] Unfreeze is no longer supported after `8.x`. So removed it from
data streams readonly migration step and throw an error for regular
indices migration.

Closes https://github.com/elastic/kibana/issues/210490

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ahmad Bamieh 2025-03-21 18:46:46 +02:00 committed by GitHub
parent 11a512e735
commit f83612f4e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
132 changed files with 6303 additions and 1859 deletions

2
.github/CODEOWNERS vendored
View file

@ -883,7 +883,7 @@ x-pack/platform/plugins/private/snapshot_restore @elastic/kibana-management
x-pack/platform/plugins/private/telemetry_collection_xpack @elastic/kibana-core
x-pack/platform/plugins/private/transform @elastic/ml-ui
x-pack/platform/plugins/private/translations @elastic/kibana-localization
x-pack/platform/plugins/private/upgrade_assistant @elastic/kibana-core
x-pack/platform/plugins/private/upgrade_assistant @elastic/kibana-management
x-pack/platform/plugins/private/watcher @elastic/kibana-management
x-pack/platform/plugins/shared/actions @elastic/response-ops
x-pack/platform/plugins/shared/ai_infra/llm_tasks @elastic/appex-ai-infra

View file

@ -200,6 +200,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`,
textEmbedding: `${MACHINE_LEARNING_DOCS}ml-nlp-model-ref.html#ml-nlp-model-ref-text-embedding`,
troubleshootSetup: `${ENTERPRISE_SEARCH_DOCS}troubleshoot-setup.html`,
upgrade9x: `${ENTERPRISE_SEARCH_DOCS}upgrading-to-9-x.html`,
usersAccess: `${ENTERPRISE_SEARCH_DOCS}users-access.html`,
},
metricbeat: {
@ -301,6 +302,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
upgradeAssistant: {
overview: `${KIBANA_DOCS}upgrade-assistant.html`,
batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`,
indexBlocks: `${ELASTICSEARCH_DOCS}index-modules-blocks.html#index-block-settings`,
remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`,
unfreezeApi: `${ELASTICSEARCH_DOCS}unfreeze-index-api.html`,
reindexWithPipeline: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-with-an-ingest-pipeline`,

View file

@ -169,6 +169,7 @@ export interface DocLinks {
readonly textEmbedding: string;
readonly troubleshootSetup: string;
readonly usersAccess: string;
readonly upgrade9x: string;
};
readonly heartbeat: {
readonly base: string;
@ -261,6 +262,7 @@ export interface DocLinks {
readonly upgradeAssistant: {
readonly overview: string;
readonly batchReindex: string;
readonly indexBlocks: string;
readonly remoteReindex: string;
readonly unfreezeApi: string;
readonly reindexWithPipeline: string;

View file

@ -47450,9 +47450,6 @@
"xpack.upgradeAssistant.esDeprecations.clusterSettings.deletingButtonLabel": "Retrait des paramètres en cours…",
"xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionText": "Retirer les paramètres",
"xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionTooltipLabel": "Corrigez ce problème en retirant les paramètres de ce cluster. La correction peut s'effectuer automatiquement.",
"xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText": "Chargement du statut…",
"xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel": "Marquer en lecture seule ou réindexer",
"xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel": "Ce problème peut être résolu en réindexant ce flux de données ou en marquant ses index en lecture seule. La correction peut s'effectuer automatiquement.",
"xpack.upgradeAssistant.esDeprecations.dataStreamsTypeLabel": "Flux de données",
"xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel": "Manuel",
"xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel": "Fermer",

View file

@ -47415,9 +47415,6 @@
"xpack.upgradeAssistant.esDeprecations.clusterSettings.deletingButtonLabel": "設定を削除中…",
"xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionText": "設定の削除",
"xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionTooltipLabel": "このクラスターから設定を削除して、この問題を解決します。この問題は自動的に解決できます。",
"xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText": "ステータスを読み込んでいます...",
"xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel": "読み取り専用に設定または再インデックス",
"xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel": "この問題を解決するには、このデータストリームを再インデックス化するか、そのインデックスを読み取り専用に設定します。この問題は自動的に解決できます。",
"xpack.upgradeAssistant.esDeprecations.dataStreamsTypeLabel": "データストリーム",
"xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel": "手動",
"xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel": "閉じる",

View file

@ -47489,9 +47489,6 @@
"xpack.upgradeAssistant.esDeprecations.clusterSettings.deletingButtonLabel": "设置移除进行中……",
"xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionText": "移除设置",
"xpack.upgradeAssistant.esDeprecations.clusterSettings.resolutionTooltipLabel": "通过从此集群中移除设置来解决该问题。此问题会自动解决。",
"xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText": "正在加载状态……",
"xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel": "标记为只读,或重新索引",
"xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel": "请通过重新索引该数据流或将其索引标记为只读来解决此问题。此问题会自动解决。",
"xpack.upgradeAssistant.esDeprecations.dataStreamsTypeLabel": "数据流",
"xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel": "手动",
"xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel": "关闭",

View file

@ -29,7 +29,7 @@ These surface runtime deprecations, e.g. a Painless script that uses a deprecate
request to a deprecated API. These are also generally surfaced as deprecation headers within the
response. Even if the cluster state is good, app maintainers need to watch the logs in case
deprecations are discovered as data is migrated. Starting in 7.x, deprecation logs can be written to a file or a data stream ([#58924](https://github.com/elastic/elasticsearch/pull/58924)). When the data stream exists, the Upgrade Assistant provides a way to analyze the logs through Observability or Discover ([#106521](https://github.com/elastic/kibana/pull/106521)).
* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/main/src/core/server/docs/kib_core_deprecations_service.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team.
* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/main/src/core/server/deprecations/README.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team.
### Fixing problems

View file

@ -35,6 +35,11 @@ export const MOCK_REINDEX_DEPRECATION: EnrichedDeprecationInfo = {
index: 'reindex_index',
correctiveAction: {
type: 'reindex',
metadata: {
isClosedIndex: false,
isFrozenIndex: false,
isInDataStream: false,
},
},
};

View file

@ -21,6 +21,9 @@ const defaultReindexStatusMeta: ReindexStatusResponse['meta'] = {
indexName: 'foo',
reindexName: 'reindexed-foo',
aliases: [],
isFrozen: false,
isReadonly: false,
isInDataStream: false,
};
describe('Reindex deprecation flyout', () => {
@ -75,28 +78,22 @@ describe('Reindex deprecation flyout', () => {
expect(exists('reindexDetails')).toBe(true);
expect(find('reindexDetails.flyoutTitle').text()).toContain(
`Reindex ${reindexDeprecation.index}`
`Update ${reindexDeprecation.index}`
);
});
it('renders error callout when reindex fails', async () => {
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
testBed.component.update();
const { actions, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
httpRequestsMockHelpers.setStartReindexingResponse(MOCK_REINDEX_DEPRECATION.index!, undefined, {
statusCode: 404,
message: 'no such index [test]',
});
await actions.reindexDeprecationFlyout.clickReindexButton();
const { actions, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(exists('reindexDetails')).toBe(true);
expect(exists('reindexDetails.reindexingFailedCallout')).toBe(true);
});
@ -106,28 +103,16 @@ describe('Reindex deprecation flyout', () => {
message: 'no such index [test]',
});
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
testBed.component.update();
const { actions, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(exists('reindexDetails.fetchFailedCallout')).toBe(true);
});
describe('reindexing progress', () => {
it('has not started yet', async () => {
const { actions, find, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
expect(find('reindexChecklistTitle').text()).toEqual('Reindexing process');
expect(exists('cancelReindexingDocumentsButton')).toBe(false);
});
it('has started but not yet reindexing documents', async () => {
httpRequestsMockHelpers.setReindexStatusResponse(MOCK_REINDEX_DEPRECATION.index!, {
reindexOp: {
@ -140,14 +125,11 @@ describe('Reindex deprecation flyout', () => {
meta: defaultReindexStatusMeta,
});
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
testBed.component.update();
const { actions, find, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 5%');
expect(exists('cancelReindexingDocumentsButton')).toBe(false);
@ -165,14 +147,11 @@ describe('Reindex deprecation flyout', () => {
meta: defaultReindexStatusMeta,
});
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
testBed.component.update();
const { actions, find, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 30%');
expect(exists('cancelReindexingDocumentsButton')).toBe(true);
@ -190,14 +169,11 @@ describe('Reindex deprecation flyout', () => {
meta: defaultReindexStatusMeta,
});
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
testBed.component.update();
const { actions, find, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 90%');
expect(exists('cancelReindexingDocumentsButton')).toBe(false);
@ -215,14 +191,11 @@ describe('Reindex deprecation flyout', () => {
meta: defaultReindexStatusMeta,
});
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
const { actions, find, exists, component } = testBed;
component.update();
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 95%');
expect(exists('cancelReindexingDocumentsButton')).toBe(false);
@ -250,14 +223,11 @@ describe('Reindex deprecation flyout', () => {
},
]);
await act(async () => {
testBed = await setupElasticsearchPage(httpSetup);
});
testBed.component.update();
const { actions, find } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);
await actions.reindexDeprecationFlyout.clickReindexButton(); // details step
await actions.reindexDeprecationFlyout.clickReindexButton(); // warning step
expect(find('lowDiskSpaceCallout').text()).toContain('Nodes with low disk space');
expect(find('impactedNodeListItem').length).toEqual(1);

View file

@ -51,6 +51,7 @@ const servicesMock = {
const idToUrlMap = {
SNAPSHOT_RESTORE_LOCATOR: 'snapshotAndRestoreUrl',
DISCOVER_APP_LOCATOR: 'discoverUrl',
OBS_LOGS_EXPLORER_DATA_VIEW_LOCATOR: 'logsExplorerUrl',
};
type IdKey = keyof typeof idToUrlMap;
@ -75,6 +76,7 @@ shareMock.url.locators.get = (id: IdKey) => ({
});
export const getAppContextMock = (kibanaVersion: SemVer) => ({
dataSourceExclusions: {},
featureSet: {
mlSnapshots: true,
migrateSystemIndices: true,

View file

@ -6,6 +6,7 @@
*/
export interface DataStreamsActionMetadata {
excludedActions?: Array<'readOnly' | 'reindex'>;
totalBackingIndices: number;
indicesRequiringUpgradeCount: number;
indicesRequiringUpgrade: string[];
@ -14,6 +15,7 @@ export interface DataStreamsActionMetadata {
reindexRequired: boolean;
}
export type DataStreamResolutionType = 'readonly' | 'reindex';
export interface DataStreamsAction {
type: 'dataStream';
metadata: DataStreamsActionMetadata;
@ -34,21 +36,22 @@ export interface DataStreamMetadata {
}
export interface DataStreamReindexStatusResponse {
warnings?: DataStreamReindexWarning[];
reindexOp?: DataStreamReindexOperation;
warnings?: DataStreamMigrationWarning[];
migrationOp?: DataStreamMigrationOperation;
hasRequiredPrivileges?: boolean;
}
export type DataStreamReindexWarningTypes = 'incompatibleDataStream';
export type DataStreamWarningTypes = 'incompatibleDataStream' | 'affectExistingSetups';
export interface DataStreamReindexWarning {
warningType: DataStreamReindexWarningTypes;
export interface DataStreamMigrationWarning {
warningType: DataStreamWarningTypes;
resolutionType: DataStreamResolutionType;
meta?: {
[key: string]: string | string[];
};
}
export enum DataStreamReindexStatus {
export enum DataStreamMigrationStatus {
notStarted,
inProgress,
completed,
@ -66,31 +69,35 @@ export interface DataStreamProgressDetails {
}
export interface DataStreamReindexStatusNotStarted {
status: DataStreamReindexStatus.notStarted;
status: DataStreamMigrationStatus.notStarted;
}
export interface DataStreamReindexStatusInProgress {
status: DataStreamReindexStatus.inProgress;
reindexTaskPercComplete: number;
resolutionType: 'reindex' | 'readonly';
status: DataStreamMigrationStatus.inProgress;
taskPercComplete: number;
progressDetails: DataStreamProgressDetails;
}
export interface DataStreamReindexStatusCompleted {
status: DataStreamReindexStatus.completed;
reindexTaskPercComplete: number;
resolutionType: 'reindex' | 'readonly';
status: DataStreamMigrationStatus.completed;
taskPercComplete: number;
progressDetails: DataStreamProgressDetails;
}
export interface DataStreamReindexStatusFailed {
status: DataStreamReindexStatus.failed;
resolutionType: 'reindex' | 'readonly';
status: DataStreamMigrationStatus.failed;
errorMessage: string;
}
export interface DataStreamReindexStatusCancelled {
status: DataStreamReindexStatus.cancelled;
resolutionType: 'reindex' | 'readonly';
status: DataStreamMigrationStatus.cancelled;
}
export type DataStreamReindexOperation =
export type DataStreamMigrationOperation =
| DataStreamReindexStatusNotStarted
| DataStreamReindexStatusInProgress
| DataStreamReindexStatusCompleted

View file

@ -6,7 +6,7 @@
*/
import { HealthReportImpact } from '@elastic/elasticsearch/lib/api/types';
import type { estypes } from '@elastic/elasticsearch';
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import { SavedObject } from '@kbn/core/types';
import type { DataStreamsAction } from './data_stream_types';
@ -54,8 +54,11 @@ export interface ReindexStatusResponse {
reindexName: string;
// Array of aliases pointing to the index being reindexed
aliases: string[];
isReadonly: boolean;
isFrozen: boolean;
isInDataStream: boolean;
};
warnings?: ReindexWarning[];
warnings?: IndexWarning[];
reindexOp?: ReindexOperation;
hasRequiredPrivileges?: boolean;
}
@ -136,10 +139,11 @@ export interface ReindexOperation {
export type ReindexSavedObject = SavedObject<ReindexOperation>;
// 8.0 -> 9.0 warnings
export type ReindexWarningTypes = 'indexSetting' | 'replaceIndexWithAlias';
export type IndexWarningType = 'indexSetting' | 'replaceIndexWithAlias' | 'makeIndexReadonly';
export interface ReindexWarning {
warningType: ReindexWarningTypes;
export interface IndexWarning {
warningType: IndexWarningType;
flow: 'reindex' | 'readonly' | 'all';
/**
* Optional metadata for deprecations
*
@ -147,7 +151,7 @@ export interface ReindexWarning {
* For "indexSetting" we want to surface the deprecated settings.
*/
meta?: {
[key: string]: string | string[];
[key: string]: string | string[] | boolean;
};
}
@ -190,16 +194,36 @@ export interface DeprecationInfo {
export interface IndexSettingsDeprecationInfo {
[indexName: string]: DeprecationInfo[];
}
export interface ReindexAction {
type: 'reindex';
export interface IndexMetadata {
isFrozenIndex: boolean;
isInDataStream: boolean;
isClosedIndex: boolean;
}
export interface IndexAction {
/**
* Indicate what blockers have been detected for calling reindex
* against this index.
*
* @remark
* In future this could be an array of blockers.
* Includes relevant information about the index related to this action
*/
blockerForReindexing?: 'index-closed'; // 'index-closed' can be handled automatically, but requires more resources, user should be warned
metadata: IndexMetadata;
}
export interface ReindexAction extends IndexAction {
type: 'reindex';
/**
* The transform IDs that are currently targeting this index
*/
transformIds?: string[];
/**
* The actions that should be excluded from the reindex corrective action.
*/
excludedActions?: string[];
}
export interface UnfreezeAction extends IndexAction {
type: 'unfreeze';
}
export interface MlAction {
@ -225,6 +249,15 @@ export interface HealthIndicatorAction {
impacts: HealthReportImpact[];
}
export type CorrectiveAction =
| ReindexAction
| UnfreezeAction
| MlAction
| IndexSettingAction
| ClusterSettingAction
| DataStreamsAction
| HealthIndicatorAction;
export interface EnrichedDeprecationInfo
extends Omit<
estypes.MigrationDeprecationsDeprecation,
@ -237,16 +270,9 @@ export interface EnrichedDeprecationInfo
| 'ilm_policies'
| 'templates';
isCritical: boolean;
frozen?: boolean;
status?: estypes.HealthReportIndicatorHealthStatus;
index?: string;
correctiveAction?:
| ReindexAction
| MlAction
| IndexSettingAction
| ClusterSettingAction
| DataStreamsAction
| HealthIndicatorAction;
correctiveAction?: CorrectiveAction;
resolveDuringUpgrade: boolean;
}
@ -322,3 +348,5 @@ export interface FeatureSet {
reindexCorrectiveActions: boolean;
migrateDataStreams: boolean;
}
export type DataSourceExclusions = Record<string, Array<'readOnly' | 'reindex'>>;

View file

@ -0,0 +1,8 @@
/*
* 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 type UpdateIndexOperation = 'blockWrite' | 'unfreeze';

View file

@ -1,9 +1,7 @@
{
"type": "plugin",
"id": "@kbn/upgrade-assistant-plugin",
"owner": [
"@elastic/kibana-core"
],
"owner": "@elastic/kibana-management",
"group": "platform",
"visibility": "private",
"plugin": {

View file

@ -11,10 +11,25 @@
margin-right: $euiSizeM;
}
$stepStatusToCallOutColor: (
failed: $euiColorDanger,
complete: $euiColorSuccess,
paused: $euiColorWarning,
cancelled: $euiColorWarning,
);
.upgStepProgress__status--circle {
text-align: center;
border-radius: $euiSizeM;
line-height: $euiSize - 2px;
@each $status, $callOutColor in $stepStatusToCallOutColor {
&-#{$status} {
$statusBg: tintOrShade($callOutColor, 90%, 70%);
color: shadeOrTint(makeHighContrastColor($callOutColor, $statusBg), 0, 20%);
background-color: $statusBg;
}
}
}
.upgStepProgress__title {

View file

@ -8,64 +8,38 @@
import classNames from 'classnames';
import React, { Fragment, ReactNode } from 'react';
import { EuiIcon, EuiLoadingSpinner, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
import './_step_progress.scss';
type STATUS = 'incomplete' | 'inProgress' | 'complete' | 'failed' | 'paused' | 'cancelled';
const StepStatus: React.FunctionComponent<{ status: STATUS; idx: number }> = ({ status, idx }) => {
const { euiTheme } = useEuiTheme();
if (status === 'incomplete') {
return <span className="upgStepProgress__status">{idx + 1}.</span>;
} else if (status === 'inProgress') {
return <EuiLoadingSpinner size="m" className="upgStepProgress__status" />;
} else if (status === 'complete') {
return (
<span
className="upgStepProgress__status upgStepProgress__status--circle"
css={css`
color: ${euiTheme.colors.textSuccess};
background-color: ${euiTheme.colors.backgroundBaseSuccess};
`}
>
<span className="upgStepProgress__status upgStepProgress__status--circle upgStepProgress__status--circle-complete">
<EuiIcon type="check" size="s" />
</span>
);
} else if (status === 'paused') {
return (
<span
className="upgStepProgress__status upgStepProgress__status--circle"
css={css`
color: ${euiTheme.colors.textWarning};
background-color: ${euiTheme.colors.backgroundBaseWarning};
`}
>
<span className="upgStepProgress__status upgStepProgress__status--circle upgStepProgress__status--circle-paused">
<EuiIcon type="pause" size="s" />
</span>
);
} else if (status === 'cancelled') {
return (
<span
className="upgStepProgress__status upgStepProgress__status--circle"
css={css`
color: ${euiTheme.colors.textWarning};
background-color: ${euiTheme.colors.backgroundBaseWarning};
`}
>
<span className="upgStepProgress__status upgStepProgress__status--circle upgStepProgress__status--circle-cancelled">
<EuiIcon type="cross" size="s" />
</span>
);
} else if (status === 'failed') {
return (
<span
className="upgStepProgress__status upgStepProgress__status--circle"
css={css`
color: ${euiTheme.colors.textDanger};
background-color: ${euiTheme.colors.backgroundBaseDanger};
`}
>
<span className="upgStepProgress__status upgStepProgress__status--circle upgStepProgress__status--circle-failed">
<EuiIcon type="cross" size="s" />
</span>
);

View file

@ -8,22 +8,31 @@
import React, { createContext, useContext } from 'react';
import { ApiService } from '../../../../lib/api';
import { useReindexStatus, ReindexState } from './use_reindex_state';
import { useMigrationStatus, MigrationState } from './use_migration_state';
import type { DataStreamResolutionType } from '../../../../../../common/types';
export interface ReindexStateContext {
reindexState: ReindexState;
startReindex: () => Promise<void>;
export interface MigrationStateContext {
loadDataStreamMetadata: () => Promise<void>;
migrationState: MigrationState;
initMigration: (resolutionType: DataStreamResolutionType) => void;
// reindex resolution actions
startReindex: () => Promise<void>;
cancelReindex: () => Promise<void>;
// readonly resolution actions
startReadonly: () => Promise<void>;
cancelReadonly: () => Promise<void>;
}
const DataStreamReindexContext = createContext<ReindexStateContext | undefined>(undefined);
const DataStreamMigrationContext = createContext<MigrationStateContext | undefined>(undefined);
export const useDataStreamReindexContext = () => {
const context = useContext(DataStreamReindexContext);
export const useDataStreamMigrationContext = () => {
const context = useContext(DataStreamMigrationContext);
if (context === undefined) {
throw new Error(
'useDataStreamReindexContext must be used within a <DataStreamReindexStatusProvider />'
'useDataStreamMigrationContext must be used within a <DataStreamMigrationStatusProvider />'
);
}
return context;
@ -35,26 +44,37 @@ interface Props {
dataStreamName: string;
}
export const DataStreamReindexStatusProvider: React.FunctionComponent<Props> = ({
export const DataStreamMigrationStatusProvider: React.FunctionComponent<Props> = ({
api,
dataStreamName,
children,
}) => {
const { reindexState, startReindex, loadDataStreamMetadata, cancelReindex } = useReindexStatus({
const {
migrationState,
cancelReadonly,
startReindex,
loadDataStreamMetadata,
cancelReindex,
startReadonly,
initMigration,
} = useMigrationStatus({
dataStreamName,
api,
});
return (
<DataStreamReindexContext.Provider
<DataStreamMigrationContext.Provider
value={{
reindexState,
migrationState,
startReindex,
cancelReindex,
startReadonly,
cancelReadonly,
initMigration,
loadDataStreamMetadata,
}}
>
{children}
</DataStreamReindexContext.Provider>
</DataStreamMigrationContext.Provider>
);
};

View file

@ -18,29 +18,33 @@ import { METRIC_TYPE } from '@kbn/analytics';
import moment from 'moment';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import {
DataStreamReindexStatus,
DataStreamMigrationStatus,
DataStreamsAction,
EnrichedDeprecationInfo,
} from '../../../../../../../common/types';
import { ReindexStateContext } from '../context';
import { MigrationStateContext } from '../context';
import { DeprecationBadge } from '../../../../shared';
import {
UIM_DATA_STREAM_REINDEX_START_CLICK,
UIM_DATA_STREAM_REINDEX_STOP_CLICK,
UIM_DATA_STREAM_START_READONLY_CLICK,
UIM_DATA_STREAM_STOP_READONLY_CLICK,
uiMetricService,
} from '../../../../../lib/ui_metric';
import { containerMessages } from './messages';
import type { FlyoutStep } from './steps/types';
import { InitializingFlyoutStep } from './steps/initializing';
import { ConfirmReindexingFlyoutStep } from './steps/confirm';
import { ConfirmMigrationFlyoutStep } from './steps/confirm';
import { DataStreamDetailsFlyoutStep } from './steps/details';
import { ChecklistFlyoutStep } from './steps/checklist';
import { ReindexingCompletedFlyoutStep } from './steps/completed';
import { MigrationCompletedFlyoutStep } from './steps/completed';
interface Props extends ReindexStateContext {
interface Props extends MigrationStateContext {
deprecation: EnrichedDeprecationInfo;
closeFlyout: () => void;
}
@ -51,30 +55,32 @@ const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b';
export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
cancelReindex,
loadDataStreamMetadata,
reindexState,
migrationState,
startReindex,
startReadonly,
initMigration,
cancelReadonly,
closeFlyout,
deprecation,
}) => {
const { status, reindexWarnings, errorMessage, meta } = reindexState;
const { index } = deprecation;
const { status, migrationWarnings, errorMessage, resolutionType, meta } = migrationState;
const { index, correctiveAction } = deprecation;
const [flyoutStep, setFlyoutStep] = useState<FlyoutStep>('initializing');
const switchFlyoutStep = useCallback(() => {
switch (status) {
case DataStreamReindexStatus.notStarted: {
case DataStreamMigrationStatus.notStarted: {
setFlyoutStep('notStarted');
return;
}
case DataStreamReindexStatus.failed:
case DataStreamReindexStatus.fetchFailed:
case DataStreamReindexStatus.cancelled:
case DataStreamReindexStatus.inProgress: {
case DataStreamMigrationStatus.failed:
case DataStreamMigrationStatus.fetchFailed:
case DataStreamMigrationStatus.cancelled:
case DataStreamMigrationStatus.inProgress: {
setFlyoutStep('inProgress');
return;
}
case DataStreamReindexStatus.completed: {
case DataStreamMigrationStatus.completed: {
setTimeout(() => {
// wait for 1.5 more seconds fur the UI to visually get to 100%
setFlyoutStep('completed');
@ -97,11 +103,21 @@ export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
await startReindex();
}, [startReindex]);
const onStartReadonly = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_START_READONLY_CLICK);
await startReadonly();
}, [startReadonly]);
const onStopReindex = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_REINDEX_STOP_CLICK);
await cancelReindex();
}, [cancelReindex]);
const onStopReadonly = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DATA_STREAM_STOP_READONLY_CLICK);
await cancelReadonly();
}, [cancelReadonly]);
const { docsSizeFormatted, indicesRequiringUpgradeDocsCount, lastIndexCreationDateFormatted } =
useMemo(() => {
if (!meta) {
@ -143,77 +159,93 @@ export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
return (
<DataStreamDetailsFlyoutStep
correctiveAction={correctiveAction as DataStreamsAction}
closeFlyout={closeFlyout}
lastIndexCreationDateFormatted={lastIndexCreationDateFormatted}
meta={meta}
startReindex={() => {
initAction={(selectedResolutionType) => {
initMigration(selectedResolutionType);
setFlyoutStep('confirm');
}}
reindexState={reindexState}
lastIndexCreationDateFormatted={lastIndexCreationDateFormatted}
meta={meta}
migrationState={migrationState}
/>
);
}
case 'confirm': {
if (!meta) {
if (!meta || !resolutionType) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return (
<ConfirmReindexingFlyoutStep
warnings={reindexWarnings ?? []}
<ConfirmMigrationFlyoutStep
warnings={(migrationWarnings ?? []).filter(
(warning) => warning.resolutionType === resolutionType
)}
meta={meta}
resolutionType={resolutionType}
hideWarningsStep={() => {
setFlyoutStep('notStarted');
}}
continueReindex={() => {
onStartReindex();
startAction={() => {
if (resolutionType === 'readonly') {
onStartReadonly();
} else {
onStartReindex();
}
}}
/>
);
}
case 'inProgress': {
if (!meta) {
if (!meta || !resolutionType) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return (
<ChecklistFlyoutStep
closeFlyout={closeFlyout}
startReindex={() => {
executeAction={() => {
setFlyoutStep('confirm');
}}
reindexState={reindexState}
cancelReindex={onStopReindex}
resolutionType={resolutionType}
migrationState={migrationState}
cancelAction={() => {
if (resolutionType === 'readonly') {
onStopReadonly();
} else {
onStopReindex();
}
}}
/>
);
}
case 'completed': {
if (!meta) {
return (
<InitializingFlyoutStep
errorMessage={errorMessage || containerMessages.errorLoadingDataStreamInfo}
/>
);
}
return <ReindexingCompletedFlyoutStep meta={meta} />;
return <MigrationCompletedFlyoutStep meta={meta} resolutionType={resolutionType} />;
}
}
}, [
flyoutStep,
reindexState,
migrationState,
closeFlyout,
onStartReindex,
onStopReindex,
lastIndexCreationDateFormatted,
reindexWarnings,
migrationWarnings,
meta,
errorMessage,
onStartReadonly,
onStopReadonly,
resolutionType,
initMigration,
correctiveAction,
]);
return (
@ -222,7 +254,7 @@ export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
<EuiFlyoutHeader hasBorder>
<DeprecationBadge
isCritical={deprecation.isCritical}
isResolved={status === DataStreamReindexStatus.completed}
isResolved={status === DataStreamMigrationStatus.completed}
/>
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
@ -237,7 +269,12 @@ export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
textStyle="reverse"
listItems={[
{
title: 'Reindexing required for indices created on or before',
title: i18n.translate(
'xpack.upgradeAssistant.dataStream.flyout.container.affectedIndicesCreatedOnOrBefore',
{
defaultMessage: 'Migration required for indices created on or before',
}
),
description: lastIndexCreationDateFormatted,
},
]}
@ -249,7 +286,12 @@ export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
textStyle="reverse"
listItems={[
{
title: 'Size',
title: i18n.translate(
'xpack.upgradeAssistant.dataStream.flyout.container.indicesDocsSize',
{
defaultMessage: 'Size',
}
),
description: docsSizeFormatted,
},
]}
@ -260,7 +302,12 @@ export const DataStreamReindexFlyout: React.FunctionComponent<Props> = ({
textStyle="reverse"
listItems={[
{
title: 'Document Count',
title: i18n.translate(
'xpack.upgradeAssistant.dataStream.flyout.container.indicesDocsCount',
{
defaultMessage: 'Document Count',
}
),
description: indicesRequiringUpgradeDocsCount,
},
]}

View file

@ -8,37 +8,46 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { DataStreamReindexStatus } from '../../../../../../../common/types';
import {
DataStreamMigrationStatus,
DataStreamResolutionType,
} from '../../../../../../../common/types';
export const getPrimaryButtonLabel = (status?: DataStreamReindexStatus) => {
export const getPrimaryButtonLabel = (
status?: DataStreamMigrationStatus,
resolutionType?: DataStreamResolutionType
) => {
switch (status) {
case DataStreamReindexStatus.fetchFailed:
case DataStreamReindexStatus.failed:
case DataStreamMigrationStatus.fetchFailed:
case DataStreamMigrationStatus.failed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.tryAgainLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.reindexButton.tryAgainLabel"
defaultMessage="Try again"
/>
);
case DataStreamReindexStatus.inProgress:
case DataStreamMigrationStatus.inProgress:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.reindexingLabel"
defaultMessage="Reindexing…"
id="xpack.upgradeAssistant.dataStream.migration.flyout.reindexButton.reindexingLabel"
defaultMessage="{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migrating}}…"
values={{ resolutionType }}
/>
);
case DataStreamReindexStatus.cancelled:
case DataStreamMigrationStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.restartLabel"
defaultMessage="Restart reindexing"
id="xpack.upgradeAssistant.dataStream.migration.flyout.reindexButton.restartLabel"
defaultMessage="{resolutionType, select, reindex {Restart reindexing} readonly {Restart marking as read-only} other {Restart migration}}"
values={{ resolutionType }}
/>
);
default:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.reindexButton.runReindexLabel"
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.dataStream.migration.flyout.reindexButton.runReindexLabel"
defaultMessage="{resolutionType, select, reindex {Start reindexing} readonly {Start marking as read-only} other {Start migration}}"
values={{ resolutionType }}
/>
);
}

View file

@ -20,10 +20,13 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataStreamReindexStatus } from '../../../../../../../../../common/types';
import {
DataStreamMigrationStatus,
DataStreamResolutionType,
} from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex_state';
import { ReindexProgress } from './progress';
import type { MigrationState } from '../../../use_migration_state';
import { MigrationProgress } from './progress';
import { useAppContext } from '../../../../../../../app_context';
import { getPrimaryButtonLabel } from '../../messages';
@ -32,25 +35,26 @@ import { getPrimaryButtonLabel } from '../../messages';
*/
export const ChecklistFlyoutStep: React.FunctionComponent<{
closeFlyout: () => void;
reindexState: ReindexState;
startReindex: () => void;
cancelReindex: () => void;
}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => {
migrationState: MigrationState;
resolutionType: DataStreamResolutionType;
executeAction: () => void;
cancelAction: () => void;
}> = ({ closeFlyout, migrationState, resolutionType, executeAction, cancelAction }) => {
const {
services: { api },
} = useAppContext();
const { loadingState, status, hasRequiredPrivileges } = reindexState;
const { loadingState, status, hasRequiredPrivileges } = migrationState;
const loading =
loadingState === LoadingState.Loading || status === DataStreamReindexStatus.inProgress;
const isCompleted = status === DataStreamReindexStatus.completed;
const hasFetchFailed = status === DataStreamReindexStatus.fetchFailed;
const hasReindexingFailed = status === DataStreamReindexStatus.failed;
loadingState === LoadingState.Loading || status === DataStreamMigrationStatus.inProgress;
const isCompleted = status === DataStreamMigrationStatus.completed;
const hasFetchFailed = status === DataStreamMigrationStatus.fetchFailed;
const hasMigrationFailed = status === DataStreamMigrationStatus.failed;
const { data: nodes } = api.useLoadNodeDiskSpace();
const showMainButton = !hasFetchFailed && !isCompleted && hasRequiredPrivileges;
const shouldShowCancelButton = showMainButton && status === DataStreamReindexStatus.inProgress;
const shouldShowCancelButton = showMainButton && status === DataStreamMigrationStatus.inProgress;
return (
<Fragment>
@ -61,8 +65,8 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this index"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to migrate this data stream"
/>
}
color="danger"
@ -79,15 +83,15 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.lowDiskSpaceCalloutTitle"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent migration. The following nodes are impacted:"
/>
<EuiSpacer size="s" />
@ -96,7 +100,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.lowDiskSpaceUsedText"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
@ -112,27 +116,30 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
</>
)}
{(hasFetchFailed || hasReindexingFailed) && (
{(hasFetchFailed || hasMigrationFailed) && (
<>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'reindexingFailedCallout'}
data-test-subj={
hasFetchFailed ? 'fetchFailedCallout' : 'dataStreamMigrationFailedCallout'
}
title={
hasFetchFailed ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.fetchFailedCalloutTitle"
defaultMessage="Reindex status not available"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.fetchFailedCalloutTitle"
defaultMessage="Migration status not available"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingFailedCalloutTitle"
defaultMessage="Reindexing error"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.migrationFailedCalloutTitle"
defaultMessage="{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} error"
values={{ resolutionType }}
/>
)
}
>
{reindexState.errorMessage}
{migrationState.errorMessage}
</EuiCallOut>
<EuiSpacer />
</>
@ -142,19 +149,19 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
<p>
<FormattedMessage
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress."
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
/>
</p>
</EuiText>
<EuiSpacer />
<ReindexProgress reindexState={reindexState} />
<MigrationProgress migrationState={migrationState} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.closeButtonLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
@ -167,13 +174,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
<EuiButton
color={'accent'}
iconType={'pause'}
onClick={cancelReindex}
onClick={cancelAction}
disabled={!hasRequiredPrivileges}
data-test-subj="cancelDataStreamReindexingButton"
data-test-subj="cancelDataStreamMigrationButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.cancelReindexButtonLabel"
defaultMessage="Cancel reindexing"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.cancelMigrationButtonLabel"
defaultMessage="Cancel {resolutionType, select, reindex {reindexing} readonly {marking as read-only} other {migration}}"
values={{ resolutionType }}
/>
</EuiButton>
</EuiFlexItem>
@ -183,12 +191,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
<EuiFlexItem grow={false}>
<EuiButton
fill
color={status === DataStreamReindexStatus.inProgress ? 'primary' : 'warning'}
iconType={status === DataStreamReindexStatus.inProgress ? undefined : 'refresh'}
onClick={startReindex}
color={status === DataStreamMigrationStatus.inProgress ? 'primary' : 'warning'}
iconType={
status === DataStreamMigrationStatus.inProgress ? undefined : 'refresh'
}
onClick={executeAction}
isLoading={loading}
disabled={loading || !hasRequiredPrivileges}
data-test-subj="startDataStreamReindexingButton"
data-test-subj="startDataStreamMigrationButton"
>
{getPrimaryButtonLabel(status)}
</EuiButton>

View file

@ -12,37 +12,39 @@ import { FormattedMessage, FormattedRelativeTime } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { DataStreamReindexStatus } from '../../../../../../../../../common/types';
import type { ReindexState } from '../../../use_reindex_state';
import { StepProgress, StepProgressStep } from '../../../../reindex/flyout/step_progress';
import { DataStreamMigrationStatus } from '../../../../../../../../../common/types';
import type { MigrationState } from '../../../use_migration_state';
import { getDataStreamReindexProgress } from '../../../../../../../lib/utils';
import { ReindexingDocumentsStepTitle } from './progress_title';
import { MigrateDocumentsStepTitle } from './progress_title';
import { CancelLoadingState } from '../../../../../../types';
import { StepProgress, type StepProgressStep } from '../../../../../common/step_progress';
interface Props {
reindexState: ReindexState;
migrationState: MigrationState;
}
/**
* Displays a list of steps in the reindex operation, the current status, a progress bar,
* and any error messages that are encountered.
*/
export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
const { status, reindexTaskPercComplete, cancelLoadingState, taskStatus } = props.reindexState;
export const MigrationProgress: React.FunctionComponent<Props> = (props) => {
const { status, taskPercComplete, cancelLoadingState, taskStatus, resolutionType } =
props.migrationState;
// The reindexing step is special because it generally lasts longer and can be cancelled mid-flight
const reindexingDocsStep = {
title: (
<EuiFlexGroup component="span">
<EuiFlexItem grow={false}>
<ReindexingDocumentsStepTitle {...props} />
<MigrateDocumentsStepTitle {...props} />
</EuiFlexItem>
</EuiFlexGroup>
),
} as StepProgressStep;
const inProgress =
status === DataStreamReindexStatus.inProgress || status === DataStreamReindexStatus.completed;
status === DataStreamMigrationStatus.inProgress ||
status === DataStreamMigrationStatus.completed;
let euiProgressColor = 'subdued';
@ -55,21 +57,21 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
) {
reindexingDocsStep.status = 'inProgress';
euiProgressColor = 'subdued';
} else if (status === DataStreamReindexStatus.failed) {
} else if (status === DataStreamMigrationStatus.failed) {
reindexingDocsStep.status = 'failed';
euiProgressColor = 'danger';
} else if (
status === DataStreamReindexStatus.cancelled ||
status === DataStreamMigrationStatus.cancelled ||
cancelLoadingState === CancelLoadingState.Success
) {
reindexingDocsStep.status = 'cancelled';
} else if (status === undefined) {
reindexingDocsStep.status = 'incomplete';
euiProgressColor = 'subdued';
} else if (status === DataStreamReindexStatus.inProgress) {
} else if (status === DataStreamMigrationStatus.inProgress) {
reindexingDocsStep.status = 'inProgress';
euiProgressColor = 'primary';
} else if (status === DataStreamReindexStatus.completed) {
} else if (status === DataStreamMigrationStatus.completed) {
reindexingDocsStep.status = 'complete';
euiProgressColor = 'success';
} else {
@ -79,7 +81,7 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
}
const progressPercentage = inProgress
? getDataStreamReindexProgress(status, reindexTaskPercComplete)
? getDataStreamReindexProgress(status, taskPercComplete)
: undefined;
const showProgressValueText = inProgress;
const progressMaxValue = inProgress ? 100 : undefined;
@ -89,15 +91,17 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
<EuiFlexItem>
<EuiTitle size="xs" data-test-subj="reindexChecklistTitle">
<h3>
{status === DataStreamReindexStatus.inProgress ? (
{status === DataStreamMigrationStatus.inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingInProgressTitle"
defaultMessage="Reindexing in progress…"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingInProgressTitle"
defaultMessage="{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} in progress…"
values={{ resolutionType }}
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklistTitle"
defaultMessage="Reindex data stream"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklistTitle"
defaultMessage="{resolutionType, select, reindex {Reindex data stream} readonly {Mark data stream as read-only} other {Migrate data stream}}"
values={{ resolutionType }}
/>
)}
</h3>
@ -110,7 +114,7 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
label={
taskStatus ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingInProgressTitle"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingInProgressTitle"
defaultMessage="Started {startTimeFromNow}"
values={{
startTimeFromNow: (
@ -139,7 +143,7 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
{!taskStatus && (
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.fetchingStatus"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.fetchingStatus"
defaultMessage="Fetching Status…"
/>
</p>
@ -151,11 +155,11 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
<EuiText size="s" color="danger">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.failedTitle',
'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.failedTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} failed to reindex.',
values: { count: taskStatus.errorsCount },
'{count, plural, =1 {# Index} other {# Indices}} failed to get {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}.',
values: { count: taskStatus.errorsCount, resolutionType },
}
)}
</p>
@ -166,11 +170,11 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
<EuiText size="s" color="success">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.completeTitle',
'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.completeTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} successfully reindexed.',
values: { count: taskStatus.successCount },
'{count, plural, =1 {# Index} other {# Indices}} successfully {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}.',
values: { count: taskStatus.successCount, resolutionType },
}
)}
</p>
@ -180,11 +184,11 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
<EuiText size="s" color="primary">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.inProgressTitle',
'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.inProgressTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} currently reindexing.',
values: { count: taskStatus.inProgressCount },
'{count, plural, =1 {# Index} other {# Indices}} currently getting {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}.',
values: { count: taskStatus.inProgressCount, resolutionType },
}
)}
</p>
@ -194,7 +198,7 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
<EuiText size="s">
<p>
{i18n.translate(
'xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.progressStep.pendingTitle',
'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.progressStep.pendingTitle',
{
defaultMessage:
'{count, plural, =1 {# Index} other {# Indices}} waiting to start.',

View file

@ -9,18 +9,18 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { CancelLoadingState } from '../../../../../../types';
import { DataStreamReindexStatus } from '../../../../../../../../../common/types';
import type { ReindexState } from '../../../use_reindex_state';
import { DataStreamMigrationStatus } from '../../../../../../../../../common/types';
import type { MigrationState } from '../../../use_migration_state';
export const ReindexingDocumentsStepTitle: React.FunctionComponent<{
reindexState: ReindexState;
}> = ({ reindexState: { status, cancelLoadingState } }) => {
export const MigrateDocumentsStepTitle: React.FunctionComponent<{
migrationState: MigrationState;
}> = ({ migrationState: { status, cancelLoadingState, resolutionType } }) => {
switch (cancelLoadingState) {
case CancelLoadingState.Requested:
case CancelLoadingState.Loading: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel"
defaultMessage="Cancelling…"
/>
);
@ -28,7 +28,7 @@ export const ReindexingDocumentsStepTitle: React.FunctionComponent<{
case CancelLoadingState.Success: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancelledLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.cancelButton.cancelledLabel"
defaultMessage="Cancelled"
/>
);
@ -36,56 +36,61 @@ export const ReindexingDocumentsStepTitle: React.FunctionComponent<{
case CancelLoadingState.Error: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel"
defaultMessage="Failed to cancel reindexing"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel"
defaultMessage="Failed to cancel {resolutionType, select, reindex {reindexing} readonly {read-only} other {}}"
values={{ resolutionType }}
/>
);
}
}
switch (status) {
case DataStreamReindexStatus.inProgress: {
case DataStreamMigrationStatus.inProgress: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="Reindexing data stream"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {}}"
values={{ resolutionType }}
/>
);
}
case DataStreamReindexStatus.failed:
case DataStreamMigrationStatus.failed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.failed.reindexingDocumentsStepTitle"
defaultMessage="Reindexing failed"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.failed.reindexingDocumentsStepTitle"
defaultMessage="Failed to {resolutionType, select, reindex {reindex} readonly {mark as read-only} other {}}"
values={{ resolutionType }}
/>
);
case DataStreamReindexStatus.fetchFailed:
case DataStreamMigrationStatus.fetchFailed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.fetchFailed.reindexingDocumentsStepTitle"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.fetchFailed.reindexingDocumentsStepTitle"
defaultMessage="Fetching status failed"
/>
);
case DataStreamReindexStatus.cancelled:
case DataStreamMigrationStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.cancelled.reindexingDocumentsStepTitle"
defaultMessage="Reindexing cancelled"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.cancelled.reindexingDocumentsStepTitle"
defaultMessage="{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {}} cancelled"
values={{ resolutionType }}
/>
);
case DataStreamReindexStatus.completed:
case DataStreamMigrationStatus.completed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.completed.reindexingDocumentsStepTitle"
defaultMessage="Reindexing completed"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.completed.reindexingDocumentsStepTitle"
defaultMessage="{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {}} completed"
values={{ resolutionType }}
/>
);
case DataStreamReindexStatus.notStarted:
case DataStreamMigrationStatus.notStarted:
default: {
return (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="Reindex data stream"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="{resolutionType, select, reindex {Reindex data stream} readonly {Mark data stream as read-only} other {Unknown action}}"
/>
);
}

View file

@ -9,30 +9,37 @@ import React from 'react';
import { EuiFlyoutBody, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataStreamMetadata } from '../../../../../../../../../common/types';
import type {
DataStreamMetadata,
DataStreamResolutionType,
} from '../../../../../../../../../common/types';
interface Props {
meta: DataStreamMetadata;
meta?: DataStreamMetadata | null;
resolutionType?: DataStreamResolutionType;
}
export const ReindexingCompletedFlyoutStep: React.FunctionComponent<Props> = ({ meta }: Props) => {
export const MigrationCompletedFlyoutStep: React.FunctionComponent<Props> = ({
meta,
resolutionType,
}: Props) => {
return (
<>
<EuiFlyoutBody>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Data Stream Reindexing Complete"
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Data Stream Migration Complete"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Success! {count, plural, =1 {# backing index} other {# backing indices}} successfully reindexed."
values={{ count: meta.indicesRequiringUpgradeCount }}
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Success! {count, plural, =0 {backing indices} =1 {# backing index} other {# backing indices}} successfully {resolutionType, select, reindex {reindexed} readonly {marked as read-only} other {migrated}}."
values={{ count: meta?.indicesRequiringUpgradeCount || 0, resolutionType }}
/>
</p>
</EuiFlyoutBody>

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { ReindexingCompletedFlyoutStep } from './completed_step';
export { MigrationCompletedFlyoutStep } from './completed_step';

View file

@ -0,0 +1,54 @@
/*
* 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 from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut } from '@elastic/eui';
export const ReindexWarningCallout: React.FunctionComponent<{}> = () => {
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.reindex.calloutTitle"
defaultMessage="This operation requires destructive changes that cannot be reversed"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.reindex.calloutDetail"
defaultMessage="Ensure data has been backed up before continuing. To proceed with reindexing this data, confirm below."
/>
</p>
</EuiCallOut>
);
};
export const ReadonlyWarningCallout: React.FunctionComponent<{}> = () => {
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.readonly.calloutTitle"
defaultMessage="Marking this data read-only could affect some of the existing setups"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.readonly.calloutDetail"
defaultMessage="Make sure you have backed up your data, etc. You can always re-index this data later to make it editable."
/>
</p>
</EuiCallOut>
);
};

View file

@ -10,59 +10,60 @@ import React, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
DataStreamReindexWarning,
DataStreamReindexWarningTypes,
DataStreamMigrationWarning,
DataStreamWarningTypes,
DataStreamMetadata,
DataStreamResolutionType,
} from '../../../../../../../../../common/types';
import { useAppContext } from '../../../../../../../app_context';
import {
IncompatibleDataInDataStreamWarningCheckbox,
WarningCheckboxProps,
} from './warning_step_checkbox';
AffectExistingSetupsWarningCheckbox,
} from './warnings';
import { ReindexWarningCallout, ReadonlyWarningCallout } from './callouts';
interface CheckedIds {
[id: string]: boolean;
}
const warningToComponentMap: Record<
DataStreamReindexWarningTypes,
DataStreamWarningTypes,
React.FunctionComponent<WarningCheckboxProps>
> = {
incompatibleDataStream: IncompatibleDataInDataStreamWarningCheckbox,
affectExistingSetups: AffectExistingSetupsWarningCheckbox,
};
export const idForWarning = (id: number) => `reindexWarning-${id}`;
interface WarningsConfirmationFlyoutProps {
hideWarningsStep: () => void;
continueReindex: () => void;
warnings: DataStreamReindexWarning[];
meta: DataStreamMetadata;
}
export const idForWarning = (id: number) => `migrationWarning-${id}`;
/**
* Displays warning text about destructive changes required to reindex this index. The user
* Displays warning text about changes required to migrate this data stream. The user
* must acknowledge each change before being allowed to proceed.
*/
export const ConfirmReindexingFlyoutStep: React.FunctionComponent<
WarningsConfirmationFlyoutProps
> = ({ warnings, hideWarningsStep, continueReindex, meta }) => {
export const ConfirmMigrationFlyoutStep: React.FunctionComponent<{
hideWarningsStep: () => void;
startAction: () => void;
resolutionType: DataStreamResolutionType;
warnings: DataStreamMigrationWarning[];
meta: DataStreamMetadata;
}> = ({ warnings, hideWarningsStep, startAction, resolutionType, meta }) => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
const { links } = docLinks;
const [checkedIds, setCheckedIds] = useState<CheckedIds>(
warnings.reduce((initialCheckedIds, warning, index) => {
initialCheckedIds[idForWarning(index)] = false;
@ -84,36 +85,75 @@ export const ConfirmReindexingFlyoutStep: React.FunctionComponent<
}));
};
const startActionButtonLabel =
resolutionType === 'reindex'
? i18n.translate(
'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.startActionButtonLabel',
{
defaultMessage: 'Start reindexing',
}
)
: i18n.translate(
'xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.startActionButtonLabel',
{
defaultMessage: 'Mark all read-only',
}
);
const actionClarification =
resolutionType === 'reindex' ? (
<>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.flyout.warningsStep.reindex.acceptChangesTitle"
defaultMessage="{count, plural, =1 {# backing index} other {# backing indices}}, including current write index, will be re-indexed. Current write index will be rolled over first."
values={{
count: meta.indicesRequiringUpgradeCount,
}}
/>
</p>
<EuiSpacer size="s" />
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.flyout.warningsStep.reindex.acceptChangesTitle"
defaultMessage="You can increase the speed of reindexing by changing throttling configuration on ES. Where changing throttling configuration allows you to utilize more resources to speed up the reindexing process. {learnMoreHtml}"
values={{
learnMoreHtml: (
<EuiLink
href={`${links.elasticsearch.docsBase}data-stream-reindex-api.html#reindex-data-stream-api-settings`}
target="_blank"
>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.learnMoreLink"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</p>
</>
) : (
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.flyout.warningsStep.readonly.acceptChangesTitle"
defaultMessage="{count, plural, =1 {# backing index} other {# backing indices}}, including current write index, will be marked as read-only."
values={{
count: meta.indicesRequiringUpgradeCount,
}}
/>
</p>
);
return (
<>
<EuiFlyoutBody>
{warnings.length > 0 && (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle"
defaultMessage="This operation requires destructive changes that cannot be reversed"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail"
defaultMessage="Ensure data has been backed up before continuing. To proceed with reindexing this data, confirm below."
/>
</p>
</EuiCallOut>
{resolutionType === 'reindex' && <ReindexWarningCallout />}
{resolutionType === 'readonly' && <ReadonlyWarningCallout />}
<EuiSpacer />
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.dataStreamReindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="{count, plural, =1 {# backing index} other {# backing indices}}, including current write index, will be re-indexed. Current write index will be rolled over first."
values={{ count: meta.indicesRequiringUpgradeCount }}
/>
</p>
{actionClarification}
<EuiSpacer size="m" />
{warnings.map((warning, index) => {
const WarningCheckbox = warningToComponentMap[warning.warningType];
@ -137,17 +177,14 @@ export const ConfirmReindexingFlyoutStep: React.FunctionComponent<
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="arrowLeft" onClick={hideWarningsStep} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.backButtonLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.backButtonLabel"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill color="primary" onClick={continueReindex} disabled={blockAdvance}>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.checklistStep.startReindexingButtonLabel"
defaultMessage="Start reindexing"
/>
<EuiButton fill color="primary" onClick={startAction} disabled={blockAdvance}>
{startActionButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { ConfirmReindexingFlyoutStep } from './confirm_step';
export { ConfirmMigrationFlyoutStep } from './confirm_step';

View file

@ -0,0 +1,31 @@
/*
* 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 from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { WarningCheckbox, WarningCheckboxProps } from './warning_step_checkbox';
export const AffectExistingSetupsWarningCheckbox: React.FunctionComponent<WarningCheckboxProps> = ({
isChecked,
onChange,
id,
}) => {
return (
<WarningCheckbox
isChecked={isChecked}
onChange={onChange}
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.affectExistingSetupsWarningTitle"
defaultMessage="Mark as read-only all incompatible data for this data stream"
/>
}
description={null}
/>
);
};

View file

@ -0,0 +1,29 @@
/*
* 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 from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { WarningCheckbox, WarningCheckboxProps } from './warning_step_checkbox';
export const IncompatibleDataInDataStreamWarningCheckbox: React.FunctionComponent<
WarningCheckboxProps
> = ({ isChecked, onChange, id }) => {
return (
<WarningCheckbox
isChecked={isChecked}
onChange={onChange}
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.incompatibleDataWarningTitle"
defaultMessage="Reindex all incompatible data for this data stream"
/>
}
description={null}
/>
);
};

View file

@ -0,0 +1,12 @@
/*
* 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 { IncompatibleDataInDataStreamWarningCheckbox } from './incompatible_data';
export { AffectExistingSetupsWarningCheckbox } from './existing_setups';
export type { WarningCheckboxProps } from './warning_step_checkbox';

View file

@ -18,19 +18,8 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLinksStart } from '@kbn/core/public';
import {
DataStreamReindexWarning,
DataStreamReindexWarningTypes,
} from '../../../../../../../../../common/types';
export const hasReindexWarning = (
warnings: DataStreamReindexWarning[],
warningType: DataStreamReindexWarningTypes
): boolean => {
return Boolean(warnings.find((warning) => warning.warningType === warningType));
};
const WarningCheckbox: React.FunctionComponent<{
export const WarningCheckbox: React.FunctionComponent<{
isChecked: boolean;
warningId: string;
label: React.ReactNode;
@ -55,7 +44,7 @@ const WarningCheckbox: React.FunctionComponent<{
<EuiIconTip
content={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.documentationLinkLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.documentationLinkLabel"
defaultMessage="Documentation"
/>
}
@ -82,22 +71,3 @@ export interface WarningCheckboxProps {
docLinks: DocLinksStart['links'];
id: string;
}
export const IncompatibleDataInDataStreamWarningCheckbox: React.FunctionComponent<
WarningCheckboxProps
> = ({ isChecked, onChange, id }) => {
return (
<WarningCheckbox
isChecked={isChecked}
onChange={onChange}
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.incompatibleDataWarningTitle"
defaultMessage="Reindex all incompatible data for this data stream"
/>
}
description={null}
/>
);
};

View file

@ -23,13 +23,14 @@ import { FormattedMessage } from '@kbn/i18n-react';
import {
DataStreamMetadata,
DataStreamReindexStatus,
DataStreamMigrationStatus,
DataStreamResolutionType,
DataStreamsAction,
} from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex_state';
import type { MigrationState } from '../../../use_migration_state';
import { useAppContext } from '../../../../../../../app_context';
import { DurationClarificationCallOut } from './warnings_callout';
import { getPrimaryButtonLabel } from '../../messages';
/**
* Displays a flyout that shows the current reindexing status for a given index.
@ -37,11 +38,19 @@ import { getPrimaryButtonLabel } from '../../messages';
export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
closeFlyout: () => void;
reindexState: ReindexState;
startReindex: () => void;
migrationState: MigrationState;
correctiveAction: DataStreamsAction;
initAction: (resolutionType: DataStreamResolutionType) => void;
lastIndexCreationDateFormatted: string;
meta: DataStreamMetadata;
}> = ({ closeFlyout, reindexState, startReindex, lastIndexCreationDateFormatted, meta }) => {
}> = ({
closeFlyout,
migrationState,
initAction,
lastIndexCreationDateFormatted,
correctiveAction,
meta,
}) => {
const {
services: {
api,
@ -49,12 +58,15 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
},
} = useAppContext();
const { loadingState, status, hasRequiredPrivileges } = reindexState;
const { loadingState, status, hasRequiredPrivileges } = migrationState;
const loading =
loadingState === LoadingState.Loading || status === DataStreamReindexStatus.inProgress;
const isCompleted = status === DataStreamReindexStatus.completed;
const hasFetchFailed = status === DataStreamReindexStatus.fetchFailed;
const hasReindexingFailed = status === DataStreamReindexStatus.failed;
loadingState === LoadingState.Loading || status === DataStreamMigrationStatus.inProgress;
const isCompleted = status === DataStreamMigrationStatus.completed;
const hasFetchFailed = status === DataStreamMigrationStatus.fetchFailed;
const hasMigrationFailed = status === DataStreamMigrationStatus.failed;
const readOnlyExcluded = correctiveAction.metadata.excludedActions?.includes('readOnly');
const reindexExcluded = correctiveAction.metadata.excludedActions?.includes('reindex');
const { data: nodes } = api.useLoadNodeDiskSpace();
@ -73,8 +85,8 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this data stream."
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to migrate this data stream."
/>
}
color="danger"
@ -91,15 +103,15 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.lowDiskSpaceCalloutTitle"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent migration. The following nodes are impacted:"
/>
<EuiSpacer size="s" />
@ -108,7 +120,7 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.lowDiskSpaceUsedText"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
@ -124,27 +136,27 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
</>
)}
{(hasFetchFailed || hasReindexingFailed) && (
{(hasFetchFailed || hasMigrationFailed) && (
<>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'reindexingFailedCallout'}
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'migrationFailedCallout'}
title={
hasFetchFailed ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.fetchFailedCalloutTitle"
defaultMessage="Data stream reindex status not available"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.fetchFailedCalloutTitle"
defaultMessage="Data stream migration status not available"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexingFailedCalloutTitle"
defaultMessage="Data stream reindexing error"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.migrationFailedCalloutTitle"
defaultMessage="Data stream migration error"
/>
)
}
>
{reindexState.errorMessage}
{migrationState.errorMessage}
</EuiCallOut>
<EuiSpacer />
</>
@ -153,7 +165,7 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
<EuiText>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.notCompatibleIndicesText"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.notCompatibleIndicesText"
defaultMessage="You have {backingIndicesCount} backing indices on this data stream that were created in ES 7.x and will not be compatible with next version."
values={{
backingIndicesCount: meta.indicesRequiringUpgradeCount,
@ -162,7 +174,7 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.requiredUpgradeText"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.requiredUpgradeText"
defaultMessage="{allBackingIndices} total backing indices, and {backingIndicesRequireingUpgrade} requires upgrade."
values={{
allBackingIndices: meta.allIndicesCount,
@ -171,33 +183,38 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
/>
</p>
<ul>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.readOnlyText"
tagName="li"
defaultMessage="If you do not need to update historical data, mark as read-only. You can reindex post-upgrade if updates are needed."
/>
<li>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexOptionListTitle"
defaultMessage="Reindex"
/>
<ul>
{!readOnlyExcluded && (
<li>
<FormattedMessage
tagName="li"
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexOption.rolledOverIndex"
defaultMessage="The current write index will be rolled over and reindexed."
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.readOnlyText"
defaultMessage="If you do not need to update historical data, mark as read-only. You can reindex post-upgrade if updates are needed."
/>
</li>
)}
{!reindexExcluded && (
<li>
<FormattedMessage
tagName="li"
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexOption.additionalIndices"
defaultMessage="Additional backing indices will be reindexed and remain editable."
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.reindexOptionListTitle"
defaultMessage="Reindex"
/>
</ul>
</li>
<ul>
<FormattedMessage
tagName="li"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.reindexOption.rolledOverIndex"
defaultMessage="The current write index will be rolled over and reindexed."
/>
<FormattedMessage
tagName="li"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.reindexOption.additionalIndices"
defaultMessage="Additional backing indices will be reindexed and remain editable."
/>
</ul>
</li>
)}
</ul>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.reindexDescription"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.migrationDescription"
defaultMessage="If you no longer need this data, you can also proceed by deleting these indices. {indexManagementLinkHtml}"
values={{
indexManagementLinkHtml: (
@ -207,7 +224,7 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
)}`}
>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.indexMgmtLink"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.indexMgmtLink"
defaultMessage="Go to index management"
/>
</EuiLink>
@ -223,24 +240,49 @@ export const DataStreamDetailsFlyoutStep: React.FunctionComponent<{
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.detailsStep.closeButtonLabel"
id="xpack.upgradeAssistant.dataStream.migration.flyout.detailsStep.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{!hasFetchFailed && !isCompleted && hasRequiredPrivileges && (
{!hasFetchFailed && !isCompleted && hasRequiredPrivileges && !reindexExcluded && (
<EuiFlexItem grow={false}>
<EuiButton
color={status === DataStreamReindexStatus.cancelled ? 'warning' : 'primary'}
iconType={status === DataStreamReindexStatus.cancelled ? 'play' : undefined}
onClick={startReindex}
color={
status === DataStreamMigrationStatus.cancelled
? 'warning'
: readOnlyExcluded
? 'primary'
: 'accent'
}
iconType={status === DataStreamMigrationStatus.cancelled ? 'play' : undefined}
onClick={() => initAction('reindex')}
isLoading={loading}
disabled={loading || !hasRequiredPrivileges}
data-test-subj="startReindexingButton"
disabled={loading || !hasRequiredPrivileges || reindexExcluded}
data-test-subj="startDataStreamReindexingButton"
>
{getPrimaryButtonLabel(status)}
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.reindexButton.initReindexButtonLabel"
defaultMessage="Reindex"
/>
</EuiButton>
</EuiFlexItem>
)}
{!readOnlyExcluded && (
<EuiFlexItem grow={false}>
<EuiButton
fill
color={'primary'}
onClick={() => initAction('readonly')}
disabled={!hasRequiredPrivileges || readOnlyExcluded}
data-test-subj="startDataStreamReadonlyButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.checklistStep.initMarkReadOnlyButtonLabel"
defaultMessage="Mark read-only"
/>
</EuiButton>
</EuiFlexItem>
)}

View file

@ -22,20 +22,26 @@ export const DurationClarificationCallOut: React.FunctionComponent<Props> = ({
<EuiCallOut color="primary">
<p>
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.indicesNeedReindexing"
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.indicesNeedReindexing"
defaultMessage="Indices created on or before {formattedDate} need to be reindexed to a compatible format or marked as read-only."
values={{ formattedDate }}
/>
<br />
<br />
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.suggestReadOnly"
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.backingIndicesUnfrozen"
defaultMessage="If any of the backing indices of the data stream are frozen, they will be converted to non-frozen indices during the update process."
/>
<br />
<br />
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.suggestReadOnly"
defaultMessage="Depending on size and resources, reindexing may take extended time and your data will be in a read-only state until the job has completed. {learnMoreHtml}"
values={{
learnMoreHtml: (
<EuiLink href={learnMoreUrl} target="_blank">
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.warningsStep.learnMoreLink"
id="xpack.upgradeAssistant.dataStream.migration.flyout.warningsStep.learnMoreLink"
defaultMessage="Learn more"
/>
</EuiLink>

View file

@ -44,12 +44,12 @@ export const InitializingFlyoutStep: React.FunctionComponent<InitializingFlyoutS
<EuiTitle size="s">
{hasInitializingError ? (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.initializingStep.errorLoadingDataStreamInfo"
id="xpack.upgradeAssistant.dataStream.migration.flyout.initializingStep.errorLoadingDataStreamInfo"
defaultMessage="Error loading data stream info"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.dataStream.reindexing.flyout.initializingStep.loadingDataStreamInfo"
id="xpack.upgradeAssistant.dataStream.migration.flyout.initializingStep.loadingDataStreamInfo"
defaultMessage="Loading Data stream info"
/>
)}

View file

@ -0,0 +1,106 @@
/*
* 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 { chunk } from 'lodash';
import {
DataStreamMetadata,
DataStreamMigrationStatus,
DataStreamMigrationOperation,
} from '../../../../../../common/types';
import { ApiService } from '../../../../lib/api';
interface ReadOnlyExecuteResponse {
migrationOp: DataStreamMigrationOperation;
}
const DEFAULT_BATCH_SIZE = 10;
export async function* readOnlyExecute(
dataStreamName: string,
meta: DataStreamMetadata | null,
api: ApiService,
batchSize: number = DEFAULT_BATCH_SIZE
): AsyncGenerator<ReadOnlyExecuteResponse, ReadOnlyExecuteResponse, ReadOnlyExecuteResponse> {
const { indicesRequiringUpgrade } = meta || {};
const startTimeMs = +Date.now();
if (!indicesRequiringUpgrade || !indicesRequiringUpgrade.length) {
return {
migrationOp: {
status: DataStreamMigrationStatus.completed,
resolutionType: 'readonly',
taskPercComplete: 1,
progressDetails: {
startTimeMs,
successCount: 0,
pendingCount: 0,
inProgressCount: 0,
errorsCount: 0,
},
},
};
}
let processedCount = 0;
const batches = chunk(indicesRequiringUpgrade, batchSize);
try {
for (const batch of batches) {
const { error } = await api.markIndicesAsReadOnly(dataStreamName, batch);
if (error) {
throw error;
}
processedCount += batch.length;
const status =
processedCount >= indicesRequiringUpgrade.length
? DataStreamMigrationStatus.completed
: DataStreamMigrationStatus.inProgress;
const taskPercComplete = processedCount / indicesRequiringUpgrade.length;
yield {
migrationOp: {
resolutionType: 'readonly',
status,
taskPercComplete,
progressDetails: {
startTimeMs,
successCount: processedCount,
pendingCount: indicesRequiringUpgrade.length - processedCount,
inProgressCount: batch.length,
errorsCount: 0,
},
},
};
}
} catch (error) {
return {
migrationOp: {
resolutionType: 'readonly',
status: DataStreamMigrationStatus.failed,
errorMessage: error.message || 'Unknown error occurred',
},
};
}
return {
migrationOp: {
resolutionType: 'readonly',
status: DataStreamMigrationStatus.completed,
taskPercComplete: 1,
progressDetails: {
startTimeMs,
successCount: indicesRequiringUpgrade.length,
pendingCount: 0,
inProgressCount: 0,
errorsCount: 0,
},
},
};
}

View file

@ -16,81 +16,146 @@ import {
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import { DataStreamReindexStatus } from '../../../../../../common/types';
import {
DataStreamMigrationStatus,
DataStreamResolutionType,
DataStreamsAction,
} from '../../../../../../common/types';
import { getDataStreamReindexProgressLabel } from '../../../../lib/utils';
import { LoadingState } from '../../../types';
import { useDataStreamReindexContext } from './context';
import { useDataStreamMigrationContext } from './context';
const i18nTexts = {
reindexLoadingStatusText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.reindexLoadingStatusText',
{
defaultMessage: 'Loading status…',
}
),
reindexInProgressText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.reindexInProgressText',
{
defaultMessage: 'Reindexing in progress…',
}
),
reindexCompleteText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.reindexCompleteText',
{
defaultMessage: 'Reindex complete',
}
),
reindexFailedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.reindexFailedText',
{
defaultMessage: 'Reindex failed',
}
),
reindexFetchFailedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.reindexFetchFailedText',
{
defaultMessage: 'Reindex status not available',
}
),
reindexCanceledText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.reindexCanceledText',
{
defaultMessage: 'Reindex cancelled',
}
),
resolutionText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLabel',
{
defaultMessage: 'Reindex',
}
),
resolutionTooltipLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipLabel',
{
defaultMessage:
'Resolve this issue by reindexing this data stream. This issue can be resolved automatically.',
}
),
const getI18nTexts = (
resolutionType?: DataStreamResolutionType,
excludedActions: Array<'readOnly' | 'reindex'> = []
) => {
const resolutionAction = excludedActions.includes('readOnly')
? 'reindex'
: excludedActions.includes('reindex')
? 'readOnly'
: 'readOnlyOrReindex';
const resolutionTexts = {
readOnlyOrReindex: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionReadOnlyOrReindexLabel',
{
defaultMessage: 'Mark as read-only, or reindex',
}
),
readOnly: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionReadOnlyLabel',
{
defaultMessage: 'Mark as read-only',
}
),
reindex: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionReindexLabel',
{
defaultMessage: 'Reindex',
}
),
};
const resolutionTooltipLabels = {
readOnlyOrReindex: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipReadOnlyOrReindexLabel',
{
defaultMessage:
'Resolve this issue by reindexing this data stream or marking its indices as read-only. This issue can be resolved automatically.',
}
),
readOnly: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipReadOnlyLabel',
{
defaultMessage:
'Resolve this issue by marking its indices as read-only. This issue can be resolved automatically.',
}
),
reindex: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionTooltipReindexLabel',
{
defaultMessage:
'Resolve this issue by reindexing this data stream. This issue can be resolved automatically.',
}
),
};
return {
loadingStatusText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionLoadingStatusText',
{
defaultMessage: 'Loading status…',
}
),
resolutionInProgressText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionInProgressText',
{
defaultMessage:
'{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} in progress…',
values: { resolutionType },
}
),
resolutionCompleteText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionCompleteText',
{
defaultMessage:
'{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} complete',
values: { resolutionType },
}
),
resolutionFailedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resulutionFailedText',
{
defaultMessage:
'{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} failed',
values: { resolutionType },
}
),
resolutionFetchFailedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionFetchFailedText',
{
defaultMessage:
'{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} status not available',
values: { resolutionType },
}
),
reindexCanceledText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.dataStream.resolutionCanceledText',
{
defaultMessage:
'{resolutionType, select, reindex {Reindexing} readonly {Marking as read-only} other {Migration}} cancelled',
values: { resolutionType },
}
),
resolutionText: resolutionTexts[resolutionAction],
resolutionTooltipLabel: resolutionTooltipLabels[resolutionAction],
};
};
export const DataStreamReindexResolutionCell: React.FunctionComponent = () => {
const { reindexState } = useDataStreamReindexContext();
export const DataStreamReindexResolutionCell: React.FunctionComponent<{
correctiveAction: DataStreamsAction;
}> = ({ correctiveAction }) => {
const { migrationState } = useDataStreamMigrationContext();
const i18nTexts = getI18nTexts(
migrationState.resolutionType,
correctiveAction.metadata.excludedActions
);
if (reindexState.loadingState === LoadingState.Loading) {
if (migrationState.loadingState === LoadingState.Loading) {
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexLoadingStatusText}</EuiText>
<EuiText size="s">{i18nTexts.loadingStatusText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
switch (reindexState.status) {
case DataStreamReindexStatus.inProgress:
switch (migrationState.status) {
case DataStreamMigrationStatus.inProgress:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
@ -98,45 +163,45 @@ export const DataStreamReindexResolutionCell: React.FunctionComponent = () => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
{i18nTexts.reindexInProgressText}{' '}
{i18nTexts.resolutionInProgressText}{' '}
{getDataStreamReindexProgressLabel(
reindexState.status,
reindexState.reindexTaskPercComplete
migrationState.status,
migrationState.taskPercComplete
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case DataStreamReindexStatus.completed:
case DataStreamMigrationStatus.completed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="check" color="success" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexCompleteText}</EuiText>
<EuiText size="s">{i18nTexts.resolutionCompleteText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case DataStreamReindexStatus.failed:
case DataStreamMigrationStatus.failed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="warning" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexFailedText}</EuiText>
<EuiText size="s">{i18nTexts.resolutionFailedText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case DataStreamReindexStatus.fetchFailed:
case DataStreamMigrationStatus.fetchFailed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="warning" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexFetchFailedText}</EuiText>
<EuiText size="s">{i18nTexts.resolutionFetchFailedText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -8,7 +8,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { EuiTableRowCell } from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { EnrichedDeprecationInfo } from '../../../../../../common/types';
import { DataStreamsAction, EnrichedDeprecationInfo } from '../../../../../../common/types';
import { GlobalFlyout } from '../../../../../shared_imports';
import { useAppContext } from '../../../../app_context';
import {
@ -20,7 +20,7 @@ import { DeprecationTableColumns } from '../../../types';
import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells';
import { DataStreamReindexResolutionCell } from './resolution_table_cell';
import { DataStreamReindexFlyout } from './flyout';
import { DataStreamReindexStatusProvider, useDataStreamReindexContext } from './context';
import { DataStreamMigrationStatusProvider, useDataStreamMigrationContext } from './context';
const { useGlobalFlyout } = GlobalFlyout;
@ -34,7 +34,7 @@ const DataStreamTableRowCells: React.FunctionComponent<TableRowProps> = ({
deprecation,
}) => {
const [showFlyout, setShowFlyout] = useState(false);
const dataStreamContext = useDataStreamReindexContext();
const dataStreamContext = useDataStreamMigrationContext();
const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } =
useGlobalFlyout();
@ -83,7 +83,11 @@ const DataStreamTableRowCells: React.FunctionComponent<TableRowProps> = ({
fieldName={field}
openFlyout={() => setShowFlyout(true)}
deprecation={deprecation}
resolutionTableCell={<DataStreamReindexResolutionCell />}
resolutionTableCell={
<DataStreamReindexResolutionCell
correctiveAction={deprecation.correctiveAction as DataStreamsAction}
/>
}
/>
</EuiTableRowCell>
);
@ -98,8 +102,8 @@ export const DataStreamTableRow: React.FunctionComponent<TableRowProps> = (props
} = useAppContext();
return (
<DataStreamReindexStatusProvider dataStreamName={props.deprecation.index!} api={api}>
<DataStreamMigrationStatusProvider dataStreamName={props.deprecation.index!} api={api}>
<DataStreamTableRowCells {...props} />
</DataStreamReindexStatusProvider>
</DataStreamMigrationStatusProvider>
);
};

View file

@ -0,0 +1,363 @@
/*
* 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 { useRef, useCallback, useState, useEffect } from 'react';
import {
DataStreamMigrationStatus,
DataStreamMigrationWarning,
DataStreamMetadata,
DataStreamReindexStatusResponse,
DataStreamProgressDetails,
DataStreamResolutionType,
ResponseError,
} from '../../../../../../common/types';
import { CancelLoadingState, LoadingState } from '../../../types';
import { ApiService } from '../../../../lib/api';
import { readOnlyExecute } from './readonly_state';
const POLL_INTERVAL = 1000;
export interface MigrationState {
loadingState: LoadingState;
cancelLoadingState?: CancelLoadingState;
resolutionType?: DataStreamResolutionType;
status?: DataStreamMigrationStatus;
taskPercComplete: number | null;
errorMessage: string | null;
migrationWarnings?: DataStreamMigrationWarning[];
hasRequiredPrivileges?: boolean;
taskStatus?: DataStreamProgressDetails;
meta: DataStreamMetadata | null;
}
const getMigrationState = (
migrationState: MigrationState,
{
migrationOp,
warnings,
hasRequiredPrivileges,
meta: updatedMeta,
}: DataStreamReindexStatusResponse & { meta?: DataStreamMetadata | null }
) => {
const newMigrationState: MigrationState = {
...migrationState,
// @ts-expect-error - resolutionType does non exist in all migration states.
resolutionType: migrationOp?.resolutionType || migrationState.resolutionType,
meta: updatedMeta || migrationState.meta,
loadingState: LoadingState.Success,
};
if (warnings) {
newMigrationState.migrationWarnings = warnings;
}
if (hasRequiredPrivileges !== undefined) {
newMigrationState.hasRequiredPrivileges = hasRequiredPrivileges;
}
if (migrationOp) {
newMigrationState.status = migrationOp.status;
if (migrationOp.status === DataStreamMigrationStatus.notStarted) {
return newMigrationState;
}
if (migrationOp.status === DataStreamMigrationStatus.failed) {
newMigrationState.errorMessage = migrationOp.errorMessage;
return newMigrationState;
}
if (
migrationOp.status === DataStreamMigrationStatus.inProgress ||
migrationOp.status === DataStreamMigrationStatus.completed
) {
newMigrationState.taskStatus = migrationOp.progressDetails;
newMigrationState.taskPercComplete = migrationOp.taskPercComplete;
}
if (
migrationState.cancelLoadingState === CancelLoadingState.Requested &&
migrationOp.status === DataStreamMigrationStatus.inProgress
) {
newMigrationState.cancelLoadingState = CancelLoadingState.Loading;
}
}
return newMigrationState;
};
export const useMigrationStatus = ({
dataStreamName,
api,
}: {
dataStreamName: string;
api: ApiService;
}) => {
const [migrationState, setMigrationState] = useState<MigrationState>({
loadingState: LoadingState.Loading,
errorMessage: null,
taskPercComplete: null,
taskStatus: undefined,
meta: null,
});
const pollIntervalIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const readonlyState = useRef<ReturnType<typeof readOnlyExecute> | null>(null);
const isMounted = useRef(false);
const clearPollInterval = useCallback(() => {
if (pollIntervalIdRef.current) {
clearTimeout(pollIntervalIdRef.current);
pollIntervalIdRef.current = null;
}
}, []);
const pollingFunction = useCallback(
async (resolutionType?: DataStreamResolutionType) => {
clearPollInterval();
try {
if (resolutionType === 'readonly' && !readonlyState.current) {
return;
}
let data: DataStreamReindexStatusResponse | null = null;
let error: ResponseError | null = null;
if (resolutionType === 'readonly') {
if (!readonlyState.current) {
throw new Error('Readonly state not initialized');
}
const { value } = await readonlyState.current.next();
data = value;
} else {
const results = await api.getDataStreamMigrationStatus(dataStreamName);
data = results.data;
error = results.error;
}
if (error) {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error!.message.toString(),
status: DataStreamMigrationStatus.fetchFailed,
};
});
return;
}
if (!data) {
return;
}
setMigrationState((prevValue: MigrationState) => {
return getMigrationState(prevValue, data!);
});
if (data.migrationOp && data.migrationOp.status === DataStreamMigrationStatus.inProgress) {
// Only keep polling if it exists and is in progress.
pollIntervalIdRef.current = setTimeout(
() => pollingFunction(migrationState.resolutionType),
POLL_INTERVAL
);
}
} catch (error) {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamMigrationStatus.fetchFailed,
};
});
}
},
[clearPollInterval, api, dataStreamName, migrationState.resolutionType]
);
const updateStatus = useCallback(async () => {
return pollingFunction(migrationState.resolutionType);
}, [pollingFunction, migrationState.resolutionType]);
const startReindex = useCallback(async () => {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
status: DataStreamMigrationStatus.inProgress,
taskPercComplete: null,
errorMessage: null,
cancelLoadingState: undefined,
};
});
if (migrationState.status === DataStreamMigrationStatus.failed) {
try {
await api.cancelDataStreamReindexTask(dataStreamName);
} catch (_) {
// if the task has already failed, attempt to cancel the task
// before attempting to start the reindexing again.
}
}
const { data: migrationOp, error } = await api.startDataStreamReindexTask(dataStreamName);
if (error) {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamMigrationStatus.failed,
};
});
return;
}
setMigrationState((prevValue: MigrationState) => {
return getMigrationState(prevValue, { migrationOp, meta: prevValue.meta });
});
updateStatus();
}, [api, dataStreamName, updateStatus, migrationState.status]);
const loadDataStreamMetadata = useCallback(async () => {
try {
const { data, error } = await api.getDataStreamMetadata(dataStreamName);
if (error) {
throw error;
}
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
loadingState: LoadingState.Success,
meta: data || null,
};
});
} catch (error) {
setMigrationState((prevValue: MigrationState) => {
// if state is completed, we don't need to update the meta
if (prevValue.status === DataStreamMigrationStatus.completed) {
return prevValue;
}
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamMigrationStatus.failed,
};
});
}
}, [api, dataStreamName]);
const cancelReindex = useCallback(async () => {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
cancelLoadingState: CancelLoadingState.Requested,
};
});
try {
const { error } = await api.cancelDataStreamReindexTask(dataStreamName);
if (error) {
throw error;
}
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
cancelLoadingState: CancelLoadingState.Success,
status: DataStreamMigrationStatus.cancelled,
};
});
} catch (error) {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
cancelLoadingState: CancelLoadingState.Error,
};
});
}
}, [api, dataStreamName]);
const startReadonly = useCallback(async () => {
/**
* Here we jsut mark the status as in progress for the polling function
* to start executing the reindexing.
*/
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
resolutionType: 'readonly',
status: DataStreamMigrationStatus.inProgress,
taskPercComplete: null,
};
});
readonlyState.current = readOnlyExecute(dataStreamName, migrationState.meta, api);
pollingFunction('readonly');
}, [api, dataStreamName, migrationState, pollingFunction]);
const cancelReadonly = useCallback(async () => {
readonlyState.current = null;
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
resolutionType: undefined,
cancelLoadingState: CancelLoadingState.Success,
status: DataStreamMigrationStatus.cancelled,
};
});
}, []);
const initMigration = useCallback((resolutionType: DataStreamResolutionType) => {
setMigrationState((prevValue: MigrationState) => {
return {
...prevValue,
resolutionType,
status: DataStreamMigrationStatus.notStarted,
};
});
}, []);
useEffect(() => {
updateStatus();
}, [updateStatus]);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
// Clean up on unmount.
clearPollInterval();
};
}, [clearPollInterval]);
return {
migrationState,
loadDataStreamMetadata,
initMigration,
updateStatus,
startReindex,
cancelReindex,
startReadonly,
cancelReadonly,
};
};

View file

@ -1,284 +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 { useRef, useCallback, useState, useEffect } from 'react';
import {
DataStreamReindexStatus,
DataStreamReindexWarning,
DataStreamMetadata,
DataStreamReindexStatusResponse,
DataStreamProgressDetails,
} from '../../../../../../common/types';
import { CancelLoadingState, LoadingState } from '../../../types';
import { ApiService } from '../../../../lib/api';
const POLL_INTERVAL = 1000;
export interface ReindexState {
loadingState: LoadingState;
cancelLoadingState?: CancelLoadingState;
status?: DataStreamReindexStatus;
reindexTaskPercComplete: number | null;
errorMessage: string | null;
reindexWarnings?: DataStreamReindexWarning[];
hasRequiredPrivileges?: boolean;
taskStatus?: DataStreamProgressDetails;
meta: DataStreamMetadata | null;
}
const getReindexState = (
reindexState: ReindexState,
{
reindexOp,
warnings,
hasRequiredPrivileges,
meta: updatedMeta,
}: DataStreamReindexStatusResponse & { meta?: DataStreamMetadata | null }
) => {
const newReindexState: ReindexState = {
...reindexState,
reindexWarnings: warnings,
meta: updatedMeta || reindexState.meta,
loadingState: LoadingState.Success,
};
if (warnings) {
newReindexState.reindexWarnings = warnings;
}
if (hasRequiredPrivileges !== undefined) {
newReindexState.hasRequiredPrivileges = hasRequiredPrivileges;
}
if (reindexOp) {
newReindexState.status = reindexOp.status;
if (reindexOp.status === DataStreamReindexStatus.notStarted) {
return newReindexState;
}
if (reindexOp.status === DataStreamReindexStatus.failed) {
newReindexState.errorMessage = reindexOp.errorMessage;
return newReindexState;
}
if (
reindexOp.status === DataStreamReindexStatus.inProgress ||
reindexOp.status === DataStreamReindexStatus.completed
) {
newReindexState.taskStatus = reindexOp.progressDetails;
newReindexState.reindexTaskPercComplete = reindexOp.reindexTaskPercComplete;
}
if (
reindexState.cancelLoadingState === CancelLoadingState.Requested &&
reindexOp.status === DataStreamReindexStatus.inProgress
) {
newReindexState.cancelLoadingState = CancelLoadingState.Loading;
}
}
return newReindexState;
};
export const useReindexStatus = ({
dataStreamName,
api,
}: {
dataStreamName: string;
api: ApiService;
}) => {
const [reindexState, setReindexState] = useState<ReindexState>({
loadingState: LoadingState.Loading,
errorMessage: null,
reindexTaskPercComplete: null,
taskStatus: undefined,
meta: null,
});
const pollIntervalIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isMounted = useRef(false);
const clearPollInterval = useCallback(() => {
if (pollIntervalIdRef.current) {
clearTimeout(pollIntervalIdRef.current);
pollIntervalIdRef.current = null;
}
}, []);
const updateStatus = useCallback(async () => {
clearPollInterval();
try {
const { data, error } = await api.getDataStreamReindexStatus(dataStreamName);
if (error) {
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamReindexStatus.fetchFailed,
};
});
return;
}
if (data === null) {
return;
}
setReindexState((prevValue: ReindexState) => {
return getReindexState(prevValue, data);
});
if (data.reindexOp && data.reindexOp.status === DataStreamReindexStatus.inProgress) {
// Only keep polling if it exists and is in progress.
pollIntervalIdRef.current = setTimeout(updateStatus, POLL_INTERVAL);
}
} catch (error) {
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamReindexStatus.fetchFailed,
};
});
}
}, [clearPollInterval, api, dataStreamName]);
const startReindex = useCallback(async () => {
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
status: DataStreamReindexStatus.inProgress,
reindexTaskPercComplete: null,
errorMessage: null,
cancelLoadingState: undefined,
};
});
if (reindexState.status === DataStreamReindexStatus.failed) {
try {
await api.cancelDataStreamReindexTask(dataStreamName);
} catch (_) {
// if the task has already failed, attempt to cancel the task
// before attempting to start the reindexing again.
}
}
const { data: reindexOp, error } = await api.startDataStreamReindexTask(dataStreamName);
if (error) {
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamReindexStatus.failed,
};
});
return;
}
setReindexState((prevValue: ReindexState) => {
return getReindexState(prevValue, { reindexOp, meta: prevValue.meta });
});
updateStatus();
}, [api, dataStreamName, updateStatus, reindexState.status]);
const loadDataStreamMetadata = useCallback(async () => {
try {
const { data, error } = await api.getDataStreamMetadata(dataStreamName);
if (error) {
throw error;
}
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
loadingState: LoadingState.Success,
meta: data || null,
};
});
} catch (error) {
setReindexState((prevValue: ReindexState) => {
// if state is completed, we don't need to update the meta
if (prevValue.status === DataStreamReindexStatus.completed) {
return prevValue;
}
return {
...prevValue,
loadingState: LoadingState.Error,
errorMessage: error.message.toString(),
status: DataStreamReindexStatus.failed,
};
});
}
}, [api, dataStreamName]);
const cancelReindex = useCallback(async () => {
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
cancelLoadingState: CancelLoadingState.Requested,
};
});
try {
const { error } = await api.cancelDataStreamReindexTask(dataStreamName);
if (error) {
throw error;
}
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
cancelLoadingState: CancelLoadingState.Success,
status: DataStreamReindexStatus.cancelled,
};
});
} catch (error) {
setReindexState((prevValue: ReindexState) => {
return {
...prevValue,
cancelLoadingState: CancelLoadingState.Error,
};
});
}
}, [api, dataStreamName]);
useEffect(() => {
updateStatus();
}, [updateStatus]);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
// Clean up on unmount.
clearPollInterval();
};
}, [clearPollInterval]);
return {
reindexState,
loadDataStreamMetadata,
startReindex,
cancelReindex,
updateStatus,
};
};

View file

@ -8,7 +8,7 @@
export { MlSnapshotsTableRow } from './ml_snapshots';
export { IndexSettingsTableRow } from './index_settings';
export { DefaultTableRow } from './default';
export { ReindexTableRow } from './reindex';
export { IndexTableRow } from './indices';
export { DataStreamTableRow } from './data_streams';
export { ClusterSettingsTableRow } from './cluster_settings';
export { HealthIndicatorTableRow } from './health_indicator';

View file

@ -0,0 +1,75 @@
/*
* 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, { createContext, useContext } from 'react';
import { ApiService } from '../../../../lib/api';
import { useReindex, ReindexState } from './use_reindex';
import { UpdateIndexState, useUpdateIndex } from './use_update_index';
import { EnrichedDeprecationInfo, IndexAction } from '../../../../../../common/types';
export interface IndexStateContext {
deprecation: EnrichedDeprecationInfo;
reindexState: ReindexState;
startReindex: () => Promise<void>;
cancelReindex: () => Promise<void>;
updateIndexState: UpdateIndexState;
updateIndex: () => Promise<void>;
}
const IndexContext = createContext<IndexStateContext | undefined>(undefined);
export const useIndexContext = () => {
const context = useContext(IndexContext);
if (context === undefined) {
throw new Error('useIndexContext must be used within a <IndexStatusProvider />');
}
return context;
};
interface Props {
api: ApiService;
children: React.ReactNode;
deprecation: EnrichedDeprecationInfo;
}
export const IndexStatusProvider: React.FunctionComponent<Props> = ({
api,
deprecation,
children,
}) => {
const indexName = deprecation.index!;
const indexAction = deprecation.correctiveAction as IndexAction;
const { reindexState, startReindex, cancelReindex } = useReindex({
indexName,
api,
isInDataStream: Boolean(indexAction?.metadata.isInDataStream),
isFrozen: Boolean(indexAction?.metadata.isFrozenIndex),
isClosedIndex: Boolean(indexAction?.metadata.isClosedIndex),
});
const { updateIndexState, updateIndex } = useUpdateIndex({
indexName,
api,
correctiveAction: deprecation.correctiveAction,
});
return (
<IndexContext.Provider
value={{
deprecation,
reindexState,
startReindex,
cancelReindex,
updateIndexState,
updateIndex,
}}
>
{children}
</IndexContext.Provider>
);
};

View file

@ -0,0 +1,246 @@
/*
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types';
import type { IndexStateContext } from '../context';
import { DeprecationBadge } from '../../../../shared';
import {
UIM_REINDEX_READONLY_CLICK,
UIM_REINDEX_READONLY_RETRY_CLICK,
UIM_REINDEX_START_CLICK,
UIM_REINDEX_STOP_CLICK,
UIM_REINDEX_UNFREEZE_CLICK,
UIM_REINDEX_UNFREEZE_RETRY_CLICK,
uiMetricService,
} from '../../../../../lib/ui_metric';
import {
ReindexDetailsFlyoutStep,
UnfreezeDetailsFlyoutStep,
UpdateIndexFlyoutStep,
ReindexFlyoutStep,
WarningFlyoutStep,
type FlyoutStep,
} from './steps';
export interface IndexFlyoutProps extends IndexStateContext {
deprecation: EnrichedDeprecationInfo;
closeFlyout: () => void;
}
export const IndexFlyout: React.FunctionComponent<IndexFlyoutProps> = ({
reindexState,
startReindex,
cancelReindex,
updateIndexState,
updateIndex,
closeFlyout,
deprecation,
}) => {
const { status: reindexStatus, reindexWarnings } = reindexState;
const { status: updateIndexStatus } = updateIndexState;
const { index, correctiveAction } = deprecation;
const [flyoutStep, setFlyoutStep] = useState<FlyoutStep>('details');
useEffect(() => {
switch (reindexStatus) {
case ReindexStatus.failed:
case ReindexStatus.fetchFailed:
case ReindexStatus.cancelled:
case ReindexStatus.inProgress:
case ReindexStatus.completed: {
setFlyoutStep('reindexing');
break;
}
default: {
switch (updateIndexStatus) {
case 'inProgress':
case 'complete':
case 'failed': {
setFlyoutStep(correctiveAction?.type === 'unfreeze' ? 'unfreeze' : 'makeReadonly');
break;
}
default: {
setFlyoutStep('details');
break;
}
}
}
}
}, [correctiveAction?.type, reindexStatus, updateIndexStatus]);
const onStartReindex = useCallback(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK);
startReindex();
}, [startReindex]);
const onMakeReadonly = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_READONLY_CLICK);
await updateIndex();
}, [updateIndex]);
const onMakeReadonlyRetry = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_READONLY_RETRY_CLICK);
await updateIndex();
}, [updateIndex]);
const onUnfreeze = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_UNFREEZE_CLICK);
await updateIndex();
}, [updateIndex]);
const onUnfreezeRetry = useCallback(async () => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_UNFREEZE_RETRY_CLICK);
await updateIndex();
}, [updateIndex]);
const onStopReindex = useCallback(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK);
cancelReindex();
}, [cancelReindex]);
const startReindexWithWarnings = useCallback(() => {
if (
reindexWarnings &&
reindexWarnings.length > 0 &&
reindexStatus !== ReindexStatus.inProgress &&
reindexStatus !== ReindexStatus.completed
) {
setFlyoutStep('confirmReindex');
} else {
onStartReindex();
}
}, [reindexWarnings, reindexStatus, onStartReindex]);
const flyoutContents = useMemo(() => {
switch (flyoutStep) {
case 'details':
return correctiveAction?.type === 'unfreeze' ? (
<UnfreezeDetailsFlyoutStep
closeFlyout={closeFlyout}
startReindex={() => {
setFlyoutStep('confirmReindex');
}}
unfreeze={() => {
setFlyoutStep('unfreeze');
onUnfreeze();
}}
updateIndexState={updateIndexState}
reindexState={reindexState}
/>
) : (
<ReindexDetailsFlyoutStep
closeFlyout={closeFlyout}
startReindex={() => {
setFlyoutStep('confirmReindex');
}}
startReadonly={() => {
setFlyoutStep('confirmReadonly');
}}
deprecation={deprecation}
updateIndexState={updateIndexState}
reindexState={reindexState}
/>
);
case 'confirmReadonly':
case 'confirmReindex':
const flow = flyoutStep === 'confirmReadonly' ? 'readonly' : 'reindex';
return (
<WarningFlyoutStep
warnings={
reindexState.reindexWarnings?.filter(
({ flow: warningFlow }) => warningFlow === 'all' || warningFlow === flow
) ?? []
}
meta={reindexState.meta}
flow={flow}
back={() => setFlyoutStep('details')}
confirm={() => {
if (flyoutStep === 'confirmReadonly') {
setFlyoutStep('makeReadonly');
onMakeReadonly();
} else {
onStartReindex();
}
}}
/>
);
case 'reindexing':
return (
<ReindexFlyoutStep
closeFlyout={closeFlyout}
startReindex={startReindexWithWarnings}
reindexState={reindexState}
cancelReindex={onStopReindex}
/>
);
case 'unfreeze':
return (
<UpdateIndexFlyoutStep
action={flyoutStep}
meta={reindexState.meta}
retry={onUnfreezeRetry}
updateIndexState={updateIndexState}
closeFlyout={closeFlyout}
/>
);
case 'makeReadonly':
return (
<UpdateIndexFlyoutStep
action={flyoutStep}
meta={reindexState.meta}
retry={onMakeReadonlyRetry}
updateIndexState={updateIndexState}
closeFlyout={closeFlyout}
/>
);
}
}, [
flyoutStep,
correctiveAction?.type,
deprecation,
closeFlyout,
updateIndexState,
reindexState,
startReindexWithWarnings,
onStopReindex,
onUnfreezeRetry,
onMakeReadonlyRetry,
onUnfreeze,
onMakeReadonly,
onStartReindex,
]);
return (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge
isCritical={deprecation.isCritical}
isResolved={reindexStatus === ReindexStatus.completed || updateIndexStatus === 'complete'}
/>
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2 id="reindexDetailsFlyoutTitle">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.flyoutHeader"
defaultMessage="Update {index}"
values={{ index }}
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
{flyoutContents}
</>
);
};

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export type { ReindexFlyoutProps } from './container';
export { ReindexFlyout } from './container';
export type { IndexFlyoutProps } from './container';
export { IndexFlyout } from './container';

View file

@ -0,0 +1,79 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiLink, EuiText, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EnrichedDeprecationInfo } from '../../../../../../../../../common/types';
import { useAppContext } from '../../../../../../../app_context';
interface Props {
deprecation: EnrichedDeprecationInfo;
}
/**
* We get copy directly from ES. This contains information that applies to indices
* that are read-only or not.
*/
export const ESTransformsTargetGuidance = ({ deprecation }: Props) => {
const {
services: {
core: { http },
},
} = useAppContext();
return (
<>
<EuiCallOut
title={i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.esTransform.calloutTitle',
{ defaultMessage: 'Transforms detected' }
)}
data-test-subj="esTransformsGuidance"
color="warning"
>
{deprecation.details}
</EuiCallOut>
<EuiSpacer size="s" />
<EuiText size="m">
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.esTransform.description1"
defaultMessage="The reindex operation will copy all of the existing documents into a new index and remove the old one. During the reindex operation your data will be in a read-only state and transforms writing to this index will be paused."
/>
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.esTransform.description2"
defaultMessage="Depending on size and resources, reindexing may take an extended time. For indices with more than 10GB of data or to avoid transform downtime refer to the {migrationGuideLink} or {transformsLink} to manage transforms writing to this index."
values={{
migrationGuideLink: (
<EuiLink target="_blank" href={deprecation.url}>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.esTransform.migrationGuideLink',
{ defaultMessage: 'migration guide' }
)}
</EuiLink>
),
transformsLink: (
<EuiLink
target="_blank"
href={`${http.basePath.prepend('/app/management/data/transform')}`}
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.esTransform.transfromsLink"
defaultMessage="go to transforms"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
</>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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 { ReindexDetailsFlyoutStep } from './reindex_details_step';
export { UnfreezeDetailsFlyoutStep } from './unfreeze_details_step';

View file

@ -0,0 +1,151 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import { EuiLink, EuiSpacer } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ReindexStatus } from '../../../../../../../../../common/types';
import { IndexClosedParagraph } from '../index_closed_paragraph';
export const getReindexButtonLabel = (status?: ReindexStatus) => {
switch (status) {
case ReindexStatus.fetchFailed:
case ReindexStatus.failed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.tryAgainLabel"
defaultMessage="Try again"
/>
);
case ReindexStatus.inProgress:
return (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.reindexingLabel"
defaultMessage="Reindexing…"
/>
);
case ReindexStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.restartLabel"
defaultMessage="Restart reindexing"
/>
);
default:
return (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.runReindexLabel"
defaultMessage="Start reindexing"
/>
);
}
};
export const getDefaultGuideanceText = ({
isClosedIndex,
readOnlyExcluded,
reindexExcluded,
indexBlockUrl,
indexManagementUrl,
}: {
isClosedIndex: boolean;
readOnlyExcluded: boolean;
reindexExcluded: boolean;
indexBlockUrl: string;
indexManagementUrl: string;
}) => {
const guideanceListItems = [];
if (!reindexExcluded) {
guideanceListItems.push({
title: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option1.title',
{
defaultMessage: 'Option {optionCount}: Reindex data',
values: { optionCount: guideanceListItems.length + 1 },
}
),
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option1.description"
defaultMessage="The reindex operation allows transforming an index into a new, compatible one. It will copy all of the existing documents into a new index and remove the old one. Depending on size and resources, reindexing may take extended time and your data will be in a read-only state until the job has completed."
/>
{isClosedIndex && (
<Fragment>
<EuiSpacer size="xs" />
<IndexClosedParagraph />
</Fragment>
)}
</EuiText>
),
});
}
if (!readOnlyExcluded) {
guideanceListItems.push({
title: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option2.title',
{
defaultMessage: 'Option {optionCount}: Mark as read-only',
values: { optionCount: guideanceListItems.length + 1 },
}
),
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option2.description"
defaultMessage="Old indices can maintain compatibility with the next major version if they are turned into read-only mode. If you no longer need to update documents in this index (or add new ones), you might want to convert it to a read-only index. {docsLink}"
values={{
docsLink: (
<EuiLink target="_blank" href={indexBlockUrl}>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
),
});
}
guideanceListItems.push({
title: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option3.title',
{
defaultMessage: 'Option {optionCount}: Delete index',
values: { optionCount: guideanceListItems.length + 1 },
}
),
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option3.description"
defaultMessage="If you no longer need it, you can also delete the index from {indexManagementLinkHtml}."
values={{
indexManagementLinkHtml: (
<EuiLink href={indexManagementUrl}>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.indexMgmtLink"
defaultMessage="Index Management"
/>
</EuiLink>
),
}}
/>
</EuiText>
),
});
return guideanceListItems;
};

View file

@ -0,0 +1,98 @@
/*
* 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 { FunctionComponent } from 'react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiDescriptionList, EuiLink, EuiText } from '@elastic/eui';
import { useAppContext } from '../../../../../../../app_context';
export const MlAnomalyGuidance: FunctionComponent = () => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
return (
<>
<p>
<EuiCallOut
title={i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.notCompatibleMlAnomalyIndexTitle',
{ defaultMessage: 'ML anomaly index detected' }
)}
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.notCompatibleMlAnomalyIndexText"
defaultMessage="Anomaly result indices that were created in 7.x must be either reindexed, marked as read-only, or deleted before upgrading to 9.x. {learnMore}."
values={{
learnMore: (
<EuiLink target="_blank" href={docLinks.links.ml.anomalyMigrationGuide}>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.notCompatibleMlAnomalyIndexText.learnMore',
{ defaultMessage: 'Learn more' }
)}
</EuiLink>
),
}}
/>
</EuiCallOut>
</p>
<EuiDescriptionList
rowGutterSize="m"
listItems={[
{
title: 'Option 1: Reindex data',
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexMlAnomalyIndexText"
defaultMessage="While anomaly detection results are being reindexed, jobs continue to run and process new data. However, you cannot completely delete an anomaly detection job that stores results in this index until the reindexing is complete."
/>
</EuiText>
),
},
{
title: 'Option 2: Mark as read-only',
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.readOnlyMlAnomalyIndexText"
defaultMessage="This skips reindexing and will mark the result index as read-only. It is useful for large indices that contain the results of only one or a few anomaly detection jobs. If you delete these jobs later, you will not be able to create a new job with the same name. {learnMore} about write blocks."
values={{
learnMore: (
<EuiLink target="_blank" href={docLinks.links.upgradeAssistant.indexBlocks}>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
),
},
{
title: 'Option 3: Delete this index',
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.deleteMlAnomalyIndexText"
defaultMessage="Use the ML UI to delete jobs that are no longer needed. The result index is deleted when all jobs that store results in it have been deleted."
/>
</EuiText>
),
},
]}
/>
</>
);
};

View file

@ -0,0 +1,493 @@
/*
* 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 { shallow } from 'enzyme';
import React from 'react';
import { ReindexDetailsFlyoutStep } from './reindex_details_step';
import type { ReindexState } from '../../../use_reindex';
import type { UpdateIndexState } from '../../../use_update_index';
import { LoadingState } from '../../../../../../types';
import { EnrichedDeprecationInfo } from '../../../../../../../../../common/types';
jest.mock('../../../../../../../app_context', () => {
const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks');
return {
useAppContext: () => {
return {
services: {
api: {
useLoadNodeDiskSpace: () => [],
},
core: {
docLinks: docLinksServiceMock.createStartContract(),
http: {
basePath: {
prepend: jest.fn(),
},
},
},
},
};
},
};
});
describe('ReindexDetailsFlyoutStep', () => {
const defaultDeprecation: () => EnrichedDeprecationInfo = () => ({
isCritical: true,
message: 'foo',
resolveDuringUpgrade: false,
type: 'index_settings',
url: 'https://te.st',
});
const defaultReindexState: () => ReindexState = () => ({
loadingState: LoadingState.Success,
meta: {
indexName: 'some_index',
reindexName: 'some_index-reindexed-for-9',
aliases: [],
isInDataStream: false,
isFrozen: false,
isReadonly: false,
isClosedIndex: false,
},
hasRequiredPrivileges: true,
reindexTaskPercComplete: null,
errorMessage: null,
});
const defaultUpdateIndexState: () => UpdateIndexState = () => ({
status: 'incomplete',
failedBefore: false,
});
it('renders for non-readonly indices', () => {
const wrapper = shallow(
<ReindexDetailsFlyoutStep
closeFlyout={jest.fn()}
startReindex={jest.fn()}
startReadonly={jest.fn()}
reindexState={defaultReindexState()}
updateIndexState={defaultUpdateIndexState()}
deprecation={defaultDeprecation()}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<EuiFlyoutBody>
<EuiText>
<p>
<MemoizedFormattedMessage
defaultMessage="This index was created in ES 7.x and it is not compatible with the next major version. Choose one of the following options:"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.notCompatibleIndexText"
/>
</p>
<EuiDescriptionList
listItems={
Array [
Object {
"description": <EuiText
size="m"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="The reindex operation allows transforming an index into a new, compatible one. It will copy all of the existing documents into a new index and remove the old one. Depending on size and resources, reindexing may take extended time and your data will be in a read-only state until the job has completed."
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option1.description"
/>
</EuiText>,
"title": "Option 1: Reindex data",
},
Object {
"description": <EuiText
size="m"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Old indices can maintain compatibility with the next major version if they are turned into read-only mode. If you no longer need to update documents in this index (or add new ones), you might want to convert it to a read-only index. {docsLink}"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option2.description"
values={
Object {
"docsLink": <EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/index-modules-blocks.html#index-block-settings"
target="_blank"
>
Learn more
</EuiLink>,
}
}
/>
</EuiText>,
"title": "Option 2: Mark as read-only",
},
Object {
"description": <EuiText
size="m"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="If you no longer need it, you can also delete the index from {indexManagementLinkHtml}."
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindex.option3.description"
values={
Object {
"indexManagementLinkHtml": <EuiLink
href="undefined"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Index Management"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.indexMgmtLink"
/>
</EuiLink>,
}
}
/>
</EuiText>,
"title": "Option 3: Delete index",
},
]
}
rowGutterSize="m"
/>
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="left"
iconType="cross"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFlexGroup
gutterSize="s"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="accent"
data-test-subj="startIndexReadonlyButton"
disabled={false}
fill={false}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Mark as read-only"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.startIndexReadonlyButton"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="startReindexingButton"
disabled={false}
fill={true}
isLoading={false}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.runReindexLabel"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
`);
});
it('renders correct guidance for indices with transforms', () => {
const wrapper = shallow(
<ReindexDetailsFlyoutStep
closeFlyout={jest.fn()}
startReindex={jest.fn()}
startReadonly={jest.fn()}
reindexState={defaultReindexState()}
updateIndexState={defaultUpdateIndexState()}
deprecation={{
...defaultDeprecation(),
correctiveAction: {
type: 'reindex',
transformIds: ['abc', 'def'],
metadata: {
isFrozenIndex: false,
isInDataStream: false,
isClosedIndex: false,
},
},
}}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<EuiFlyoutBody>
<EuiText>
<ESTransformsTargetGuidance
deprecation={
Object {
"correctiveAction": Object {
"metadata": Object {
"isClosedIndex": false,
"isFrozenIndex": false,
"isInDataStream": false,
},
"transformIds": Array [
"abc",
"def",
],
"type": "reindex",
},
"isCritical": true,
"message": "foo",
"resolveDuringUpgrade": false,
"type": "index_settings",
"url": "https://te.st",
}
}
/>
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="left"
iconType="cross"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFlexGroup
gutterSize="s"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="startReindexingButton"
disabled={false}
fill={true}
isLoading={false}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.runReindexLabel"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
`);
});
it('renders for readonly indices (warning deprecation)', () => {
const props = defaultReindexState();
props.meta.isReadonly = true;
const wrapper = shallow(
<ReindexDetailsFlyoutStep
closeFlyout={jest.fn()}
startReindex={jest.fn()}
startReadonly={jest.fn()}
reindexState={props}
updateIndexState={defaultUpdateIndexState()}
deprecation={defaultDeprecation()}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<EuiFlyoutBody>
<EuiText>
<p>
<MemoizedFormattedMessage
defaultMessage="This index was created in ES 7.x. It has been marked as read-only, which enables compatibility with the next major version."
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.readonlyCompatibleIndexText"
/>
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="The reindex operation allows transforming an index into a new, compatible one. It will copy all of the existing documents into a new index and remove the old one. Depending on size and resources, reindexing may take extended time and your data will be in a read-only state until the job has completed."
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexText"
/>
</p>
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="left"
iconType="cross"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFlexGroup
gutterSize="s"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="startReindexingButton"
disabled={false}
fill={true}
isLoading={false}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.runReindexLabel"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
`);
});
it('renders ML anomaly index guidance', () => {
const reindexState = defaultReindexState();
reindexState.meta.indexName = '.ml-anomalies-1';
const deprecation = defaultDeprecation();
deprecation.index = '.ml-anomalies-1';
const wrapper = shallow(
<ReindexDetailsFlyoutStep
closeFlyout={jest.fn()}
startReindex={jest.fn()}
startReadonly={jest.fn()}
reindexState={reindexState}
updateIndexState={defaultUpdateIndexState()}
deprecation={deprecation}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<EuiFlyoutBody>
<EuiText>
<MlAnomalyGuidance />
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="left"
iconType="cross"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiFlexGroup
gutterSize="s"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="accent"
data-test-subj="startIndexReadonlyButton"
disabled={false}
fill={false}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Mark as read-only"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.startIndexReadonlyButton"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="startReindexingButton"
disabled={false}
fill={true}
isLoading={false}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexButton.runReindexLabel"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
`);
});
});

View file

@ -0,0 +1,270 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EnrichedDeprecationInfo,
ReindexAction,
ReindexStatus,
} from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex';
import { useAppContext } from '../../../../../../../app_context';
import { getDefaultGuideanceText, getReindexButtonLabel } from './messages';
import { FrozenCallOut } from '../frozen_callout';
import type { UpdateIndexState } from '../../../use_update_index';
import { FetchFailedCallOut } from '../fetch_failed_callout';
import { ReindexingFailedCallOut } from '../reindexing_failed_callout';
import { MlAnomalyGuidance } from './ml_anomaly_guidance';
import { ESTransformsTargetGuidance } from './es_transform_target_guidance';
import { IndexClosedParagraph } from '../index_closed_paragraph';
const ML_ANOMALIES_PREFIX = '.ml-anomalies-';
/**
* Displays a flyout that shows the details / corrective action for a "reindex" deprecation for a given index.
*/
export const ReindexDetailsFlyoutStep: React.FunctionComponent<{
reindexState: ReindexState;
updateIndexState: UpdateIndexState;
deprecation: EnrichedDeprecationInfo;
startReindex: () => void;
startReadonly: () => void;
closeFlyout: () => void;
}> = ({
reindexState,
updateIndexState,
deprecation,
startReindex,
startReadonly,
closeFlyout,
}) => {
const {
services: {
api,
core: { docLinks, http },
},
} = useAppContext();
const { loadingState, status: reindexStatus, hasRequiredPrivileges, meta } = reindexState;
const { status: updateIndexStatus } = updateIndexState;
const { indexName, isFrozen, isClosedIndex, isReadonly } = meta;
const loading = loadingState === LoadingState.Loading;
const isCompleted = reindexStatus === ReindexStatus.completed || updateIndexStatus === 'complete';
const hasFetchFailed = reindexStatus === ReindexStatus.fetchFailed;
const hasReindexingFailed = reindexStatus === ReindexStatus.failed;
const correctiveAction = deprecation.correctiveAction as ReindexAction | undefined;
const isESTransformTarget = !!correctiveAction?.transformIds?.length;
const isMLAnomalyIndex = Boolean(indexName?.startsWith(ML_ANOMALIES_PREFIX));
const { excludedActions = [] } = (deprecation.correctiveAction as ReindexAction) || {};
const readOnlyExcluded = excludedActions.includes('readOnly');
const reindexExcluded = excludedActions.includes('reindex');
const { data: nodes } = api.useLoadNodeDiskSpace();
let showEsTransformsGuidance = false;
let showMlAnomalyReindexingGuidance = false;
let showReadOnlyGuidance = false;
let showDefaultGuidance = false;
if (isESTransformTarget) {
showEsTransformsGuidance = true;
} else if (isReadonly) {
showReadOnlyGuidance = true;
} else if (isMLAnomalyIndex) {
showMlAnomalyReindexingGuidance = true;
} else {
showDefaultGuidance = true;
}
return (
<Fragment>
<EuiFlyoutBody>
{hasRequiredPrivileges === false && (
<Fragment>
<EuiSpacer />
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this index."
/>
}
color="danger"
iconType="warning"
/>
</Fragment>
)}
{nodes && nodes.length > 0 && (
<>
<EuiCallOut
color="warning"
iconType="warning"
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
/>
<EuiSpacer size="s" />
<ul>
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
available,
}}
/>
</li>
))}
</ul>
</>
</EuiCallOut>
<EuiSpacer />
</>
)}
{hasFetchFailed && <FetchFailedCallOut errorMessage={reindexState.errorMessage!} />}
{!hasFetchFailed && hasReindexingFailed && (
<ReindexingFailedCallOut errorMessage={reindexState.errorMessage!} />
)}
{isFrozen && <FrozenCallOut />}
<EuiText>
{showEsTransformsGuidance && <ESTransformsTargetGuidance deprecation={deprecation} />}
{showMlAnomalyReindexingGuidance && <MlAnomalyGuidance />}
{showReadOnlyGuidance && (
<Fragment>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.readonlyCompatibleIndexText"
defaultMessage="This index was created in ES 7.x. It has been marked as read-only, which enables compatibility with the next major version."
/>
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexText"
defaultMessage="The reindex operation allows transforming an index into a new, compatible one. It will copy all of the existing documents into a new index and remove the old one. Depending on size and resources, reindexing may take extended time and your data will be in a read-only state until the job has completed."
/>
</p>
{isClosedIndex && (
<p>
<IndexClosedParagraph />
</p>
)}
</Fragment>
)}
{showDefaultGuidance && (
<Fragment>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.notCompatibleIndexText"
defaultMessage="This index was created in ES 7.x and it is not compatible with the next major version. Choose one of the following options:"
/>
</p>
<EuiDescriptionList
rowGutterSize="m"
listItems={getDefaultGuideanceText({
isClosedIndex,
readOnlyExcluded,
reindexExcluded,
indexManagementUrl: `${http.basePath.prepend(
`/app/management/data/index_management/indices/index_details?indexName=${indexName}`
)}`,
indexBlockUrl: docLinks.links.upgradeAssistant.indexBlocks,
})}
/>
</Fragment>
)}
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{!isReadonly &&
!hasFetchFailed &&
!isCompleted &&
hasRequiredPrivileges &&
!isESTransformTarget &&
!readOnlyExcluded && (
<EuiFlexItem grow={false}>
<EuiButton
onClick={startReadonly}
disabled={loading}
color={reindexExcluded ? 'primary' : 'accent'}
fill={reindexExcluded}
data-test-subj="startIndexReadonlyButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.startIndexReadonlyButton"
defaultMessage="Mark as read-only"
/>
</EuiButton>
</EuiFlexItem>
)}
{!hasFetchFailed && !isCompleted && hasRequiredPrivileges && !reindexExcluded && (
<EuiFlexItem grow={false}>
<EuiButton
fill
color={reindexStatus === ReindexStatus.cancelled ? 'warning' : 'primary'}
iconType={reindexStatus === ReindexStatus.cancelled ? 'play' : undefined}
onClick={startReindex}
isLoading={loading}
disabled={loading}
data-test-subj="startReindexingButton"
>
{getReindexButtonLabel(reindexStatus)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
);
};

View file

@ -0,0 +1,98 @@
/*
* 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 { shallow } from 'enzyme';
import React from 'react';
import type { ReindexState } from '../../../use_reindex';
import type { UpdateIndexState } from '../../../use_update_index';
import { LoadingState } from '../../../../../../types';
import { UnfreezeDetailsFlyoutStep } from './unfreeze_details_step';
jest.mock('../../../../../../../app_context', () => {
const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks');
return {
useAppContext: () => {
return {
services: {
api: {
useLoadNodeDiskSpace: () => [],
},
core: {
docLinks: docLinksServiceMock.createStartContract(),
http: {
basePath: {
prepend: jest.fn(),
},
},
},
},
};
},
};
});
describe('UnfreezeDetailsFlyoutStep', () => {
const defaultReindexState: ReindexState = {
loadingState: LoadingState.Success,
meta: {
indexName: 'some_index',
aliases: [],
isFrozen: true,
isReadonly: true,
isInDataStream: false,
isClosedIndex: false,
reindexName: 'some_index-reindexed-for-9',
},
hasRequiredPrivileges: true,
reindexTaskPercComplete: null,
errorMessage: null,
};
const defaultUpdateIndexState: UpdateIndexState = {
status: 'incomplete',
failedBefore: false,
};
it('renders all options for regular indices', () => {
const wrapper = shallow(
<UnfreezeDetailsFlyoutStep
closeFlyout={jest.fn()}
startReindex={jest.fn()}
unfreeze={jest.fn()}
reindexState={defaultReindexState}
updateIndexState={defaultUpdateIndexState}
/>
);
expect(wrapper.find('EuiButton[data-test-subj="startReindexingButton"]')).toHaveLength(1);
expect(wrapper.find('EuiButton[data-test-subj="startUnfreezeButton"]')).toHaveLength(1);
});
it('does NOT render Reindex option for data stream backing indices', () => {
const backingIndexReindexState = {
...defaultReindexState,
meta: {
...defaultReindexState.meta,
isInDataStream: true,
},
};
const wrapper = shallow(
<UnfreezeDetailsFlyoutStep
closeFlyout={jest.fn()}
startReindex={jest.fn()}
unfreeze={jest.fn()}
reindexState={backingIndexReindexState}
updateIndexState={defaultUpdateIndexState}
/>
);
expect(wrapper.find('EuiButton[data-test-subj="startReindexingButton"]')).toHaveLength(0);
expect(wrapper.find('EuiButton[data-test-subj="startUnfreezeButton"]')).toHaveLength(1);
});
});

View file

@ -0,0 +1,265 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiLink,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { ReindexStatus } from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex';
import { useAppContext } from '../../../../../../../app_context';
import { getReindexButtonLabel } from './messages';
import type { UpdateIndexState } from '../../../use_update_index';
import { FetchFailedCallOut } from '../fetch_failed_callout';
import { ReindexingFailedCallOut } from '../reindexing_failed_callout';
import { IndexClosedParagraph } from '../index_closed_paragraph';
/**
* Displays a flyout that shows the details / corrective action for a "reindex" deprecation for a given index.
*/
export const UnfreezeDetailsFlyoutStep: React.FunctionComponent<{
closeFlyout: () => void;
reindexState: ReindexState;
updateIndexState: UpdateIndexState;
startReindex: () => void;
unfreeze: () => void;
}> = ({ closeFlyout, reindexState, updateIndexState, startReindex, unfreeze }) => {
const {
services: {
api,
core: { http },
},
} = useAppContext();
const { loadingState, status: reindexStatus, hasRequiredPrivileges, meta } = reindexState;
const { status: updateIndexStatus } = updateIndexState;
const { indexName, isInDataStream, isClosedIndex } = meta;
const loading = loadingState === LoadingState.Loading;
const isCompleted = reindexStatus === ReindexStatus.completed || updateIndexStatus === 'complete';
const hasFetchFailed = reindexStatus === ReindexStatus.fetchFailed;
const hasReindexingFailed = reindexStatus === ReindexStatus.failed;
const { data: nodes } = api.useLoadNodeDiskSpace();
return (
<Fragment>
<EuiFlyoutBody>
{hasRequiredPrivileges === false && (
<Fragment>
<EuiSpacer />
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this index."
/>
}
color="danger"
iconType="warning"
/>
</Fragment>
)}
{nodes && nodes.length > 0 && (
<>
<EuiCallOut
color="warning"
iconType="warning"
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
/>
<EuiSpacer size="s" />
<ul>
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
available,
}}
/>
</li>
))}
</ul>
</>
</EuiCallOut>
<EuiSpacer />
</>
)}
{hasFetchFailed && <FetchFailedCallOut errorMessage={reindexState.errorMessage!} />}
{!hasFetchFailed && hasReindexingFailed && (
<ReindexingFailedCallOut errorMessage={reindexState.errorMessage!} />
)}
<EuiText>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.frozenIndexText"
defaultMessage="This index is frozen. Frozen indices will no longer be supported after the upgrade. Choose one of the following options:"
/>
</p>
<EuiDescriptionList
rowGutterSize="m"
listItems={[
{
title: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.option1.title',
{
defaultMessage: 'Option 1: Unfreeze index',
}
),
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.option1.description"
defaultMessage="Unfreeze this index and make it read-only. This ensures that the index will remain compatible with the next major version."
/>
</EuiText>
),
},
/* We cannot reindex backing indices in the same way as regular indices (that would break the related data_stream) */
...(!isInDataStream
? [
{
title: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.option2.title',
{
defaultMessage: 'Option 2: Reindex data',
}
),
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.option2.description"
defaultMessage="Alternatively, you can reindex the data into a new, compatible index. All existing documents will be copied over to a new index, and the old index will be removed. Depending on the size of the index and the available resources, the reindexing operation can take some time. Your data will be in read-only mode until the reindexing has completed."
/>
{isClosedIndex && (
<Fragment>
<EuiSpacer size="xs" />
<IndexClosedParagraph />
</Fragment>
)}
</EuiText>
),
},
]
: []),
{
title: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.alternativeOption.title',
{
defaultMessage: 'Alternative: Delete the index',
}
),
description: (
<EuiText size="m">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreeze.alternativeOption.description"
defaultMessage="If you no longer need it, you can also delete the index from {indexManagementLinkHtml}."
values={{
indexManagementLinkHtml: (
<EuiLink
href={`${http.basePath.prepend(
`/app/management/data/index_management/indices/index_details?indexName=${indexName}`
)}`}
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.indexMgmtLink"
defaultMessage="Index Management"
/>
</EuiLink>
),
}}
/>
</EuiText>
),
},
]}
/>
</EuiText>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{/* We cannot reindex backing indices in the same way as regular indices (that would break the related data_stream) */}
{!isInDataStream && !hasFetchFailed && !isCompleted && hasRequiredPrivileges && (
<EuiFlexItem grow={false}>
<EuiButton
color={reindexStatus === ReindexStatus.cancelled ? 'warning' : 'primary'}
iconType={reindexStatus === ReindexStatus.cancelled ? 'play' : undefined}
onClick={startReindex}
isLoading={loading}
disabled={loading}
data-test-subj="startReindexingButton"
>
{getReindexButtonLabel(reindexStatus)}
</EuiButton>
</EuiFlexItem>
)}
{!hasFetchFailed && !isCompleted && hasRequiredPrivileges && (
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={unfreeze}
disabled={loading}
data-test-subj="startUnfreezeButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.unfreezeIndexButton"
defaultMessage="Unfreeze"
/>
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
);
};

View file

@ -0,0 +1,36 @@
/*
* 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';
import { FormattedMessage } from '@kbn/i18n-react';
interface Props {
errorMessage: string;
}
export const FetchFailedCallOut: React.FunctionComponent<Props> = (props) => {
const { errorMessage } = props;
return (
<Fragment>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj="fetchFailedCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.fetchFailedCalloutTitle"
defaultMessage="Reindex status not available"
/>
}
>
{errorMessage}
</EuiCallOut>
<EuiSpacer />
</Fragment>
);
};

View file

@ -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 React, { Fragment } from 'react';
import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useAppContext } from '../../../../../../app_context';
export const FrozenCallOut: React.FunctionComponent = () => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
return (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.frozenCallout.reindexFrozenIndexTitle"
defaultMessage="This index is frozen"
/>
}
iconType="iInCircle"
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.frozenCallout.reindexFrozenIndex"
defaultMessage="Frozen indices will no longer be supported after the upgrade. As a result, this index will be transformed into a non-frozen index during the update operation. {docsLink}"
values={{
docsLink: (
<EuiLink target="_blank" href={docLinks.links.upgradeAssistant.unfreezeApi}>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
),
}}
/>
</EuiCallOut>
<EuiSpacer />
</Fragment>
);
};

View file

@ -0,0 +1,13 @@
/*
* 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 type { FlyoutStep } from './types';
export { ReindexDetailsFlyoutStep } from './details/reindex_details_step';
export { UnfreezeDetailsFlyoutStep } from './details/unfreeze_details_step';
export { WarningFlyoutStep } from './warning/warning_step';
export { ReindexFlyoutStep } from './reindex/reindex_step';
export { UpdateIndexFlyoutStep } from './update/update_step';

View file

@ -0,0 +1,47 @@
/*
* 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 from 'react';
import { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useAppContext } from '../../../../../../app_context';
export const IndexClosedParagraph: React.FunctionComponent = () => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
return (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.indexClosed"
defaultMessage="This index is currently closed. The Upgrade Assistant will open, reindex and then close the index. {reindexingMayTakeLongerEmph}. {learnMore}"
values={{
learnMore: (
<EuiLink target="_blank" href={docLinks.links.apis.openIndex}>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
),
reindexingMayTakeLongerEmph: (
<b>
{i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.detailsStep.reindexTakesLonger',
{ defaultMessage: 'Reindexing may take longer than usual' }
)}
</b>
),
}}
/>
);
};

View file

@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChecklistFlyout renders 1`] = `
exports[`ReindexStep renders 1`] = `
<Fragment>
<EuiFlyoutBody>
<EuiText>
<p>
<MemoizedFormattedMessage
defaultMessage="The index will be read-only during reindexing. You won't be able to add, update, or delete documents until reindexing is complete. If you need to reindex to a new cluster, use the reindex API. {docsLink}"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexDescription"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexDescription"
values={
Object {
"docsLink": <EuiLink
@ -23,7 +23,7 @@ exports[`ChecklistFlyout renders 1`] = `
<p>
<MemoizedFormattedMessage
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress or resume reindexing after a Kibana restart."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.readonlyCallout.backgroundResumeDetail"
/>
</p>
</EuiText>
@ -39,6 +39,10 @@ exports[`ChecklistFlyout renders 1`] = `
"meta": Object {
"aliases": Array [],
"indexName": "myIndex",
"isClosedIndex": false,
"isFrozen": false,
"isInDataStream": false,
"isReadonly": false,
"reindexName": "reindexed-myIndex",
},
"reindexTaskPercComplete": null,
@ -62,7 +66,7 @@ exports[`ChecklistFlyout renders 1`] = `
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
@ -79,7 +83,7 @@ exports[`ChecklistFlyout renders 1`] = `
>
<MemoizedFormattedMessage
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.runReindexLabel"
/>
</EuiButton>
</EuiFlexItem>
@ -88,14 +92,15 @@ exports[`ChecklistFlyout renders 1`] = `
</Fragment>
`;
exports[`ChecklistFlyout renders for frozen indices 1`] = `
exports[`ReindexStep renders for frozen indices 1`] = `
<Fragment>
<EuiFlyoutBody>
<FrozenCallOut />
<EuiText>
<p>
<MemoizedFormattedMessage
defaultMessage="The index will be read-only during reindexing. You won't be able to add, update, or delete documents until reindexing is complete. If you need to reindex to a new cluster, use the reindex API. {docsLink}"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexDescription"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexDescription"
values={
Object {
"docsLink": <EuiLink
@ -108,35 +113,10 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = `
}
/>
</p>
<EuiCallOut
iconType="iInCircle"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="This index is frozen"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexFrozenIndexTitle"
/>
}
>
<MemoizedFormattedMessage
defaultMessage="Frozen indices will no longer be supported after upgrade, so this index will be deleted as part the reindex operation. {docsLink}"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexFrozenIndex"
values={
Object {
"docsLink": <EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/unfreeze-index-api.html"
target="_blank"
>
Learn more
</EuiLink>,
}
}
/>
</EuiCallOut>
<EuiSpacer />
<p>
<MemoizedFormattedMessage
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress or resume reindexing after a Kibana restart."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.readonlyCallout.backgroundResumeDetail"
/>
</p>
</EuiText>
@ -152,6 +132,10 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = `
"meta": Object {
"aliases": Array [],
"indexName": "myIndex",
"isClosedIndex": false,
"isFrozen": true,
"isInDataStream": false,
"isReadonly": false,
"reindexName": "reindexed-myIndex",
},
"reindexTaskPercComplete": null,
@ -175,7 +159,7 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = `
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
@ -192,7 +176,7 @@ exports[`ChecklistFlyout renders for frozen indices 1`] = `
>
<MemoizedFormattedMessage
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.runReindexLabel"
/>
</EuiButton>
</EuiFlexItem>

View file

@ -8,9 +8,9 @@
import { shallow } from 'enzyme';
import React from 'react';
import { ReindexStatus, ReindexStep } from '../../../../../../../common/types';
import { LoadingState } from '../../../../types';
import type { ReindexState } from '../use_reindex_state';
import { ReindexStatus, ReindexStep } from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex';
import { ReindexProgress } from './progress';
describe('ReindexProgress', () => {
@ -28,6 +28,10 @@ describe('ReindexProgress', () => {
indexName: 'foo',
reindexName: 'reindexed-foo',
aliases: [],
isFrozen: false,
isReadonly: false,
isInDataStream: false,
isClosedIndex: false,
},
} as ReindexState
}
@ -44,7 +48,7 @@ describe('ReindexProgress', () => {
<h3>
<MemoizedFormattedMessage
defaultMessage="Reindexing in progress… {percents}"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingInProgressTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingInProgressTitle"
values={
Object {
"percents": "0%",
@ -60,7 +64,7 @@ describe('ReindexProgress', () => {
"status": "inProgress",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Setting {indexName} index to read-only."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.readonlyStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.readonlyStepTitle"
values={
Object {
"indexName": <EuiCode>
@ -74,7 +78,7 @@ describe('ReindexProgress', () => {
"status": "incomplete",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Create {reindexName} index."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.createIndexStepTitle"
values={
Object {
"reindexName": <EuiCode>
@ -96,6 +100,10 @@ describe('ReindexProgress', () => {
"meta": Object {
"aliases": Array [],
"indexName": "foo",
"isClosedIndex": false,
"isFrozen": false,
"isInDataStream": false,
"isReadonly": false,
"reindexName": "reindexed-foo",
},
"reindexTaskPercComplete": null,
@ -108,7 +116,7 @@ describe('ReindexProgress', () => {
"status": "incomplete",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Copy original index settings from {indexName} to {reindexName}."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.indexSettingsRestoredStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.indexSettingsRestoredStepTitle"
values={
Object {
"indexName": <EuiCode>
@ -125,7 +133,7 @@ describe('ReindexProgress', () => {
"status": "incomplete",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Create {indexName} alias for {reindexName} index."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasCreatedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.aliasCreatedStepTitle"
values={
Object {
"indexName": <EuiCode>
@ -142,7 +150,7 @@ describe('ReindexProgress', () => {
"status": "incomplete",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Delete original {indexName} index."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.originalIndexDeletedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.originalIndexDeletedStepTitle"
values={
Object {
"indexName": <EuiCode>
@ -173,6 +181,10 @@ describe('ReindexProgress', () => {
indexName: 'foo',
reindexName: 'reindexed-foo',
aliases: [],
isFrozen: true,
isReadonly: false,
isInDataStream: false,
isClosedIndex: false,
},
} as ReindexState
}

View file

@ -18,11 +18,11 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { ReindexStatus, ReindexStep } from '../../../../../../../common/types';
import { CancelLoadingState } from '../../../../types';
import type { ReindexState } from '../use_reindex_state';
import { StepProgress, StepProgressStep } from './step_progress';
import { getReindexProgressLabel } from '../../../../../lib/utils';
import { ReindexStatus, ReindexStep } from '../../../../../../../../../common/types';
import { CancelLoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex';
import { StepProgress, StepProgressStep } from '../../../../../common/step_progress';
import { getReindexProgressLabel } from '../../../../../../../lib/utils';
const ErrorCallout: React.FunctionComponent<{ errorMessage: string | null }> = ({
errorMessage,
@ -49,7 +49,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{
return (
<>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelledTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.cancelledTitle"
defaultMessage="Reindexing cancelled."
/>
</>
@ -71,7 +71,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{
case CancelLoadingState.Loading:
cancelText = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.cancelButton.cancellingLabel"
defaultMessage="Cancelling…"
/>
);
@ -79,7 +79,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{
case CancelLoadingState.Success:
cancelText = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancelledLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.cancelButton.cancelledLabel"
defaultMessage="Cancelled"
/>
);
@ -87,7 +87,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{
case CancelLoadingState.Error:
cancelText = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.cancelButton.errorLabel"
defaultMessage="Could not cancel"
/>
);
@ -95,7 +95,7 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{
default:
cancelText = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancelLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.cancelButton.cancelLabel"
defaultMessage="Cancel"
/>
);
@ -106,12 +106,12 @@ const ReindexingDocumentsStepTitle: React.FunctionComponent<{
<EuiFlexItem grow={false}>
{stepInProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="Reindexing documents."
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.reindexingDocumentsStepTitle"
defaultMessage="Reindex documents."
/>
)}
@ -141,7 +141,7 @@ const getStepTitle = (
if (step === ReindexStep.readonly) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.readonlyStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.readonlyStepTitle"
defaultMessage="Setting {indexName} index to read-only."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -149,7 +149,7 @@ const getStepTitle = (
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.readonlyStepTitle"
defaultMessage="Set {indexName} index to read-only."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -161,7 +161,7 @@ const getStepTitle = (
if (step === ReindexStep.newIndexCreated) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.createIndexStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.createIndexStepTitle"
defaultMessage="Creating {reindexName} index."
values={{
reindexName: <EuiCode>{meta.reindexName}</EuiCode>,
@ -169,7 +169,7 @@ const getStepTitle = (
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.createIndexStepTitle"
defaultMessage="Create {reindexName} index."
values={{
reindexName: <EuiCode>{meta.reindexName}</EuiCode>,
@ -181,7 +181,7 @@ const getStepTitle = (
if (step === ReindexStep.indexSettingsRestored) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.indexSettingsRestoredStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.indexSettingsRestoredStepTitle"
defaultMessage="Copying original index settings from {indexName} to {reindexName}."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -190,7 +190,7 @@ const getStepTitle = (
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.indexSettingsRestoredStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.indexSettingsRestoredStepTitle"
defaultMessage="Copy original index settings from {indexName} to {reindexName}."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -203,7 +203,7 @@ const getStepTitle = (
if (step === ReindexStep.aliasCreated) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.aliasCreatedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.aliasCreatedStepTitle"
defaultMessage="Creating {indexName} alias for {reindexName} index."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -212,7 +212,7 @@ const getStepTitle = (
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasCreatedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.aliasCreatedStepTitle"
defaultMessage="Create {indexName} alias for {reindexName} index."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -225,7 +225,7 @@ const getStepTitle = (
if (step === ReindexStep.originalIndexDeleted) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.originalIndexDeletedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.originalIndexDeletedStepTitle"
defaultMessage="Deleting original {indexName} index."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -233,7 +233,7 @@ const getStepTitle = (
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.originalIndexDeletedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.originalIndexDeletedStepTitle"
defaultMessage="Delete original {indexName} index."
values={{
indexName: <EuiCode>{meta.indexName}</EuiCode>,
@ -245,7 +245,7 @@ const getStepTitle = (
if (step === ReindexStep.existingAliasesUpdated) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.aliasesUpdatedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.inProgress.aliasesUpdatedStepTitle"
defaultMessage="Updating {existingAliases} aliases to point to {reindexName} index."
values={{
existingAliases: <EuiCode>{`[${meta.aliases.join(',')}]`}</EuiCode>,
@ -254,7 +254,7 @@ const getStepTitle = (
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasesUpdatedStepTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklist.aliasesUpdatedStepTitle"
defaultMessage="Update {existingAliases} aliases to point to {reindexName} index."
values={{
existingAliases: <EuiCode>{`[${meta.aliases.join(',')}]`}</EuiCode>,
@ -378,7 +378,7 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
<h3>
{status === ReindexStatus.inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingInProgressTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingInProgressTitle"
defaultMessage="Reindexing in progress… {percents}"
values={{
percents: getReindexProgressLabel(
@ -391,7 +391,7 @@ export const ReindexProgress: React.FunctionComponent<Props> = (props) => {
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingChecklistTitle"
defaultMessage="Reindexing process"
/>
)}

View file

@ -9,12 +9,12 @@ import { shallow } from 'enzyme';
import { cloneDeep } from 'lodash';
import React from 'react';
import { ReindexStatus } from '../../../../../../../common/types';
import { LoadingState } from '../../../../types';
import type { ReindexState } from '../use_reindex_state';
import { ChecklistFlyoutStep } from './checklist_step';
import { ReindexStatus } from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex';
import { ReindexFlyoutStep } from './reindex_step';
jest.mock('../../../../../app_context', () => {
jest.mock('../../../../../../../app_context', () => {
const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks');
return {
@ -33,10 +33,9 @@ jest.mock('../../../../../app_context', () => {
};
});
describe('ChecklistFlyout', () => {
describe('ReindexStep', () => {
const defaultProps = {
indexName: 'myIndex',
frozen: false,
closeFlyout: jest.fn(),
confirmInputValue: 'CONFIRM',
onConfirmInputChange: jest.fn(),
@ -60,31 +59,35 @@ describe('ChecklistFlyout', () => {
indexName: 'myIndex',
reindexName: 'reindexed-myIndex',
aliases: [],
isReadonly: false,
isFrozen: false,
isInDataStream: false,
isClosedIndex: false,
},
} as ReindexState,
};
it('renders', () => {
expect(shallow(<ChecklistFlyoutStep {...defaultProps} />)).toMatchSnapshot();
expect(shallow(<ReindexFlyoutStep {...defaultProps} />)).toMatchSnapshot();
});
it('renders for frozen indices', () => {
const props = cloneDeep(defaultProps);
props.frozen = true;
expect(shallow(<ChecklistFlyoutStep {...props} />)).toMatchSnapshot();
props.reindexState.meta.isFrozen = true;
expect(shallow(<ReindexFlyoutStep {...props} />)).toMatchSnapshot();
});
it('disables button while reindexing', () => {
const props = cloneDeep(defaultProps);
props.reindexState.status = ReindexStatus.inProgress;
const wrapper = shallow(<ChecklistFlyoutStep {...props} />);
const wrapper = shallow(<ReindexFlyoutStep {...props} />);
expect((wrapper.find('EuiButton').props() as any).isLoading).toBe(true);
});
it('hides button if hasRequiredPrivileges is false', () => {
const props = cloneDeep(defaultProps);
props.reindexState.hasRequiredPrivileges = false;
const wrapper = shallow(<ChecklistFlyoutStep {...props} />);
const wrapper = shallow(<ReindexFlyoutStep {...props} />);
expect(wrapper.exists('EuiButton')).toBe(false);
});
@ -92,24 +95,30 @@ describe('ChecklistFlyout', () => {
const props = cloneDeep(defaultProps);
props.reindexState.status = ReindexStatus.fetchFailed;
props.reindexState.errorMessage = 'Index not found';
const wrapper = shallow(<ChecklistFlyoutStep {...props} />);
const wrapper = shallow(<ReindexFlyoutStep {...props} />);
expect(wrapper.exists('EuiButton')).toBe(false);
});
it('shows get status error callout', () => {
it('shows fetch failed error callout', () => {
const props = cloneDeep(defaultProps);
props.reindexState.status = ReindexStatus.fetchFailed;
props.reindexState.errorMessage = 'Index not found';
const wrapper = shallow(<ChecklistFlyoutStep {...props} />);
expect(wrapper.exists('[data-test-subj="fetchFailedCallout"]')).toBe(true);
const wrapper = shallow(<ReindexFlyoutStep {...props} />);
expect(wrapper.find('FetchFailedCallOut').exists()).toBe(true);
expect(wrapper.find('FetchFailedCallOut').props()).toEqual({
errorMessage: 'Index not found',
});
});
it('shows reindexing callout', () => {
const props = cloneDeep(defaultProps);
props.reindexState.status = ReindexStatus.failed;
props.reindexState.errorMessage = 'Index not found';
const wrapper = shallow(<ChecklistFlyoutStep {...props} />);
expect(wrapper.exists('[data-test-subj="reindexingFailedCallout"]')).toBe(true);
props.reindexState.errorMessage = 'Reindex failed';
const wrapper = shallow(<ReindexFlyoutStep {...props} />);
expect(wrapper.find('ReindexingFailedCallOut').exists()).toBe(true);
expect(wrapper.find('ReindexingFailedCallOut').props()).toEqual({
errorMessage: 'Reindex failed',
});
});
it('calls startReindex when button is clicked', () => {
@ -121,7 +130,7 @@ describe('ChecklistFlyout', () => {
status: undefined,
},
};
const wrapper = shallow(<ChecklistFlyoutStep {...props} />);
const wrapper = shallow(<ReindexFlyoutStep {...props} />);
wrapper.find('EuiButton').simulate('click');
expect(props.startReindex).toHaveBeenCalled();

View file

@ -22,46 +22,49 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { ReindexStatus } from '../../../../../../../common/types';
import { LoadingState } from '../../../../types';
import type { ReindexState } from '../use_reindex_state';
import { ReindexStatus } from '../../../../../../../../../common/types';
import { LoadingState } from '../../../../../../types';
import type { ReindexState } from '../../../use_reindex';
import { ReindexProgress } from './progress';
import { useAppContext } from '../../../../../app_context';
import { useAppContext } from '../../../../../../../app_context';
import { FrozenCallOut } from '../frozen_callout';
import { FetchFailedCallOut } from '../fetch_failed_callout';
import { ReindexingFailedCallOut } from '../reindexing_failed_callout';
const buttonLabel = (status?: ReindexStatus) => {
switch (status) {
case ReindexStatus.failed:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.tryAgainLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.tryAgainLabel"
defaultMessage="Try again"
/>
);
case ReindexStatus.inProgress:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.reindexingLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.reindexingLabel"
defaultMessage="Reindexing…"
/>
);
case ReindexStatus.paused:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.resumeLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.resumeLabel"
defaultMessage="Resume reindexing"
/>
);
case ReindexStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.restartLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.restartLabel"
defaultMessage="Restart reindexing"
/>
);
default:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexButton.runReindexLabel"
defaultMessage="Start reindexing"
/>
);
@ -71,13 +74,12 @@ const buttonLabel = (status?: ReindexStatus) => {
/**
* Displays a flyout that shows the current reindexing status for a given index.
*/
export const ChecklistFlyoutStep: React.FunctionComponent<{
frozen?: boolean;
export const ReindexFlyoutStep: React.FunctionComponent<{
closeFlyout: () => void;
reindexState: ReindexState;
startReindex: () => void;
cancelReindex: () => void;
}> = ({ frozen, closeFlyout, reindexState, startReindex, cancelReindex }) => {
}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => {
const {
services: {
api,
@ -96,13 +98,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
return (
<Fragment>
<EuiFlyoutBody>
{reindexState.meta.isFrozen && <FrozenCallOut />}
{hasRequiredPrivileges === false && (
<Fragment>
<EuiSpacer />
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.insufficientPrivilegeCallout.calloutTitle"
defaultMessage="You do not have sufficient privileges to reindex this index"
/>
}
@ -111,7 +114,6 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
/>
</Fragment>
)}
{nodes && nodes.length > 0 && (
<>
<EuiCallOut
@ -120,14 +122,14 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
data-test-subj="lowDiskSpaceCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.lowDiskSpaceCalloutTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.lowDiskSpaceCalloutTitle"
defaultMessage="Nodes with low disk space"
/>
}
>
<>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.lowDiskSpaceCalloutDescription"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.lowDiskSpaceCalloutDescription"
defaultMessage="Disk usage has exceeded the low watermark, which may prevent reindexing. The following nodes are impacted:"
/>
@ -137,7 +139,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
{nodes.map(({ nodeName, available, nodeId }) => (
<li key={nodeId} data-test-subj="impactedNodeListItem">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.lowDiskSpaceUsedText"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.lowDiskSpaceUsedText"
defaultMessage="{nodeName} ({available} available)"
values={{
nodeName,
@ -152,43 +154,20 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
<EuiSpacer />
</>
)}
{(hasFetchFailed || hasReindexingFailed) && (
<>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'reindexingFailedCallout'}
title={
hasFetchFailed ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.fetchFailedCalloutTitle"
defaultMessage="Reindex status not available"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingFailedCalloutTitle"
defaultMessage="Reindexing error"
/>
)
}
>
{reindexState.errorMessage}
</EuiCallOut>
<EuiSpacer />
</>
{hasFetchFailed && <FetchFailedCallOut errorMessage={reindexState.errorMessage!} />}
{!hasFetchFailed && hasReindexingFailed && (
<ReindexingFailedCallOut errorMessage={reindexState.errorMessage!} />
)}
<EuiText>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexDescription"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexDescription"
defaultMessage="The index will be read-only during reindexing. You won't be able to add, update, or delete documents until reindexing is complete. If you need to reindex to a new cluster, use the reindex API. {docsLink}"
values={{
docsLink: (
<EuiLink target="_blank" href={docLinks.links.upgradeAssistant.remoteReindex}>
{i18n.translate(
'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel',
'xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
@ -198,40 +177,9 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
}}
/>
</p>
{frozen && (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexFrozenIndexTitle"
defaultMessage="This index is frozen"
/>
}
iconType="iInCircle"
>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexFrozenIndex"
defaultMessage="Frozen indices will no longer be supported after upgrade, so this index will be deleted as part the reindex operation. {docsLink}"
values={{
docsLink: (
<EuiLink target="_blank" href={docLinks.links.upgradeAssistant.unfreezeApi}>
{i18n.translate(
'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
),
}}
/>
</EuiCallOut>
<EuiSpacer />
</>
)}
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.readonlyCallout.backgroundResumeDetail"
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress or resume reindexing after a Kibana restart."
/>
</p>
@ -244,7 +192,7 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>

View file

@ -0,0 +1,36 @@
/*
* 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';
import { FormattedMessage } from '@kbn/i18n-react';
interface Props {
errorMessage: string;
}
export const ReindexingFailedCallOut: React.FunctionComponent<Props> = (props) => {
const { errorMessage } = props;
return (
<Fragment>
<EuiCallOut
color="danger"
iconType="warning"
data-test-subj="reindexingFailedCallout"
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.reindexStep.reindexingFailedCalloutTitle"
defaultMessage="Reindexing error"
/>
}
>
{errorMessage}
</EuiCallOut>
<EuiSpacer />
</Fragment>
);
};

View file

@ -0,0 +1,15 @@
/*
* 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 type FlyoutStep =
| 'details'
| 'confirmReadonly'
| 'confirmReindex'
| 'reindexing'
| 'makeReadonly'
| 'unfreeze'
| 'completed';

View file

@ -0,0 +1,169 @@
/*
* 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 { shallow } from 'enzyme';
import React from 'react';
import { UpdateIndexFlyoutStep } from './update_step';
import type { ReindexState } from '../../../use_reindex';
import type { UpdateIndexState } from '../../../use_update_index';
describe('UpdateIndexFlyoutStep', () => {
const meta: ReindexState['meta'] = {
indexName: 'some_index',
aliases: [],
isInDataStream: false,
isFrozen: false,
isReadonly: false,
isClosedIndex: false,
reindexName: 'some_index-reindexed-for-9',
};
const defaultUpdateIndexState: UpdateIndexState = {
status: 'incomplete',
failedBefore: false,
};
it('renders makeReadonly operation', () => {
const wrapper = shallow(
<UpdateIndexFlyoutStep
action="makeReadonly"
closeFlyout={jest.fn()}
meta={meta}
retry={jest.fn()}
updateIndexState={defaultUpdateIndexState}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<EuiFlyoutBody>
<EuiTitle
size="xs"
>
<h3>
<MemoizedFormattedMessage
defaultMessage="Upgrade in progress…"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.title.updateInProgressText"
/>
</h3>
</EuiTitle>
<EuiSpacer />
<StepProgress
steps={
Array [
Object {
"status": "incomplete",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Setting {indexName} index to read-only."
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.step.readonlyStepText"
values={
Object {
"indexName": <EuiCode>
some_index
</EuiCode>,
}
}
/>,
},
]
}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="left"
iconType="cross"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
`);
});
it('renders unfreeze operation', () => {
const wrapper = shallow(
<UpdateIndexFlyoutStep
action="unfreeze"
closeFlyout={jest.fn()}
meta={meta}
retry={jest.fn()}
updateIndexState={defaultUpdateIndexState}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<EuiFlyoutBody>
<EuiTitle
size="xs"
>
<h3>
<MemoizedFormattedMessage
defaultMessage="Upgrade in progress…"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.title.updateInProgressText"
/>
</h3>
</EuiTitle>
<EuiSpacer />
<StepProgress
steps={
Array [
Object {
"status": "incomplete",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Unfreezing {indexName} index."
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.step.unfreezeStepText"
values={
Object {
"indexName": <EuiCode>
some_index
</EuiCode>,
}
}
/>,
},
]
}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="left"
iconType="cross"
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
`);
});
});

View file

@ -0,0 +1,143 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiCode,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { UpdateIndexState } from '../../../use_update_index';
import { FrozenCallOut } from '../frozen_callout';
import { StepProgress, type StepProgressStep } from '../../../../../common/step_progress';
import type { ReindexState } from '../../../use_reindex';
interface UpdateIndexFlyoutStepProps {
action: 'unfreeze' | 'makeReadonly';
closeFlyout: () => void;
meta: ReindexState['meta'];
updateIndexState: UpdateIndexState;
retry: () => void;
}
const ErrorCallout: React.FunctionComponent<{ reason: string }> = ({ reason }) => (
<EuiCallOut color="danger" title="There was an error">
<EuiText>
<p>{reason}</p>
</EuiText>
</EuiCallOut>
);
/**
* In charge of rendering the result of the make read-only calls
*/
export const UpdateIndexFlyoutStep: React.FunctionComponent<UpdateIndexFlyoutStepProps> = ({
action,
closeFlyout,
meta,
updateIndexState,
retry,
}) => {
const { isFrozen, indexName } = meta;
const { status, failedBefore, reason } = updateIndexState;
const title =
action === 'makeReadonly' ? (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.step.readonlyStepText"
defaultMessage="Setting {indexName} index to read-only."
values={{
indexName: <EuiCode>{indexName}</EuiCode>,
}}
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.step.unfreezeStepText"
defaultMessage="Unfreezing {indexName} index."
values={{
indexName: <EuiCode>{indexName}</EuiCode>,
}}
/>
);
const steps: StepProgressStep[] = [
{
title,
status,
...(reason && { children: <ErrorCallout {...{ reason }} /> }),
},
];
return (
<Fragment>
<EuiFlyoutBody>
{isFrozen && <FrozenCallOut />}
<EuiTitle size="xs">
<h3>
{(status === 'inProgress' || status === 'incomplete') && (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.title.updateInProgressText"
defaultMessage="Upgrade in progress…"
/>
)}
{status === 'complete' && (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.title.updateCompleteText"
defaultMessage="Operation completed"
/>
)}
{status === 'failed' && (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.checklist.title.updateFailedText"
defaultMessage="Operation failed"
/>
)}
</h3>
</EuiTitle>
<EuiSpacer />
<StepProgress steps={steps} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{status !== 'complete' && failedBefore && (
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={retry}
isLoading={status === 'inProgress'}
disabled={status === 'inProgress'}
data-test-subj="startIndexReindexingButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.updateStep.retryButtonLabel"
defaultMessage="Retry"
/>
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>
);
};

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WarningsFlyoutStep renders 1`] = `
exports[`WarningFlyoutStep renders 1`] = `
<Fragment>
<EuiFlyoutBody />
<EuiFlyoutFooter>
@ -17,7 +17,7 @@ exports[`WarningsFlyoutStep renders 1`] = `
>
<MemoizedFormattedMessage
defaultMessage="Back"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.backButtonLabel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
@ -26,13 +26,14 @@ exports[`WarningsFlyoutStep renders 1`] = `
>
<EuiButton
color="primary"
data-test-subj="startReindexingButton"
disabled={false}
fill={true}
onClick={[MockFunction]}
>
<MemoizedFormattedMessage
defaultMessage="Continue reindexing"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.reindex.continueButtonLabel"
/>
</EuiButton>
</EuiFlexItem>

View file

@ -10,12 +10,12 @@ import { mount, shallow } from 'enzyme';
import React from 'react';
import SemVer from 'semver/classes/semver';
import { ReindexWarning } from '../../../../../../../common/types';
import { idForWarning, WarningsFlyoutStep } from './warnings_step';
import { idForWarning, WarningFlyoutStep } from './warning_step';
import { IndexWarning } from '../../../../../../../../../common/types';
const kibanaVersion = new SemVer('8.0.0');
jest.mock('../../../../../app_context', () => {
jest.mock('../../../../../../../app_context', () => {
const { docLinksServiceMock } = jest.requireActual('@kbn/core-doc-links-browser-mocks');
return {
@ -31,20 +31,24 @@ jest.mock('../../../../../app_context', () => {
};
});
describe('WarningsFlyoutStep', () => {
describe('WarningFlyoutStep', () => {
const defaultProps = {
warnings: [] as ReindexWarning[],
hideWarningsStep: jest.fn(),
continueReindex: jest.fn(),
warnings: [] as IndexWarning[],
back: jest.fn(),
confirm: jest.fn(),
flow: 'reindex' as const,
meta: {
indexName: 'foo',
reindexName: 'reindexed-foo',
aliases: [],
isFrozen: false,
isReadonly: false,
isInDataStream: false,
},
};
it('renders', () => {
expect(shallow(<WarningsFlyoutStep {...defaultProps} />)).toMatchSnapshot();
expect(shallow(<WarningFlyoutStep {...defaultProps} />)).toMatchSnapshot();
});
if (kibanaVersion.major === 7) {
@ -53,28 +57,29 @@ describe('WarningsFlyoutStep', () => {
...defaultProps,
warnings: [
{
flow: 'all' as const,
warningType: 'indexSetting',
meta: {
deprecatedSettings: ['index.force_memory_term_dictionary'],
},
},
] as ReindexWarning[],
] as IndexWarning[],
};
const wrapper = mount(
<I18nProvider>
<WarningsFlyoutStep {...defaultPropsWithWarnings} />
<WarningFlyoutStep {...defaultPropsWithWarnings} />
</I18nProvider>
);
const button = wrapper.find('EuiButton');
button.simulate('click');
expect(defaultPropsWithWarnings.continueReindex).not.toHaveBeenCalled();
expect(defaultPropsWithWarnings.confirm).not.toHaveBeenCalled();
// first warning (indexSetting)
wrapper.find(`input#${idForWarning(1)}`).simulate('change');
button.simulate('click');
expect(defaultPropsWithWarnings.continueReindex).toHaveBeenCalled();
expect(defaultPropsWithWarnings.confirm).toHaveBeenCalled();
});
}
});

View file

@ -0,0 +1,213 @@
/*
* 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, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
IndexWarning,
IndexWarningType,
ReindexStatusResponse,
} from '../../../../../../../../../common/types';
import { useAppContext } from '../../../../../../../app_context';
import {
DeprecatedSettingWarningCheckbox,
ReplaceIndexWithAliasWarningCheckbox,
MakeIndexReadonlyWarningCheckbox,
WarningCheckboxProps,
} from './warning_step_checkbox';
import { FrozenCallOut } from '../frozen_callout';
interface CheckedIds {
[id: string]: boolean;
}
const warningToComponentMap: {
[key in IndexWarningType]: React.FunctionComponent<WarningCheckboxProps>;
} = {
indexSetting: DeprecatedSettingWarningCheckbox,
replaceIndexWithAlias: ReplaceIndexWithAliasWarningCheckbox,
makeIndexReadonly: MakeIndexReadonlyWarningCheckbox,
};
export const idForWarning = (id: number) => `reindexWarning-${id}`;
interface WarningFlyoutStepProps {
back: () => void;
confirm: () => void;
flow: 'readonly' | 'reindex';
meta: ReindexStatusResponse['meta'];
warnings: IndexWarning[];
}
/**
* Displays warning text about destructive changes required to reindex this index. The user
* must acknowledge each change before being allowed to proceed.
*/
export const WarningFlyoutStep: React.FunctionComponent<WarningFlyoutStepProps> = ({
back,
confirm,
flow,
meta,
warnings,
}) => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
const { links } = docLinks;
const [checkedIds, setCheckedIds] = useState<CheckedIds>(
warnings.reduce((initialCheckedIds, warning, index) => {
initialCheckedIds[idForWarning(index)] = false;
return initialCheckedIds;
}, {} as { [id: string]: boolean })
);
// Do not allow to proceed until all checkboxes are checked.
const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const optionId = e.target.id;
setCheckedIds((prev) => ({
...prev,
...{
[optionId]: !checkedIds[optionId],
},
}));
};
return (
<>
<EuiFlyoutBody>
{meta.isFrozen && <FrozenCallOut />}
{warnings.length > 0 && (
<>
{flow === 'reindex' && (
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.reindex.calloutTitle"
defaultMessage="This index requires destructive changes that cannot be reversed"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.reindex.calloutDetail"
defaultMessage="Back up the index before continuing. To proceed with the reindex, accept each change."
/>
</p>
</EuiCallOut>
)}
{flow === 'readonly' && (
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.readonly.calloutTitle"
defaultMessage="Enable compatibility by marking this index as read-only"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.readonly.calloutDetail"
defaultMessage="You can enable compatibility with the next version by marking the index as read-only. Note that any attempts to insert new documents or update existing ones will fail. You can choose to reindex after upgrading if needed, to convert the index into a writable one."
/>
</p>
</EuiCallOut>
)}
<EuiSpacer />
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.acceptChangesTitle"
defaultMessage="Accept changes"
/>
</h3>
</EuiTitle>
<EuiSpacer />
{warnings.map((warning, index) => {
const WarningCheckbox = warningToComponentMap[warning.warningType];
return (
<WarningCheckbox
key={idForWarning(index)}
isChecked={checkedIds[idForWarning(index)]}
onChange={onChange}
docLinks={links}
id={idForWarning(index)}
meta={{ ...meta, ...warning.meta }}
/>
);
})}
</>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="arrowLeft" onClick={back} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.backButtonLabel"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{flow === 'reindex' && (
<EuiButton
fill
color="primary"
onClick={confirm}
disabled={blockAdvance}
data-test-subj="startReindexingButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.reindex.continueButtonLabel"
defaultMessage="Continue reindexing"
/>
</EuiButton>
)}
{flow === 'readonly' && (
<EuiButton
fill
color="primary"
onClick={confirm}
disabled={blockAdvance}
data-test-subj="startIndexReadonlyButton"
>
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.readonly.continueButtonLabel"
data-test-subj="startIndexReadonlyButton"
defaultMessage="Mark as read-only"
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

View file

@ -19,11 +19,11 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLinksStart } from '@kbn/core/public';
import { ReindexWarning, ReindexWarningTypes } from '../../../../../../../common/types';
import { IndexWarning, IndexWarningType } from '../../../../../../../../../common/types';
export const hasReindexWarning = (
warnings: ReindexWarning[],
warningType: ReindexWarningTypes
export const hasIndexWarning = (
warnings: IndexWarning[],
warningType: IndexWarningType
): boolean => {
return Boolean(warnings.find((warning) => warning.warningType === warningType));
};
@ -53,7 +53,7 @@ const WarningCheckbox: React.FunctionComponent<{
<EuiIconTip
content={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.documentationLinkLabel"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.documentationLinkLabel"
defaultMessage="Documentation"
/>
}
@ -79,7 +79,7 @@ export interface WarningCheckboxProps {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
docLinks: DocLinksStart['links'];
id: string;
meta?: ReindexWarning['meta'];
meta?: IndexWarning['meta'];
}
export const DeprecatedSettingWarningCheckbox: React.FunctionComponent<WarningCheckboxProps> = ({
@ -96,14 +96,14 @@ export const DeprecatedSettingWarningCheckbox: React.FunctionComponent<WarningCh
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.deprecatedIndexSettingsWarningTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.deprecatedIndexSettingsWarningTitle"
defaultMessage="Remove deprecated index settings"
/>
}
description={
<>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.deprecatedIndexSettingsWarningDetail"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.deprecatedIndexSettingsWarningDetail"
defaultMessage="The following deprecated index settings were detected:"
/>
@ -135,7 +135,7 @@ export const ReplaceIndexWithAliasWarningCheckbox: React.FunctionComponent<
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.replaceIndexWithAliasWarningTitle"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.replaceIndexWithAliasWarningTitle"
defaultMessage="Replace {indexName} index with {reindexName} index and create {indexName} index alias"
values={{
indexName: <EuiCode>{meta?.indexName}</EuiCode>,
@ -145,7 +145,7 @@ export const ReplaceIndexWithAliasWarningCheckbox: React.FunctionComponent<
}
description={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.replaceIndexWithAliasWarningDetail"
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.replaceIndexWithAliasWarningDetail"
defaultMessage="You can search {indexName} as before. To delete the data you'll have to delete {reindexName}"
values={{
indexName: <EuiCode>{meta?.indexName}</EuiCode>,
@ -156,3 +156,36 @@ export const ReplaceIndexWithAliasWarningCheckbox: React.FunctionComponent<
/>
);
};
export const MakeIndexReadonlyWarningCheckbox: React.FunctionComponent<WarningCheckboxProps> = ({
isChecked,
onChange,
id,
meta,
}) => {
return (
<WarningCheckbox
isChecked={isChecked}
onChange={onChange}
warningId={id}
label={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.makeIndexReadonlyWarningTitle"
defaultMessage="Flag {indexName} index as read-only"
values={{
indexName: <EuiCode>{meta?.indexName}</EuiCode>,
}}
/>
}
description={
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.indices.indexFlyout.warningsStep.makeIndexReadonlyWarningDetail"
defaultMessage="You can continue to search and retrieve documents from {indexName}. You will not be able to insert new documents or modify existing ones."
values={{
indexName: <EuiCode>{meta?.indexName}</EuiCode>,
}}
/>
}
/>
);
};

View file

@ -19,65 +19,89 @@ import {
import { ReindexStatus } from '../../../../../../common/types';
import { getReindexProgressLabel } from '../../../../lib/utils';
import { LoadingState } from '../../../types';
import { useReindexContext } from './context';
import { useIndexContext } from './context';
const i18nTexts = {
reindexLoadingStatusText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexLoadingStatusText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexLoadingStatusText',
{
defaultMessage: 'Loading status…',
}
),
reindexInProgressText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexInProgressText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexInProgressText',
{
defaultMessage: 'Reindexing in progress…',
}
),
reindexCompleteText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexCompleteText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexCompleteText',
{
defaultMessage: 'Reindex complete',
}
),
reindexFailedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexFailedText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexFailedText',
{
defaultMessage: 'Reindex failed',
}
),
reindexFetchFailedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexFetchFailedText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexFetchFailedText',
{
defaultMessage: 'Reindex status not available',
}
),
reindexCanceledText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexCanceledText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexCanceledText',
{
defaultMessage: 'Reindex cancelled',
}
),
reindexPausedText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.reindexPausedText',
'xpack.upgradeAssistant.esDeprecations.indices.reindexPausedText',
{
defaultMessage: 'Reindex paused',
}
),
resolutionText: i18n.translate('xpack.upgradeAssistant.esDeprecations.reindex.resolutionLabel', {
reindexText: i18n.translate('xpack.upgradeAssistant.esDeprecations.indices.reindexLabel', {
defaultMessage: 'Reindex',
}),
resolutionTooltipLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.resolutionTooltipLabel',
reindexTooltipLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.reindexTooltipLabel',
{
defaultMessage: 'Resolve this issue by reindexing into a new, compatible index.',
}
),
updateText: i18n.translate('xpack.upgradeAssistant.esDeprecations.indices.updateLabel', {
defaultMessage: 'Update',
}),
updateCompleteText: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.updateCompleteText',
{
defaultMessage: 'Update complete',
}
),
updateTooltipLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.updateTooltipLabel',
{
defaultMessage:
'Resolve this issue by reindexing this index. This issue can be resolved automatically.',
'Resolve this issue by updating this index. This issue can be resolved automatically either by marking the index as read-only (recommended for large indices) or by reindexing into a new, compatible index.',
}
),
unfreezeText: i18n.translate('xpack.upgradeAssistant.esDeprecations.indices.unfreezeLabel', {
defaultMessage: 'Unfreeze',
}),
unfreezeTooltipLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.indices.unfreezeTooltipLabel',
{
defaultMessage: 'Resolve this issue by unfreezing this index.',
}
),
};
export const ReindexResolutionCell: React.FunctionComponent = () => {
const { reindexState } = useReindexContext();
const { reindexState, deprecation, updateIndexState } = useIndexContext();
const hasExistingAliases = reindexState.meta.aliases.length > 0;
if (reindexState.loadingState === LoadingState.Loading) {
@ -158,14 +182,51 @@ export const ReindexResolutionCell: React.FunctionComponent = () => {
);
}
return (
<EuiToolTip position="top" content={i18nTexts.resolutionTooltipLabel}>
switch (updateIndexState.status) {
case 'complete':
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="check" color="success" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.updateCompleteText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
// reindex status "not started"
return deprecation.correctiveAction?.type === 'unfreeze' ? (
<EuiToolTip position="top" content={i18nTexts.unfreezeTooltipLabel}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="indexSettings" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.resolutionText}</EuiText>
<EuiText size="s">{i18nTexts.unfreezeText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
) : reindexState.meta.isReadonly ? (
<EuiToolTip position="top" content={i18nTexts.reindexTooltipLabel}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="indexSettings" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>
) : (
<EuiToolTip position="top" content={i18nTexts.updateTooltipLabel}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="indexSettings" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.updateText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiToolTip>

View file

@ -19,8 +19,8 @@ import {
import { DeprecationTableColumns } from '../../../types';
import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells';
import { ReindexResolutionCell } from './resolution_table_cell';
import { ReindexFlyout, ReindexFlyoutProps } from './flyout';
import { ReindexStatusProvider, useReindexContext } from './context';
import { IndexFlyout, IndexFlyoutProps } from './flyout';
import { IndexStatusProvider, useIndexContext } from './context';
const { useGlobalFlyout } = GlobalFlyout;
@ -29,31 +29,30 @@ interface TableRowProps {
rowFieldNames: DeprecationTableColumns[];
}
const ReindexTableRowCells: React.FunctionComponent<TableRowProps> = ({
const IndexTableRowCells: React.FunctionComponent<TableRowProps> = ({
rowFieldNames,
deprecation,
}) => {
const [showFlyout, setShowFlyout] = useState(false);
const reindexState = useReindexContext();
const indexContext = useIndexContext();
const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } =
useGlobalFlyout();
const closeFlyout = useCallback(async () => {
removeContentFromGlobalFlyout('reindexFlyout');
removeContentFromGlobalFlyout('indexFlyout');
setShowFlyout(false);
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_CLOSE_FLYOUT_CLICK);
}, [removeContentFromGlobalFlyout]);
useEffect(() => {
if (showFlyout) {
addContentToGlobalFlyout<ReindexFlyoutProps>({
id: 'reindexFlyout',
Component: ReindexFlyout,
addContentToGlobalFlyout<IndexFlyoutProps>({
id: 'indexFlyout',
Component: IndexFlyout,
props: {
deprecation,
closeFlyout,
...reindexState,
...indexContext,
},
flyoutProps: {
onClose: closeFlyout,
@ -63,7 +62,7 @@ const ReindexTableRowCells: React.FunctionComponent<TableRowProps> = ({
},
});
}
}, [addContentToGlobalFlyout, deprecation, showFlyout, reindexState, closeFlyout]);
}, [addContentToGlobalFlyout, deprecation, showFlyout, indexContext, closeFlyout]);
useEffect(() => {
if (showFlyout) {
@ -93,14 +92,14 @@ const ReindexTableRowCells: React.FunctionComponent<TableRowProps> = ({
);
};
export const ReindexTableRow: React.FunctionComponent<TableRowProps> = (props) => {
export const IndexTableRow: React.FunctionComponent<TableRowProps> = (props) => {
const {
services: { api },
} = useAppContext();
return (
<ReindexStatusProvider indexName={props.deprecation.index!} api={api}>
<ReindexTableRowCells {...props} />
</ReindexStatusProvider>
<IndexStatusProvider deprecation={props.deprecation} api={api}>
<IndexTableRowCells {...props} />
</IndexStatusProvider>
);
};

View file

@ -11,7 +11,7 @@ import {
ReindexStatusResponse,
ReindexStatus,
ReindexStep,
ReindexWarning,
IndexWarning,
} from '../../../../../../common/types';
import { CancelLoadingState, LoadingState } from '../../../types';
import { ApiService } from '../../../../lib/api';
@ -25,20 +25,24 @@ export interface ReindexState {
status?: ReindexStatus;
reindexTaskPercComplete: number | null;
errorMessage: string | null;
reindexWarnings?: ReindexWarning[];
reindexWarnings?: IndexWarning[];
hasRequiredPrivileges?: boolean;
meta: {
indexName: string;
reindexName: string;
aliases: string[];
isFrozen: boolean;
isReadonly: boolean;
isInDataStream: boolean;
isClosedIndex: boolean;
};
}
const getReindexState = (
reindexState: ReindexState,
{ reindexOp, warnings, hasRequiredPrivileges, meta: updatedMeta }: ReindexStatusResponse
) => {
const meta = { ...(updatedMeta ?? reindexState.meta) };
): ReindexState => {
const meta = { ...reindexState.meta, ...updatedMeta };
// Once we have received an array of existing aliases, we won't update the meta value anymore because
// when we'll delete the original alias during the reindex process there won't be any aliases pointing
// to it anymore and the last reindex step (Update existing aliases) would be suddenly removed.
@ -49,7 +53,6 @@ const getReindexState = (
meta: { ...meta, aliases },
loadingState: LoadingState.Success,
};
if (warnings) {
newReindexState.reindexWarnings = warnings;
}
@ -107,15 +110,32 @@ const getReindexState = (
return newReindexState;
};
export const useReindexStatus = ({ indexName, api }: { indexName: string; api: ApiService }) => {
export const useReindex = ({
indexName,
isFrozen,
isInDataStream,
isClosedIndex,
api,
}: {
indexName: string;
isFrozen: boolean;
isInDataStream: boolean;
isClosedIndex: boolean;
api: ApiService;
}) => {
const [reindexState, setReindexState] = useState<ReindexState>({
loadingState: LoadingState.Loading,
errorMessage: null,
reindexTaskPercComplete: null,
meta: {
indexName,
reindexName: '', // will be known after fetching the reindexStatus
aliases: [], // will be known after fetching the reindexStatus
// these properties will be known after fetching the reindexStatus
reindexName: '',
aliases: [],
isFrozen,
isInDataStream,
isClosedIndex,
isReadonly: false, // we don't have this information in the deprecation list
},
});

View file

@ -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 { useCallback, useState } from 'react';
import type { UpdateIndexOperation } from '../../../../../../common/update_index';
import type { CorrectiveAction } from '../../../../../../common/types';
import type { ApiService } from '../../../../lib/api';
export interface UpdateIndexState {
failedBefore: boolean;
status: 'incomplete' | 'inProgress' | 'complete' | 'failed';
reason?: string;
}
export interface UseUpdateIndexParams {
indexName: string;
api: ApiService;
correctiveAction?: CorrectiveAction;
}
export const useUpdateIndex = ({ indexName, api, correctiveAction }: UseUpdateIndexParams) => {
const [failedState, setFailedState] = useState<boolean>(false);
const [updateIndexState, setUpdateIndexState] = useState<UpdateIndexState>({
failedBefore: false,
status: 'incomplete',
});
const updateIndex = useCallback(async () => {
const operations: UpdateIndexOperation[] =
correctiveAction?.type === 'unfreeze' ? ['unfreeze'] : ['blockWrite', 'unfreeze'];
setUpdateIndexState({ status: 'inProgress', failedBefore: failedState });
const res = await api.updateIndex(indexName, operations);
const status = res.error ? 'failed' : 'complete';
const failedBefore = failedState || status === 'failed';
setFailedState(failedBefore);
setUpdateIndexState({
status,
failedBefore,
...(res.error && { reason: res.error.message.toString() }),
});
}, [api, correctiveAction, failedState, indexName]);
return {
updateIndexState,
updateIndex,
};
};

View file

@ -1,56 +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, { createContext, useContext } from 'react';
import { ApiService } from '../../../../lib/api';
import { useReindexStatus, ReindexState } from './use_reindex_state';
export interface ReindexStateContext {
reindexState: ReindexState;
startReindex: () => Promise<void>;
cancelReindex: () => Promise<void>;
}
const ReindexContext = createContext<ReindexStateContext | undefined>(undefined);
export const useReindexContext = () => {
const context = useContext(ReindexContext);
if (context === undefined) {
throw new Error('useReindexContext must be used within a <ReindexStatusProvider />');
}
return context;
};
interface Props {
api: ApiService;
children: React.ReactNode;
indexName: string;
}
export const ReindexStatusProvider: React.FunctionComponent<Props> = ({
api,
indexName,
children,
}) => {
const { reindexState, startReindex, cancelReindex } = useReindexStatus({
indexName,
api,
});
return (
<ReindexContext.Provider
value={{
reindexState,
startReindex,
cancelReindex,
}}
>
{children}
</ReindexContext.Provider>
);
};

View file

@ -1,106 +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, { useCallback, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types';
import type { ReindexStateContext } from '../context';
import { ChecklistFlyoutStep } from './checklist_step';
import { WarningsFlyoutStep } from './warnings_step';
import { DeprecationBadge } from '../../../../shared';
import {
UIM_REINDEX_START_CLICK,
UIM_REINDEX_STOP_CLICK,
uiMetricService,
} from '../../../../../lib/ui_metric';
export interface ReindexFlyoutProps extends ReindexStateContext {
deprecation: EnrichedDeprecationInfo;
closeFlyout: () => void;
}
export const ReindexFlyout: React.FunctionComponent<ReindexFlyoutProps> = ({
reindexState,
startReindex,
cancelReindex,
closeFlyout,
deprecation,
}) => {
const { status, reindexWarnings } = reindexState;
const { index } = deprecation;
const [showWarningsStep, setShowWarningsStep] = useState(false);
const onStartReindex = useCallback(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK);
startReindex();
}, [startReindex]);
const onStopReindex = useCallback(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK);
cancelReindex();
}, [cancelReindex]);
const startReindexWithWarnings = () => {
if (
reindexWarnings &&
reindexWarnings.length > 0 &&
status !== ReindexStatus.inProgress &&
status !== ReindexStatus.completed
) {
setShowWarningsStep(true);
} else {
onStartReindex();
}
};
const flyoutContents = showWarningsStep ? (
<WarningsFlyoutStep
warnings={reindexState.reindexWarnings ?? []}
meta={reindexState.meta}
hideWarningsStep={() => setShowWarningsStep(false)}
continueReindex={() => {
setShowWarningsStep(false);
onStartReindex();
}}
/>
) : (
<ChecklistFlyoutStep
frozen={deprecation.frozen}
closeFlyout={closeFlyout}
startReindex={startReindexWithWarnings}
reindexState={reindexState}
cancelReindex={onStopReindex}
/>
);
return (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge
isCritical={deprecation.isCritical}
isResolved={status === ReindexStatus.completed}
/>
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2 id="reindexDetailsFlyoutTitle">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.flyoutHeader"
defaultMessage="Reindex {index}"
values={{ index }}
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
{flyoutContents}
</>
);
};

View file

@ -1,162 +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, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
ReindexWarning,
ReindexWarningTypes,
ReindexStatusResponse,
} from '../../../../../../../common/types';
import { useAppContext } from '../../../../../app_context';
import {
DeprecatedSettingWarningCheckbox,
ReplaceIndexWithAliasWarningCheckbox,
WarningCheckboxProps,
} from './warning_step_checkbox';
interface CheckedIds {
[id: string]: boolean;
}
const warningToComponentMap: {
[key in ReindexWarningTypes]: React.FunctionComponent<WarningCheckboxProps>;
} = {
indexSetting: DeprecatedSettingWarningCheckbox,
replaceIndexWithAlias: ReplaceIndexWithAliasWarningCheckbox,
};
export const idForWarning = (id: number) => `reindexWarning-${id}`;
interface WarningsConfirmationFlyoutProps {
hideWarningsStep: () => void;
continueReindex: () => void;
warnings: ReindexWarning[];
meta: ReindexStatusResponse['meta'];
}
/**
* Displays warning text about destructive changes required to reindex this index. The user
* must acknowledge each change before being allowed to proceed.
*/
export const WarningsFlyoutStep: React.FunctionComponent<WarningsConfirmationFlyoutProps> = ({
warnings,
hideWarningsStep,
continueReindex,
meta,
}) => {
const {
services: {
core: { docLinks },
},
} = useAppContext();
const { links } = docLinks;
const [checkedIds, setCheckedIds] = useState<CheckedIds>(
warnings.reduce((initialCheckedIds, warning, index) => {
initialCheckedIds[idForWarning(index)] = false;
return initialCheckedIds;
}, {} as { [id: string]: boolean })
);
// Do not allow to proceed until all checkboxes are checked.
const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length;
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const optionId = e.target.id;
setCheckedIds((prev) => ({
...prev,
...{
[optionId]: !checkedIds[optionId],
},
}));
};
return (
<>
<EuiFlyoutBody>
{warnings.length > 0 && (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle"
defaultMessage="This index requires destructive changes that cannot be reversed"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail"
defaultMessage="Back up the index before continuing. To proceed with the reindex, accept each change."
/>
</p>
</EuiCallOut>
<EuiSpacer />
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Accept changes"
/>
</h3>
</EuiTitle>
<EuiSpacer />
{warnings.map((warning, index) => {
const WarningCheckbox = warningToComponentMap[warning.warningType];
return (
<WarningCheckbox
key={idForWarning(index)}
isChecked={checkedIds[idForWarning(index)]}
onChange={onChange}
docLinks={links}
id={idForWarning(index)}
meta={{ ...meta, ...warning.meta }}
/>
);
})}
</>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="arrowLeft" onClick={hideWarningsStep} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill color="primary" onClick={continueReindex} disabled={blockAdvance}>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel"
defaultMessage="Continue reindexing"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

View file

@ -31,7 +31,7 @@ import {
MlSnapshotsTableRow,
DefaultTableRow,
IndexSettingsTableRow,
ReindexTableRow,
IndexTableRow,
ClusterSettingsTableRow,
HealthIndicatorTableRow,
DataStreamTableRow,
@ -126,7 +126,8 @@ const renderTableRowCells = (
return <ClusterSettingsTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
case 'reindex':
return <ReindexTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
case 'unfreeze':
return <IndexTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
case 'healthIndicator':
return <HealthIndicatorTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;

View file

@ -28,7 +28,7 @@ const i18nTexts = {
}
),
manualCellTooltipLabel: i18n.translate(
'xpack.upgradeAssistant.esDeprecations.reindex.manualCellTooltipLabel',
'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellTooltipLabel',
{
defaultMessage: 'This issue needs to be resolved manually.',
}

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import type { HttpSetup } from '@kbn/core/public';
import {
import type { UpdateIndexOperation } from '../../../common/update_index';
import type {
ESUpgradeStatus,
CloudBackupStatus,
ClusterUpgradeState,
@ -24,9 +25,9 @@ import {
CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS,
} from '../../../common/constants';
import {
UseRequestConfig,
SendRequestConfig,
SendRequestResponse,
type UseRequestConfig,
type SendRequestConfig,
type SendRequestResponse,
sendRequest as _sendRequest,
useRequest as _useRequest,
} from '../../shared_imports';
@ -211,41 +212,51 @@ export class ApiService {
});
}
public async getDataStreamReindexStatus(dataStreamName: string) {
/**
* Data Stream Migrations
* Reindex and readonly operations
*/
public async getDataStreamMigrationStatus(dataStreamName: string) {
return await this.sendRequest<DataStreamReindexStatusResponse>({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}`,
path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}`,
method: 'get',
});
}
public async getDataStreamMetadata(dataStreamName: string) {
return await this.sendRequest<DataStreamMetadata>({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/metadata`,
path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/metadata`,
method: 'get',
});
}
public async startDataStreamReindexTask(dataStreamName: string) {
return await this.sendRequest({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}`,
path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/reindex`,
method: 'post',
});
}
public async cancelDataStreamReindexTask(dataStreamName: string) {
return await this.sendRequest({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/cancel`,
path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/reindex/cancel`,
method: 'post',
});
}
public async pauseDataStreamReindexTask(dataStreamName: string) {
public async markIndicesAsReadOnly(dataStreamName: string, indices: string[]) {
return await this.sendRequest({
path: `${API_BASE_PATH}/reindex_data_streams/${dataStreamName}/pause`,
path: `${API_BASE_PATH}/migrate_data_stream/${dataStreamName}/readonly`,
method: 'post',
body: { indices },
});
}
/**
* FINISH: Data Stream Migrations
*/
public async getReindexStatus(indexName: string) {
return await this.sendRequest<ReindexStatusResponse>({
path: `${API_BASE_PATH}/reindex/${indexName}`,
@ -267,6 +278,14 @@ export class ApiService {
});
}
public async updateIndex(indexName: string, operations: UpdateIndexOperation[]) {
return await this.sendRequest({
path: `${API_BASE_PATH}/update_index/${indexName}`,
method: 'post',
body: { operations },
});
}
public useLoadUpgradeStatus() {
return this.useRequest<{
readyForUpgrade: boolean;

View file

@ -19,11 +19,18 @@ export const UIM_REINDEX_OPEN_FLYOUT_CLICK = 'reindex_open_flyout_click';
export const UIM_REINDEX_CLOSE_FLYOUT_CLICK = 'reindex_close_flyout_click';
export const UIM_REINDEX_START_CLICK = 'reindex_start_click';
export const UIM_REINDEX_STOP_CLICK = 'reindex_stop_click';
export const UIM_REINDEX_READONLY_CLICK = 'reindex_readonly_click';
export const UIM_REINDEX_UNFREEZE_CLICK = 'reindex_unfreeze_click';
export const UIM_REINDEX_READONLY_RETRY_CLICK = 'reindex_readonly_retry_click';
export const UIM_REINDEX_UNFREEZE_RETRY_CLICK = 'reindex_unfreeze_retry_click';
// Data Streams Reindexing
export const UIM_DATA_STREAM_REINDEX_OPEN_FLYOUT_CLICK = 'data_stream_reindex_open_flyout_click';
export const UIM_DATA_STREAM_REINDEX_CLOSE_FLYOUT_CLICK = 'data_stream_reindex_close_flyout_click';
export const UIM_DATA_STREAM_REINDEX_START_CLICK = 'data_stream_reindex_start_click';
export const UIM_DATA_STREAM_REINDEX_STOP_CLICK = 'data_stream_reindex_stop_click';
export const UIM_DATA_STREAM_START_READONLY_CLICK = 'data_stream_readonly_start_click';
export const UIM_DATA_STREAM_STOP_READONLY_CLICK = 'data_stream_readonly_stop_click';
export const UIM_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click';
export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click';

View file

@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { tryCatch, fold } from 'fp-ts/lib/Either';
import { DEPRECATION_WARNING_UPPER_LIMIT } from '../../../common/constants';
import { ReindexStep, DataStreamReindexStatus } from '../../../common/types';
import { ReindexStep, DataStreamMigrationStatus } from '../../../common/types';
export const validateRegExpString = (s: string) =>
pipe(
@ -103,20 +103,20 @@ export const getReindexProgressLabel = (
};
export const getDataStreamReindexProgress = (
status: DataStreamReindexStatus,
reindexTaskPercComplete: number | null
status: DataStreamMigrationStatus,
taskPercComplete: number | null
): number => {
switch (status) {
case DataStreamReindexStatus.notStarted:
case DataStreamMigrationStatus.notStarted:
return 0;
case DataStreamReindexStatus.fetchFailed:
case DataStreamReindexStatus.failed:
case DataStreamReindexStatus.cancelled:
case DataStreamReindexStatus.inProgress: {
return reindexTaskPercComplete !== null ? Math.round(reindexTaskPercComplete * 100) : 0;
case DataStreamMigrationStatus.fetchFailed:
case DataStreamMigrationStatus.failed:
case DataStreamMigrationStatus.cancelled:
case DataStreamMigrationStatus.inProgress: {
return taskPercComplete !== null ? Math.round(taskPercComplete * 100) : 0;
}
case DataStreamReindexStatus.completed: {
case DataStreamMigrationStatus.completed: {
return 100;
}
}
@ -125,9 +125,9 @@ export const getDataStreamReindexProgress = (
};
export const getDataStreamReindexProgressLabel = (
status: DataStreamReindexStatus,
reindexTaskPercComplete: number | null
status: DataStreamMigrationStatus,
taskPercComplete: number | null
): string => {
const percentsComplete = getDataStreamReindexProgress(status, reindexTaskPercComplete);
const percentsComplete = getDataStreamReindexProgress(status, taskPercComplete);
return `${percentsComplete}%`;
};

View file

@ -21,6 +21,22 @@ const configSchema = schema.object({
serverless: schema.boolean({ defaultValue: true }),
}),
/**
* Exlcude certain data streams or indices from getting certain correctiveActions.
* The key is the data source name or pattern and the value is an array of corrective actions to exclude.
*
* Exclude readOnly data sources from getting read-only corrective actions.
* This is needed to avoid breaking certain built-in/system functionality that might rely on writing to these data sources.
* Example (excludes read-only corrective actions for 7_17_data_stream):
* xpack.upgrade_assistant.dataSourceExclusions:
* 7_17_data_stream: ["readOnly"]
*/
dataSourceExclusions: schema.recordOf(
schema.string(),
schema.arrayOf(schema.oneOf([schema.literal('readOnly'), schema.literal('reindex')])),
{ defaultValue: {} }
),
featureSet: schema.object({
/**
* Ml Snapshot should only be enabled for major version upgrades. Currently this
@ -33,12 +49,12 @@ const configSchema = schema.object({
* to change the constant `MachineLearningField.MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION`
* to something higher than 7.0.0 in the Elasticsearch code.
*/
mlSnapshots: schema.boolean({ defaultValue: false }),
mlSnapshots: schema.boolean({ defaultValue: true }),
/**
* Migrating system indices should only be enabled for major version upgrades.
* Currently this is manually set to `true` on every `x.last` version.
*/
migrateSystemIndices: schema.boolean({ defaultValue: false }),
migrateSystemIndices: schema.boolean({ defaultValue: true }),
/**
* Deprecations with reindexing corrective actions are only enabled for major version upgrades.
* Currently this is manually set to `true` on every `x.last` version.
@ -46,12 +62,12 @@ const configSchema = schema.object({
* The reindex action includes some logic that is specific to the 8.0 upgrade
* End users could get into a bad situation if this is enabled before this logic is fixed.
*/
reindexCorrectiveActions: schema.boolean({ defaultValue: false }),
reindexCorrectiveActions: schema.boolean({ defaultValue: true }),
/**
* Migrating deprecated data streams should only be enabled for major version upgrades.
* Currently this is manually set to `true` on every `x.last` version.
*/
migrateDataStreams: schema.boolean({ defaultValue: false }),
migrateDataStreams: schema.boolean({ defaultValue: true }),
}),
/**
* This config allows to hide the UI without disabling the plugin.

View file

@ -67,10 +67,12 @@
{
"level": "critical",
"message": "Index created before 7.0",
"url":
"https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html",
"url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html",
"details": "This index was created using version: 6.8.13",
"resolve_during_rolling_upgrade": false
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true
}
}
],
"frozen_index": [
@ -98,17 +100,18 @@
"message": "Index created before 7.0",
"url": "https: //www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html",
"details": "This index was created using version: 6.8.13",
"resolve_during_rolling_upgrade": false
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true
}
}
],
"deprecated_settings": [
{
"level": "warning",
"message": "Setting [index.routing.allocation.include._tier] is deprecated",
"url":
"https://www.elastic.co/guide/en/elasticsearch/reference/7.16/migrating-7.13.html#deprecate-tier-filter-setting",
"details":
"Remove the [index.routing.allocation.include._tier] setting. Use [index.routing.allocation.include._tier_preference] to control allocation to data tiers.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/7.16/migrating-7.13.html#deprecate-tier-filter-setting",
"details": "Remove the [index.routing.allocation.include._tier] setting. Use [index.routing.allocation.include._tier_preference] to control allocation to data tiers.",
"resolve_during_rolling_upgrade": false,
"_meta": {
"actions": [
@ -167,41 +170,137 @@
"resolve_during_rolling_upgrade": false
}
],
"transforms_index": [
{
"level": "critical",
"message": "Old index with a compatibility version < 8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details": "This index has version: 7.17.25",
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true,
"transform_ids": ["abc"]
}
}
],
"myindex": [
{
"level": "critical",
"message": "Old index with a compatibility version < 8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details": "This index has version: 7.17.25",
"resolve_during_rolling_upgrade": false
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true
}
}
],
".ent-search-1": [
{
"level": "critical",
"message": "Old index with a compatibility version < 8.0",
"url": "https: //www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0",
"details": "This index has version: 7.17.28-8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true
}
}
],
".ent-search-2": [
{
"level": "critical",
"message": "Old index with a compatibility version < 8.0",
"url": "https: //www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0",
"details": "This index has version: 7.17.28-8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true
}
}
],
".ent-search-3": [
{
"level": "critical",
"message": "Old index with a compatibility version < 8.0",
"url": "https: //www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0",
"details": "This index has version: 7.17.28-8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"reindex_required": true
}
}
]
},
"data_streams": {
"my-v7-data-stream" : [{
"level" : "critical",
"message" : "Old data stream with a compatibility version < 8.0",
"url" : "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details" : "This data stream has backing indices that were created before Elasticsearch 8.0.0",
"resolve_during_rolling_upgrade" : false,
"_meta": {
"backing_indices": {
"count": 52,
"need_upgrading": {
"count": 37,
"searchable_snapshot": {
"count": 23,
"fully_mounted": {
"count": 7
},
"partially_mounted": {
"count": 16
}
}
}
"my-v7-data-stream": [
{
"level": "critical",
"message": "Old data stream with a compatibility version < 8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details": "This data stream has backing indices that were created before Elasticsearch 8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"indices_requiring_upgrade": [
".ds-some-backing-index-5-2024.11.07-000001"
],
"indices_requiring_upgrade_count": 1,
"total_backing_indices": 2,
"reindex_required": true
}
}
}]
],
"logs-enterprise_search.default": [
{
"level": "critical",
"message": "Old data stream with a compatibility version < 8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details": "This data stream has backing indices that were created before Elasticsearch 8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"indices_requiring_upgrade": [
".ds-some-backing-index-5-2024.11.07-000001"
],
"indices_requiring_upgrade_count": 1,
"total_backing_indices": 2,
"reindex_required": true
}
}
],
"logs-app_search.default": [
{
"level": "critical",
"message": "Old data stream with a compatibility version < 8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details": "This data stream has backing indices that were created before Elasticsearch 8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"indices_requiring_upgrade": [
".ds-some-backing-index-5-2024.11.07-000001"
],
"indices_requiring_upgrade_count": 1,
"total_backing_indices": 2,
"reindex_required": true
}
}
],
"logs-workplace_search.default": [
{
"level": "critical",
"message": "Old data stream with a compatibility version < 8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
"details": "This data stream has backing indices that were created before Elasticsearch 8.0.0",
"resolve_during_rolling_upgrade": false,
"_meta": {
"indices_requiring_upgrade": [
".ds-some-backing-index-5-2024.11.07-000001"
],
"indices_requiring_upgrade_count": 1,
"total_backing_indices": 2,
"reindex_required": true
}
}
]
},
"ilm_policies": {
"myfreezepolicy": [
@ -224,4 +323,4 @@
}
]
}
}
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { estypes } from '@elastic/elasticsearch';
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
export const diskIndicatorGreen: estypes.HealthReportDiskIndicator = {
status: 'green',

View file

@ -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 { matchExclusionPattern } from './data_source_exclusions';
import { DataSourceExclusions } from '../../common/types';
describe('matchExclusionPattern', () => {
it('should return the actions that should be excluded', () => {
const exclusions: DataSourceExclusions = {
data_stream_1: ['readOnly'],
};
const result = matchExclusionPattern('data_stream_1', exclusions);
expect(result).toEqual(['readOnly']);
});
it('should return an empty array if no exclusions match', () => {
const exclusions: DataSourceExclusions = {
data_stream_1: ['readOnly'],
};
const result = matchExclusionPattern('data_stream_2', exclusions);
expect(result).toEqual([]);
});
it(`should match patterns ending with '*'`, () => {
const exclusions: DataSourceExclusions = {
'data_stream_*': ['readOnly'],
};
const result = matchExclusionPattern('data_stream_1', exclusions);
expect(result).toEqual(['readOnly']);
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 { DataSourceExclusions } from '../../common/types';
/**
* These are the default exclusions for data sources (data streams and indices).
*
* They are used to exclude migrations from getting certain corrective actions.
* This is needed to avoid breaking certain built-in/system functionality that might rely on writing to these data source.
*
* These indices can be overridden by the user in the Kibana configuration:
*
* For Example this will renenable all corrective actions for the siem-signals data source:
* xpack.upgrade_assistant.dataSourceExclusions:
* '.siem-signals*': []
*/
export const defaultExclusions: DataSourceExclusions = {
'.siem-signals*': ['readOnly'],
'.alerts*': ['readOnly'],
'.internal.alerts*': ['readOnly'],
'.preview.alerts*': ['readOnly'],
'.internal.preview.alerts*': ['readOnly'],
'.lists-*': ['readOnly'],
'.items-*': ['readOnly'],
'.logs-endpoint.actions-*': ['readOnly'],
'.logs-endpoint.action.responses-*': ['readOnly'],
'.metrics-endpoint.metadata_united_default': ['readOnly'],
'.logs-osquery_manager.actions-*': ['readOnly'],
'.logs-osquery_manager.action.responses-*': ['readOnly'],
'.logs-endpoint.diagnostic.collection-*': ['readOnly'],
'kibana_sample_data_*': ['readOnly'],
'.elastic-connectors*': ['readOnly'],
};
/**
* Matches the data source name against the exclusion pattern and returns the actions that should be excluded.
* If the exclusion ends with a `*` it will match any data source that starts with the excluded pattern.
* Otherwise it will match the data source name exactly.
*/
export const matchExclusionPattern = (dataStreamName: string, exclusions: DataSourceExclusions) => {
const result = Object.entries(exclusions).find(([excludedPattern]) => {
const isPattern = /.+\*$/.test(excludedPattern);
if (isPattern) {
const matcher = excludedPattern.slice(0, -1);
return dataStreamName.startsWith(matcher);
}
return dataStreamName === excludedPattern;
});
if (!result) {
return [];
}
return result[1];
};

View file

@ -13,31 +13,31 @@ import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import { TransportResult } from '@elastic/elasticsearch';
import _ from 'lodash';
import {
DataStreamReindexStatus,
DataStreamReindexOperation,
DataStreamMigrationStatus,
DataStreamMigrationOperation,
DataStreamMetadata,
DataStreamReindexWarning,
DataStreamMigrationWarning,
DataStreamReindexTaskStatusResponse,
DataStreamReindexStatusCancelled,
} from '../../../common/types';
import { error } from './error';
import { DataStreamMigrationError, error } from './error';
interface DataStreamReindexService {
interface DataStreamMigrationService {
/**
* Checks whether or not the user has proper privileges required to reindex this index.
* Checks whether or not the user has proper privileges required to migrate this index.
* @param dataStreamName
*/
hasRequiredPrivileges: (dataStreamName: string) => Promise<boolean>;
/**
* Checks an index's settings and mappings to flag potential issues during reindex.
* Checks an index's settings and mappings to flag potential issues during migration.
* Resolves to null if index does not exist.
* @param dataStreamName
*/
detectReindexWarnings: (
detectMigrationWarnings: (
dataStreamName: string
) => Promise<DataStreamReindexWarning[] | undefined>;
) => Promise<DataStreamMigrationWarning[] | undefined>;
/**
* Creates a new reindex operation for a given index.
@ -49,7 +49,7 @@ interface DataStreamReindexService {
* Polls Elasticsearch's Data stream status API to retrieve the status of the reindex operation.
* @param dataStreamName
*/
fetchReindexStatus: (dataStreamName: string) => Promise<DataStreamReindexOperation>;
fetchMigrationStatus: (dataStreamName: string) => Promise<DataStreamMigrationOperation>;
/**
* Cancels an in-progress reindex operation for a given index.
@ -62,18 +62,27 @@ interface DataStreamReindexService {
* @param dataStreamName
*/
getDataStreamMetadata: (dataStreamName: string) => Promise<DataStreamMetadata | null>;
/**
* Marks the given indices as read-only.
* First it will roll over the write index if it exists in the deprecated indices.
* Then it will unfreeze the indices and set them to read-only.
* @param dataStreamName
* @param indices
*/
readonlyIndices: (dataStreamName: string, indices: string[]) => Promise<void>;
}
export interface DataStreamReindexServiceFactoryParams {
export interface DataStreamMigrationServiceFactoryParams {
esClient: ElasticsearchClient;
log: Logger;
licensing: LicensingPluginSetup;
}
export const dataStreamReindexServiceFactory = ({
export const dataStreamMigrationServiceFactory = ({
esClient,
licensing,
}: DataStreamReindexServiceFactoryParams): DataStreamReindexService => {
}: DataStreamMigrationServiceFactoryParams): DataStreamMigrationService => {
return {
hasRequiredPrivileges: async (dataStreamName: string): Promise<boolean> => {
/**
@ -105,10 +114,15 @@ export const dataStreamReindexServiceFactory = ({
return resp.has_all_requested;
},
async detectReindexWarnings(): Promise<DataStreamReindexWarning[]> {
async detectMigrationWarnings(): Promise<DataStreamMigrationWarning[]> {
return [
{
warningType: 'affectExistingSetups',
resolutionType: 'readonly',
},
{
warningType: 'incompatibleDataStream',
resolutionType: 'reindex',
},
];
},
@ -149,7 +163,7 @@ export const dataStreamReindexServiceFactory = ({
);
}
},
async fetchReindexStatus(dataStreamName: string): Promise<DataStreamReindexOperation> {
async fetchMigrationStatus(dataStreamName: string): Promise<DataStreamMigrationOperation> {
// Check reindexing task progress
try {
const taskResponse = await esClient.transport.request<DataStreamReindexTaskStatusResponse>({
@ -169,24 +183,58 @@ export const dataStreamReindexServiceFactory = ({
);
}
// Propagate errors from the reindex task even if reindexing is not yet complete.
if (taskResponse.errors.length) {
// Include the entire task result in the error message. This should be guaranteed
// to be JSON-serializable since it just came back from Elasticsearch.
throw error.reindexTaskFailed(
`Reindexing failed with ${taskResponse.errors.length} errors:\n${JSON.stringify(
taskResponse,
null,
2
)}`
);
}
if (taskResponse.complete) {
// Check that no failures occurred
if (taskResponse.errors.length) {
// Include the entire task result in the error message. This should be guaranteed
// to be JSON-serializable since it just came back from Elasticsearch.
throw error.reindexTaskFailed(
`Reindexing failed with ${taskResponse.errors.length} errors:\n${JSON.stringify(
taskResponse,
null,
2
)}`
/**
* If the task is complete, check if there are any remaining indices that require upgrade
* If that is the case, we need to update the status to not started
* This way the user can trigger a new migration.
* Note: This is the best place to do this call because we it'll only be called
* 1 timeonce the task is complete.
* Cases we reach this code execution:
* 1. Task is complete and the user has the UA open. It'll disappear once the user refreshes.
* 2. Task is complete but we have remaining indices that require upgrade.
*/
const { data_streams: dataStreamsDeprecations } = await esClient.migration.deprecations({
filter_path: `data_streams`,
});
const deprecationsDetails = dataStreamsDeprecations[dataStreamName];
if (deprecationsDetails && deprecationsDetails.length) {
const deprecationDetails = deprecationsDetails.find(
(deprecation) => deprecation._meta!.reindex_required
);
if (deprecationDetails) {
const stillNeedsUpgrade =
deprecationDetails._meta!.reindex_required === true &&
deprecationDetails._meta!.indices_requiring_upgrade_count > 0;
if (stillNeedsUpgrade) {
return {
status: DataStreamMigrationStatus.notStarted,
};
}
}
}
// Find the first deprecation that has reindex_required set to true
// Update the status
return {
reindexTaskPercComplete: 1,
status: DataStreamReindexStatus.completed,
taskPercComplete: 1,
status: DataStreamMigrationStatus.completed,
resolutionType: 'reindex',
progressDetails: {
startTimeMs: taskResponse.start_time_millis,
successCount: taskResponse.successes,
@ -200,8 +248,9 @@ export const dataStreamReindexServiceFactory = ({
const perc = taskResponse.successes / taskResponse.total_indices_in_data_stream;
return {
status: DataStreamReindexStatus.inProgress,
reindexTaskPercComplete: perc,
status: DataStreamMigrationStatus.inProgress,
taskPercComplete: perc,
resolutionType: 'reindex',
progressDetails: {
startTimeMs: taskResponse.start_time_millis,
successCount: taskResponse.successes,
@ -219,12 +268,13 @@ export const dataStreamReindexServiceFactory = ({
// cancelled, never started, or successful task but finished from than 24 hours ago
// Since this API should be called as a follow up from _migrate API, we can assume that the task is not started
return {
status: DataStreamReindexStatus.notStarted,
status: DataStreamMigrationStatus.notStarted,
};
}
return {
status: DataStreamReindexStatus.failed,
status: DataStreamMigrationStatus.failed,
resolutionType: 'reindex',
errorMessage: err.toString(),
};
}
@ -240,7 +290,8 @@ export const dataStreamReindexServiceFactory = ({
}
return {
status: DataStreamReindexStatus.cancelled,
status: DataStreamMigrationStatus.cancelled,
resolutionType: 'reindex',
};
},
async getDataStreamMetadata(dataStreamName: string): Promise<DataStreamMetadata | null> {
@ -285,9 +336,9 @@ export const dataStreamReindexServiceFactory = ({
throw error.cannotGrabMetadata(`Index ${index} does not exist in this cluster.`);
}
indicesRequiringUpgradeDocsSize += (indexStats[1] as any).total.store
indicesRequiringUpgradeDocsSize += (indexStats[1] as any).primaries.store
.total_data_set_size_in_bytes;
indicesRequiringUpgradeDocsCount += (indexStats[1] as any).total.docs.count;
indicesRequiringUpgradeDocsCount += (indexStats[1] as any).primaries.docs.count;
const body = await esClient.indices.getSettings({
index,
@ -319,5 +370,49 @@ export const dataStreamReindexServiceFactory = ({
);
}
},
async readonlyIndices(dataStreamName: string, indices: string[]) {
try {
const { data_streams: dataStreamsDetails } = await esClient.indices.getDataStream({
name: dataStreamName,
});
// Since we are not using a pattern it should only return one item
const dataStreamBackIndices = dataStreamsDetails[0].indices;
// The last item in this array contains information about the streams current write index.
const writeIndex = dataStreamBackIndices[dataStreamBackIndices.length - 1].index_name;
const hasWriteIndex = indices.some((index) => index === writeIndex);
if (hasWriteIndex) {
const rollOverResponse = await esClient.indices.rollover({
alias: dataStreamName,
});
if (!rollOverResponse.acknowledged) {
throw error.readonlyTaskFailed(`Could not rollover data stream ${dataStreamName}.`);
}
}
} catch (err) {
throw error.readonlyTaskFailed(`Could not migrate data stream ${dataStreamName}.`);
}
for (const index of indices) {
try {
const addBlock = await esClient.indices.addBlock({ index, block: 'write' });
if (!addBlock.acknowledged) {
throw error.readonlyTaskFailed(`Could not set index ${index} to readonly.`);
}
} catch (err) {
if (err instanceof DataStreamMigrationError) {
throw err;
}
// ES errors are serializable, so we can just stringify the error and throw it.
const stringifiedErr = JSON.stringify(err, null, 2);
throw error.readonlyTaskFailed(
`Could not migrate index "${index}". Got: ${stringifiedErr}`
);
}
}
},
};
};

View file

@ -12,16 +12,17 @@ import {
ReindexAlreadyInProgress,
ReindexCannotBeCancelled,
MetadataCannotBeGrabbed,
ReadonlyTaskFailed,
} from './error_symbols';
export class ReindexError extends Error {
export class DataStreamMigrationError extends Error {
constructor(message: string, public readonly symbol: symbol) {
super(message);
}
}
export const createErrorFactory = (symbol: symbol) => (message: string) => {
return new ReindexError(message, symbol);
return new DataStreamMigrationError(message, symbol);
};
export const error = {
@ -31,4 +32,5 @@ export const error = {
reindexTaskFailed: createErrorFactory(ReindexTaskFailed),
reindexAlreadyInProgress: createErrorFactory(ReindexAlreadyInProgress),
reindexCannotBeCancelled: createErrorFactory(ReindexCannotBeCancelled),
readonlyTaskFailed: createErrorFactory(ReadonlyTaskFailed),
};

View file

@ -11,3 +11,4 @@ export const ReindexTaskFailed = Symbol('ReindexTaskFailed');
export const ReindexAlreadyInProgress = Symbol('ReindexAlreadyInProgress');
export const ReindexCannotBeCancelled = Symbol('ReindexCannotBeCancelled');
export const MetadataCannotBeGrabbed = Symbol('MetadataCannotBeGrabbed');
export const ReadonlyTaskFailed = Symbol('ReadonlyTaskFailed');

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { dataStreamReindexServiceFactory } from './data_stream_reindex_service';
export { dataStreamMigrationServiceFactory } from './data_stream_migration_service';

View file

@ -0,0 +1,98 @@
/*
* 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 { DeprecationDetailsMessage, DeprecationsDetails } from '@kbn/core-deprecations-common';
import { GetDeprecationsContext } from '@kbn/core-deprecations-server';
import { getEnterpriseSearchPre8IndexDeprecations } from './enterprise_search_deprecations';
import indexDeprecatorFxns = require('./pre_eight_index_deprecator');
const ctx = {
esClient: {
asInternalUser: {},
},
} as GetDeprecationsContext;
function getMessageFromDeprecation(details: DeprecationsDetails): string {
const message = details.message as DeprecationDetailsMessage;
return message.content;
}
describe('getEnterpriseSearchPre8IndexDeprecations', () => {
it('can register index and data stream deprecations that need to be set to read only', async () => {
const getIndicesMock = jest.fn(() =>
Promise.resolve([
{
name: '.ent-search-index_without_datastream',
hasDatastream: false,
datastreams: [],
},
{
name: '.ent-search-with_data_stream',
hasDatastream: true,
datastreams: ['datastream-testing'],
},
])
);
jest
.spyOn(indexDeprecatorFxns, 'getPreEightEnterpriseSearchIndices')
.mockImplementation(getIndicesMock);
const deprecations = await getEnterpriseSearchPre8IndexDeprecations(ctx, 'docsurl');
expect(deprecations).toHaveLength(1);
expect(deprecations[0].correctiveActions.api?.path).toStrictEqual(
'/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only'
);
expect(deprecations[0].title).toMatch('Pre 8.x Enterprise Search indices compatibility');
expect(getMessageFromDeprecation(deprecations[0])).toContain(
'The following indices are found to be incompatible for upgrade'
);
expect(getMessageFromDeprecation(deprecations[0])).toContain(
'.ent-search-index_without_datastream'
);
expect(getMessageFromDeprecation(deprecations[0])).toContain(
'The following data streams are found to be incompatible for upgrade'
);
expect(getMessageFromDeprecation(deprecations[0])).toContain('.ent-search-with_data_stream');
});
it('can register an index without data stream deprecations that need to be set to read only', async () => {
const getIndicesMock = jest.fn(() =>
Promise.resolve([
{
name: '.ent-search-index_without_datastream',
hasDatastream: false,
datastreams: [''],
},
])
);
jest
.spyOn(indexDeprecatorFxns, 'getPreEightEnterpriseSearchIndices')
.mockImplementation(getIndicesMock);
const deprecations = await getEnterpriseSearchPre8IndexDeprecations(ctx, 'docsurl');
expect(deprecations).toHaveLength(1);
expect(deprecations[0].correctiveActions.api?.path).toStrictEqual(
'/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only'
);
expect(deprecations[0].title).toMatch('Pre 8.x Enterprise Search indices compatibility');
expect(getMessageFromDeprecation(deprecations[0])).toContain(
'The following indices are found to be incompatible for upgrade'
);
expect(getMessageFromDeprecation(deprecations[0])).toContain(
'.ent-search-index_without_datastream'
);
expect(getMessageFromDeprecation(deprecations[0])).not.toContain(
'The following data streams are found to be incompatible for upgrade'
);
expect(getMessageFromDeprecation(deprecations[0])).not.toContain(
'.ent-search-with_data_stream'
);
});
});

View file

@ -0,0 +1,126 @@
/*
* 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 { DeprecationsDetails } from '@kbn/core-deprecations-common';
import { GetDeprecationsContext, RegisterDeprecationsConfig } from '@kbn/core-deprecations-server';
import { i18n } from '@kbn/i18n';
import { getPreEightEnterpriseSearchIndices } from './pre_eight_index_deprecator';
export const getEntepriseSearchRegisteredDeprecations = (
docsUrl: string
): RegisterDeprecationsConfig => {
return {
getDeprecations: async (ctx: GetDeprecationsContext) => {
const [entSearchIndexIncompatibility] = await Promise.all([
getEnterpriseSearchPre8IndexDeprecations(ctx, docsUrl),
]);
return [...entSearchIndexIncompatibility];
},
};
};
/**
* If there are any Enterprise Search indices that were created with Elasticsearch 7.x, they must be removed
* or set to read-only
*/
export async function getEnterpriseSearchPre8IndexDeprecations(
ctx: GetDeprecationsContext,
docsUrl: string
): Promise<DeprecationsDetails[]> {
const deprecations: DeprecationsDetails[] = [];
const entSearchIndices = await getPreEightEnterpriseSearchIndices(ctx.esClient.asInternalUser);
if (!entSearchIndices || entSearchIndices.length === 0) {
return deprecations;
}
let indicesList = '';
let datastreamsList = '';
for (const index of entSearchIndices) {
if (index.hasDatastream) {
indicesList += `${index.name}\n`;
for (const datastream of index.datastreams) {
if (datastream === '') continue;
datastreamsList += `${datastream}\n`;
}
} else {
indicesList += `${index.name}\n`;
}
}
let message = `There are ${entSearchIndices.length} incompatible Enterprise Search indices.\n\n`;
if (indicesList.length > 0) {
message +=
'The following indices are found to be incompatible for upgrade:\n\n' +
'```\n' +
`${indicesList}` +
'\n```\n' +
'These indices must be either set to read-only or deleted before upgrading.\n';
}
if (datastreamsList.length > 0) {
message +=
'\nThe following data streams are found to be incompatible for upgrade:\n\n' +
'```\n' +
`${datastreamsList}` +
'\n```\n' +
'Using the "quick resolve" button below will roll over any datastreams and set all incompatible indices to read-only.\n\n' +
'Alternatively, manually deleting these indices and data streams will also unblock your upgrade.';
} else {
message +=
'Setting these indices to read-only can be attempted with the "quick resolve" button below.\n\n' +
'Alternatively, manually deleting these indices will also unblock your upgrade.';
}
deprecations.push({
level: 'critical',
deprecationType: 'feature',
title: i18n.translate(
'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.title',
{
defaultMessage: 'Pre 8.x Enterprise Search indices compatibility',
}
),
message: {
type: 'markdown',
content: i18n.translate(
'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.message',
{
defaultMessage: message,
}
),
},
documentationUrl: docsUrl,
correctiveActions: {
manualSteps: [
i18n.translate(
'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.deleteIndices',
{
defaultMessage: 'Set all incompatible indices and data streams to read only, or',
}
),
i18n.translate(
'xpack.upgradeAssistant.deprecations.incompatibleEnterpriseSearchIndexes.deleteIndices',
{
defaultMessage: 'Delete all incompatible indices and data streams',
}
),
],
api: {
method: 'POST',
path: '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only',
body: {
deprecationDetails: { domainId: 'enterpriseSearch' },
},
},
},
});
return deprecations;
}

View file

@ -0,0 +1,80 @@
/*
* 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 { kibanaResponseFactory } from '@kbn/core/server';
import { handleEsError } from '../../shared_imports';
import {
createMockRouter,
MockRouter,
routeHandlerContextMock,
} from '../../routes/__mocks__/routes.mock';
import { createRequestMock } from '../../routes/__mocks__/request.mock';
jest.mock('../es_version_precheck', () => ({
versionCheckHandlerWrapper: (a: any) => a,
}));
import indexDeprecatorFxns = require('./pre_eight_index_deprecator');
import { registerEnterpriseSearchDeprecationRoutes } from './enterprise_search_deprecations_routes';
describe('deprecation routes', () => {
let routeDependencies: any;
describe('POST /internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only', () => {
let mockRouter: MockRouter;
function registerMockRouter({ mlSnapshots } = { mlSnapshots: true }) {
mockRouter = createMockRouter();
routeDependencies = {
config: {
featureSet: { mlSnapshots, migrateSystemIndices: true, reindexCorrectiveActions: true },
},
router: mockRouter,
lib: { handleEsError },
};
registerEnterpriseSearchDeprecationRoutes(routeDependencies);
}
beforeEach(() => {
registerMockRouter();
});
afterEach(() => {
jest.resetAllMocks();
});
it('sets read-only and 200s correctly in happy path', async () => {
const setIndicesReadOnlyMock = jest.spyOn(
indexDeprecatorFxns,
'setPreEightEnterpriseSearchIndicesReadOnly'
);
setIndicesReadOnlyMock.mockResolvedValue('');
const resp = await routeDependencies.router.getHandler({
method: 'post',
pathPattern:
'/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only',
})(
routeHandlerContextMock,
createRequestMock({
body: { deprecationDetails: { domainId: 'enterpriseSearch' } },
}),
kibanaResponseFactory
);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
acknowedged: true,
});
expect(setIndicesReadOnlyMock).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -0,0 +1,33 @@
/*
* 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 { setPreEightEnterpriseSearchIndicesReadOnly } from './pre_eight_index_deprecator';
import { versionCheckHandlerWrapper } from '../es_version_precheck';
import { RouteDependencies } from '../../types';
export function registerEnterpriseSearchDeprecationRoutes({ router }: RouteDependencies) {
router.post(
{
path: '/internal/enterprise_search/deprecations/set_enterprise_search_indices_read_only',
validate: {},
},
versionCheckHandlerWrapper(async ({ core }, request, response) => {
const { client } = (await core).elasticsearch;
const setResponse = await setPreEightEnterpriseSearchIndicesReadOnly(client.asCurrentUser);
if (setResponse.length > 0) {
return response.badRequest({
body: { message: setResponse },
headers: { 'content-type': 'application/json' },
});
}
return response.ok({
body: { acknowedged: true },
headers: { 'content-type': 'application/json' },
});
})
);
}

View file

@ -0,0 +1,12 @@
/*
* 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 {
ENT_SEARCH_DATASTREAM_PATTERN,
ENT_SEARCH_DATASTREAM_PREFIXES,
ENT_SEARCH_INDEX_PREFIX,
} from './pre_eight_index_deprecator';

View file

@ -0,0 +1,276 @@
/*
* 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 { ElasticsearchClient } from '@kbn/core/server';
import {
getPreEightEnterpriseSearchIndices,
setPreEightEnterpriseSearchIndicesReadOnly,
} from './pre_eight_index_deprecator';
import type {
IndicesDataStream,
IndicesGetDataStreamResponse,
IndicesGetResponse,
IndicesIndexState,
} from '@elastic/elasticsearch/lib/api/types';
const testIndices = {
'.ent-search-already_read_only': {
settings: {
index: {
version: {
created: '7.0.0',
},
blocks: {
write: 'true',
},
verified_read_only: 'true',
},
},
data_stream: 'datastream-123',
},
'.ent-search-post_7_index': {
settings: {
index: {
version: {
created: '8.0.0',
},
},
},
},
'.ent-search-index_without_datastream': {
settings: {
index: {
version: {
created: '7.0.0',
},
},
},
},
'.ent-search-with_data_stream': {
settings: {
index: {
version: {
created: '7.0.0',
},
},
},
data_stream: 'datastream-testing',
},
'.ent-search-with_another_data_stream': {
settings: {
index: {
version: {
created: '7.0.0',
},
},
},
data_stream: 'datastream-testing-another',
},
'.ent-search-with_same_data_stream': {
settings: {
index: {
version: {
created: '7.0.0',
},
},
},
data_stream: 'datastream-testing',
},
};
const testBackingIndex = {
'.ds-some-other-backing-index': {
settings: {
index: {
version: {
created: '7.0.0',
},
},
},
data_stream: 'logs-app_search.testdatastream',
},
};
const additionalDatastreams: Record<string, IndicesDataStream> = {
'logs-app_search.testdatastream': {
name: 'logs-app_search.testdatastream',
indices: [
{ index_name: '.ds-some-other-backing-index', index_uuid: '1' },
{ index_name: '.ent-search-with_same_data_stream', index_uuid: '2' },
],
} as IndicesDataStream,
};
const testIndicesWithoutDatastream: Record<string, IndicesIndexState> = {
'.ent-search-already_read_only': {
settings: {
index: {
version: {
created: '7.0.0',
},
blocks: {
write: 'true',
},
verified_read_only: 'true',
},
},
},
'.ent-search-post_7_index': {
settings: {
index: {
version: {
created: '8.0.0',
},
},
},
},
'.ent-search-index_without_datastream': {
settings: {
index: {
version: {
created: '7.0.0',
},
},
},
},
};
function getMockIndicesFxn(values: Record<string, IndicesIndexState>) {
return () => {
const ret: IndicesGetResponse = {};
for (const [index, indexData] of Object.entries(values)) {
ret[index] = indexData;
}
return Promise.resolve(ret);
};
}
function getMockDatastreamsFxn(values: Record<string, IndicesDataStream>) {
return () => {
const ret: IndicesGetDataStreamResponse = { data_streams: [] };
for (const [, datastreamData] of Object.entries(values)) {
ret.data_streams.push(datastreamData);
}
return Promise.resolve(ret);
};
}
describe('getPreEightEnterpriseSearchIndices', () => {
let esClientMock: ElasticsearchClient;
let getIndicesMock: jest.Mock;
let getDatastreamsMock: jest.Mock;
beforeEach(() => {
getIndicesMock = jest.fn();
getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testIndices));
getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testBackingIndex));
getDatastreamsMock = jest.fn(getMockDatastreamsFxn(additionalDatastreams));
esClientMock = {
indices: {
get: getIndicesMock,
getDataStream: getDatastreamsMock,
},
} as unknown as ElasticsearchClient;
});
it('returns the correct indices', async () => {
const indices = await getPreEightEnterpriseSearchIndices(esClientMock);
expect(indices).toEqual([
{
name: '.ent-search-index_without_datastream',
hasDatastream: false,
datastreams: [''],
},
{
name: '.ent-search-with_data_stream',
hasDatastream: true,
datastreams: ['datastream-testing'],
},
{
name: '.ent-search-with_another_data_stream',
hasDatastream: true,
datastreams: ['datastream-testing-another'],
},
{
name: '.ent-search-with_same_data_stream',
hasDatastream: true,
datastreams: ['datastream-testing'],
},
{
name: '.ds-some-other-backing-index',
hasDatastream: true,
datastreams: ['logs-app_search.testdatastream'],
},
]);
expect(getIndicesMock).toHaveBeenCalledTimes(2);
expect(getIndicesMock).toHaveBeenNthCalledWith(1, {
expand_wildcards: ['all', 'hidden'],
ignore_unavailable: true,
index: '.ent-search-*',
});
expect(getIndicesMock).toHaveBeenNthCalledWith(2, {
ignore_unavailable: true,
index: ['.ds-some-other-backing-index'],
});
expect(getDatastreamsMock).toHaveBeenCalledTimes(1);
expect(getDatastreamsMock).toHaveBeenCalledWith({
expand_wildcards: ['all', 'hidden'],
name: 'logs-enterprise_search.*,logs-app_search.*,logs-workplace_search.*',
});
});
});
describe('setPreEightEnterpriseSearchIndicesReadOnly', () => {
it('does not rollover datastreams if there are none', async () => {
const getIndicesMock = jest.fn(getMockIndicesFxn(testIndicesWithoutDatastream));
const getDatastreamsMock = jest.fn(() => Promise.resolve({ data_streams: [] }));
const rolloverMock = jest.fn(() => Promise.resolve(true));
const addBlockMock = jest.fn(() => Promise.resolve({ acknowledged: true }));
const esClientMock = {
indices: {
get: getIndicesMock,
getDataStream: getDatastreamsMock,
rollover: rolloverMock,
addBlock: addBlockMock,
},
} as unknown as ElasticsearchClient;
const result = await setPreEightEnterpriseSearchIndicesReadOnly(esClientMock);
expect(result).toEqual('');
expect(getIndicesMock).toHaveBeenCalledTimes(1);
expect(rolloverMock).not.toHaveBeenCalled();
expect(addBlockMock).toHaveBeenCalledTimes(1);
});
it('does rollover datastreams if there are any', async () => {
const getIndicesMock = jest.fn();
getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testIndices));
getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testBackingIndex));
getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testIndices));
getIndicesMock.mockImplementationOnce(getMockIndicesFxn(testBackingIndex));
const getDatastreamsMock = getMockDatastreamsFxn(additionalDatastreams);
const rolloverMock = jest.fn(() => Promise.resolve(true));
const addBlockMock = jest.fn(() => Promise.resolve({ acknowledged: true }));
const esClientMock = {
indices: {
get: getIndicesMock,
getDataStream: getDatastreamsMock,
rollover: rolloverMock,
addBlock: addBlockMock,
},
} as unknown as ElasticsearchClient;
const result = await setPreEightEnterpriseSearchIndicesReadOnly(esClientMock);
expect(result).toEqual('');
expect(getIndicesMock).toHaveBeenCalledTimes(4);
expect(rolloverMock).toHaveBeenCalledTimes(3);
expect(addBlockMock).toHaveBeenCalledTimes(5);
});
});

View file

@ -0,0 +1,141 @@
/*
* 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 { IndicesIndexState } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core/server';
export const ENT_SEARCH_INDEX_PREFIX = '.ent-search-';
export const ENT_SEARCH_DATASTREAM_PREFIXES = [
'logs-enterprise_search.',
'logs-app_search.',
'logs-workplace_search.',
];
export const ENT_SEARCH_DATASTREAM_PATTERN = [
'logs-enterprise_search.*',
'logs-app_search.*',
'logs-workplace_search.*',
];
export interface EnterpriseSearchIndexMapping {
name: string;
hasDatastream: boolean;
datastreams: string[];
}
function is7xIncompatibleIndex(indexData: IndicesIndexState): boolean {
const isReadOnly = indexData.settings?.index?.verified_read_only ?? 'false';
return Boolean(
indexData.settings?.index?.version?.created?.startsWith('7') && isReadOnly !== 'true'
);
}
export async function getPreEightEnterpriseSearchIndices(
esClient: ElasticsearchClient
): Promise<EnterpriseSearchIndexMapping[]> {
const entSearchIndices = await esClient.indices.get({
index: `${ENT_SEARCH_INDEX_PREFIX}*`,
ignore_unavailable: true,
expand_wildcards: ['all', 'hidden'],
});
const returnIndices: EnterpriseSearchIndexMapping[] = [];
for (const [index, indexData] of Object.entries(entSearchIndices)) {
if (is7xIncompatibleIndex(indexData)) {
const dataStreamName = indexData.data_stream;
returnIndices.push({
name: index,
hasDatastream: dataStreamName ? true : false,
datastreams: [dataStreamName ?? ''],
});
}
}
const { data_streams: entSearchDatastreams } = await esClient.indices.getDataStream({
name: ENT_SEARCH_DATASTREAM_PATTERN.join(','),
expand_wildcards: ['all', 'hidden'],
});
const dsIndices = new Set<string>();
entSearchDatastreams.forEach(({ indices: dsi }) => {
dsi.forEach(({ index_name: indexName }) => {
dsIndices.add(indexName);
});
});
if (!dsIndices.size) return returnIndices;
for (const returnIndex of returnIndices) {
if (dsIndices.has(returnIndex.name)) {
dsIndices.delete(returnIndex.name);
}
}
if (!dsIndices.size) return returnIndices;
const entSearchDsIndices = await esClient.indices.get({
index: Array.from(dsIndices.values()),
ignore_unavailable: true,
});
for (const [index, indexData] of Object.entries(entSearchDsIndices)) {
if (is7xIncompatibleIndex(indexData)) {
const dataStreamName = indexData.data_stream;
returnIndices.push({
name: index,
hasDatastream: dataStreamName ? true : false,
datastreams: [dataStreamName ?? ''],
});
}
}
return returnIndices;
}
export async function setPreEightEnterpriseSearchIndicesReadOnly(
esClient: ElasticsearchClient
): Promise<string> {
// get the indices again to ensure nothing's changed since the last check
let indices = await getPreEightEnterpriseSearchIndices(esClient);
// rollover any datastreams first
const rolledOverDatastreams: { [id: string]: boolean } = {};
for (const index of indices) {
if (index.hasDatastream) {
for (const datastream of index.datastreams) {
if (datastream.length === 0 || rolledOverDatastreams[datastream]) {
continue;
}
const indexResponse = await esClient.indices.rollover({ alias: datastream });
if (!indexResponse) {
return `Could not roll over datastream: ${index.name}`;
}
rolledOverDatastreams[datastream] = true;
}
}
}
if (Object.keys(rolledOverDatastreams).length > 0) {
// we rolled over at least one datastream,
// get the indices again
indices = await getPreEightEnterpriseSearchIndices(esClient);
}
for (const index of indices) {
const indexName = index.name;
const indexResponse = await esClient.indices.addBlock({ index: indexName, block: 'write' });
if (!indexResponse || indexResponse.acknowledged !== true) {
return `Could not set index read-only: ${indexName}`;
}
}
return '';
}

View file

@ -80,7 +80,12 @@ Object {
},
Object {
"correctiveAction": Object {
"blockerForReindexing": undefined,
"excludedActions": Array [],
"metadata": Object {
"isClosedIndex": false,
"isFrozenIndex": false,
"isInDataStream": false,
},
"type": "reindex",
},
"details": "This index was created using version: 6.8.13",
@ -93,21 +98,29 @@ Object {
},
Object {
"correctiveAction": Object {
"blockerForReindexing": undefined,
"type": "reindex",
"metadata": Object {
"isClosedIndex": false,
"isFrozenIndex": true,
"isInDataStream": false,
},
"type": "unfreeze",
},
"details": "This index has version: 7.17.28-8.0.0",
"frozen": true,
"details": "Frozen indices must be unfrozen before upgrading to version 9.0. (The legacy frozen indices feature no longer offers any advantages. You may consider cold or frozen tiers in place of frozen indices.)",
"index": "frozen_index",
"isCritical": true,
"message": "Old index with a compatibility version < 8.0",
"message": "Index [frozen_index] is a frozen index. The frozen indices feature is deprecated and will be removed in version 9.0.",
"resolveDuringUpgrade": false,
"type": "index_settings",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migrating-8.0.html#breaking-changes-8.0",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/frozen-indices.html",
},
Object {
"correctiveAction": Object {
"blockerForReindexing": "index-closed",
"excludedActions": Array [],
"metadata": Object {
"isClosedIndex": true,
"isFrozenIndex": false,
"isInDataStream": false,
},
"type": "reindex",
},
"details": "This index was created using version: 6.8.13",
@ -175,7 +188,33 @@ Object {
},
Object {
"correctiveAction": Object {
"blockerForReindexing": undefined,
"excludedActions": Array [],
"metadata": Object {
"isClosedIndex": false,
"isFrozenIndex": false,
"isInDataStream": false,
},
"transformIds": Array [
"abc",
],
"type": "reindex",
},
"details": "This index has version: 7.17.25",
"index": "transforms_index",
"isCritical": true,
"message": "Old index with a compatibility version < 8.0",
"resolveDuringUpgrade": false,
"type": "index_settings",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
},
Object {
"correctiveAction": Object {
"excludedActions": Array [],
"metadata": Object {
"isClosedIndex": false,
"isFrozenIndex": false,
"isInDataStream": false,
},
"type": "reindex",
},
"details": "This index has version: 7.17.25",
@ -187,7 +226,20 @@ Object {
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-9.0.html",
},
Object {
"correctiveAction": undefined,
"correctiveAction": Object {
"metadata": Object {
"excludedActions": Array [],
"ignoredIndicesRequiringUpgrade": Array [],
"ignoredIndicesRequiringUpgradeCount": 0,
"indicesRequiringUpgrade": Array [
".ds-some-backing-index-5-2024.11.07-000001",
],
"indicesRequiringUpgradeCount": 1,
"reindexRequired": true,
"totalBackingIndices": 2,
},
"type": "dataStream",
},
"details": "This data stream has backing indices that were created before Elasticsearch 8.0.0",
"index": "my-v7-data-stream",
"isCritical": true,
@ -217,7 +269,7 @@ Object {
"url": "https://github.com/elastic/elasticsearch/pull/117172",
},
],
"totalCriticalDeprecations": 8,
"totalCriticalDeprecations": 9,
"totalCriticalHealthIssues": 0,
}
`;

View file

@ -5,24 +5,31 @@
* 2.0.
*/
import { EnrichedDeprecationInfo } from '../../../common/types';
import type { CorrectiveAction } from '../../../common/types';
import type { BaseDeprecation } from './migrations';
interface Action {
action_type: 'remove_settings';
objects: string[];
}
interface Actions {
interface CommonActionMetadata {
actions?: Action[];
}
interface MlActionMetadata {
actions?: Action[];
interface MlActionMetadata extends CommonActionMetadata {
snapshot_id: string;
job_id: string;
}
interface DataStreamActionMetadata {
actions?: Action[];
interface IndexActionMetadata extends CommonActionMetadata {
reindex_required: boolean;
transform_ids: string[];
is_in_data_stream?: boolean;
}
interface DataStreamActionMetadata extends CommonActionMetadata {
excludedActions?: Array<'readOnly' | 'reindex'>;
total_backing_indices: number;
reindex_required: boolean;
@ -35,33 +42,28 @@ interface DataStreamActionMetadata {
ignored_indices_requiring_upgrade_count?: number;
}
export type EsMetadata = Actions | MlActionMetadata | DataStreamActionMetadata;
export type EsMetadata = IndexActionMetadata | MlActionMetadata | DataStreamActionMetadata;
// TODO(jloleysens): Replace these regexes once this issue is addressed https://github.com/elastic/elasticsearch/issues/118062
const ES_INDEX_MESSAGES_REQIURING_REINDEX = [
/Index created before/,
/index with a compatibility version \</,
];
export const isFrozenDeprecation = (message: string, indexName?: string): boolean =>
Boolean(indexName) && message.includes(`Index [${indexName}] is a frozen index`);
export const getCorrectiveAction = (deprecation: BaseDeprecation): CorrectiveAction | undefined => {
const { index, type, message, metadata } = deprecation;
export const getCorrectiveAction = (
deprecationType: EnrichedDeprecationInfo['type'],
message: string,
metadata: EsMetadata,
indexName?: string
): EnrichedDeprecationInfo['correctiveAction'] => {
const indexSettingDeprecation = metadata?.actions?.find(
(action) => action.action_type === 'remove_settings' && indexName
(action) => action.action_type === 'remove_settings' && index
);
const clusterSettingDeprecation = metadata?.actions?.find(
(action) => action.action_type === 'remove_settings' && typeof indexName === 'undefined'
);
const requiresReindexAction = ES_INDEX_MESSAGES_REQIURING_REINDEX.some((regexp) =>
regexp.test(message)
(action) => action.action_type === 'remove_settings' && typeof index === 'undefined'
);
const requiresReindexAction =
(type === 'index_settings' || type === 'node_settings') &&
(deprecation.metadata as IndexActionMetadata)?.reindex_required === true;
const requiresUnfreezeAction = isFrozenDeprecation(message, index);
const requiresIndexSettingsAction = Boolean(indexSettingDeprecation);
const requiresClusterSettingsAction = Boolean(clusterSettingDeprecation);
const requiresMlAction = /[Mm]odel snapshot/.test(message);
const requiresDataStreamsAction = deprecationType === 'data_streams';
const requiresDataStreamsAction = type === 'data_streams';
if (requiresDataStreamsAction) {
const {
@ -97,8 +99,26 @@ export const getCorrectiveAction = (
}
if (requiresReindexAction) {
const transformIds = (metadata as IndexActionMetadata)?.transform_ids;
return {
type: 'reindex',
...(transformIds?.length ? { transformIds } : {}),
metadata: {
isClosedIndex: Boolean(deprecation.isClosedIndex),
isFrozenIndex: Boolean(deprecation.isFrozenIndex),
isInDataStream: Boolean((deprecation.metadata as IndexActionMetadata)?.is_in_data_stream),
},
};
}
if (requiresUnfreezeAction) {
return {
type: 'unfreeze',
metadata: {
isClosedIndex: Boolean(deprecation.isClosedIndex),
isFrozenIndex: Boolean(deprecation.isFrozenIndex),
isInDataStream: Boolean((deprecation.metadata as IndexActionMetadata)?.is_in_data_stream),
},
};
}

View file

@ -5,18 +5,18 @@
* 2.0.
*/
import { elasticsearchServiceMock, ScopedClusterClientMock } from '@kbn/core/server/mocks';
import { elasticsearchServiceMock, type ElasticsearchClientMock } from '@kbn/core/server/mocks';
import { getHealthIndicators } from './health_indicators';
import * as healthIndicatorsMock from '../__fixtures__/health_indicators';
describe('getHealthIndicators', () => {
let esClient: ScopedClusterClientMock;
let esClient: ElasticsearchClientMock;
beforeEach(() => {
esClient = elasticsearchServiceMock.createScopedClusterClient();
esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;
});
it('returns empty array on green indicators', async () => {
esClient.asCurrentUser.healthReport.mockResponse({
esClient.healthReport.mockResponse({
cluster_name: 'mock',
indicators: {
disk: healthIndicatorsMock.diskIndicatorGreen,
@ -30,7 +30,7 @@ describe('getHealthIndicators', () => {
});
it('returns unknown indicators', async () => {
esClient.asCurrentUser.healthReport.mockResponse({
esClient.healthReport.mockResponse({
cluster_name: 'mock',
indicators: {
disk: healthIndicatorsMock.diskIndicatorUnknown,
@ -48,7 +48,7 @@ describe('getHealthIndicators', () => {
});
it('returns unhealthy shards_capacity indicator', async () => {
esClient.asCurrentUser.healthReport.mockResponse({
esClient.healthReport.mockResponse({
cluster_name: 'mock',
indicators: {
disk: healthIndicatorsMock.diskIndicatorGreen,
@ -92,7 +92,7 @@ describe('getHealthIndicators', () => {
});
it('returns unhealthy disk indicator', async () => {
esClient.asCurrentUser.healthReport.mockResponse({
esClient.healthReport.mockResponse({
cluster_name: 'mock',
indicators: {
disk: healthIndicatorsMock.diskIndicatorRed,

Some files were not shown because too many files have changed in this diff Show more