diff --git a/docs/api/upgrade-assistant/batch_reindexing.asciidoc b/docs/api/upgrade-assistant/batch_reindexing.asciidoc index db3e080d0918..6b355185de5c 100644 --- a/docs/api/upgrade-assistant/batch_reindexing.asciidoc +++ b/docs/api/upgrade-assistant/batch_reindexing.asciidoc @@ -6,7 +6,7 @@ experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] -Start or resume multiple reindexing tasks in one request. Additionally, reindexing tasks started or resumed +Start or resume multiple <> tasks in one request. Additionally, reindexing tasks started or resumed via the batch endpoint will be placed on a queue and executed one-by-one, which ensures that minimal cluster resources are consumed over time. @@ -76,7 +76,7 @@ Similar to the <>, the API retur } -------------------------------------------------- -<1> A list of reindex operations created, the order in the array indicates the order in which tasks will be executed. +<1> A list of reindex tasks created, the order in the array indicates the order in which tasks will be executed. <2> Presence of this key indicates that the reindex job will occur in the batch. <3> A Unix timestamp of when the reindex task was placed in the queue. <4> A list of errors that may have occurred preventing the reindex task from being created. diff --git a/docs/api/upgrade-assistant/cancel_reindex.asciidoc b/docs/api/upgrade-assistant/cancel_reindex.asciidoc index 04ab3bdde35f..93e4c6fda6b4 100644 --- a/docs/api/upgrade-assistant/cancel_reindex.asciidoc +++ b/docs/api/upgrade-assistant/cancel_reindex.asciidoc @@ -4,7 +4,7 @@ Cancel reindex ++++ -experimental[] Cancel reindexes that are waiting for the {es} reindex task to complete. For example, `lastCompletedStep` set to `40`. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] Cancel reindexes that are waiting for the Elasticsearch reindex task to complete. For example, `lastCompletedStep` set to `40`. diff --git a/docs/api/upgrade-assistant/check_reindex_status.asciidoc b/docs/api/upgrade-assistant/check_reindex_status.asciidoc index 75aac7b3699f..934fd92312b0 100644 --- a/docs/api/upgrade-assistant/check_reindex_status.asciidoc +++ b/docs/api/upgrade-assistant/check_reindex_status.asciidoc @@ -4,7 +4,9 @@ Check reindex status ++++ -experimental[] Check the status of the reindex operation. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] + +Check the status of the reindex task. [[check-reindex-status-request]] ==== Request @@ -43,7 +45,7 @@ The API returns the following: <2> Current status of the reindex. For details, see <>. <3> Last successfully completed step of the reindex. For details, see <> table. <4> Task ID of the reindex task in Elasticsearch. Only present if reindexing has started. -<5> Percentage of how far the reindexing task in Elasticsearch has progressed, in decimal from from 0 to 1. +<5> Percentage of how far the reindexing task in Elasticsearch has progressed, in decimal form from 0 to 1. <6> Error that caused the reindex to fail, if it failed. <7> An array of any warning codes explaining what changes are required for this reindex. For details, see <>. <8> Specifies if the user has sufficient privileges to reindex this index. When security is unavailable or disables, returns `true`. @@ -73,7 +75,7 @@ To resume the reindex, you must submit a new POST request to the `/api/upgrade_a ==== Step codes `0`:: - The reindex operation has been created in Kibana. + The reindex task has been created in Kibana. `10`:: The index group services stopped. Only applies to some system indices. diff --git a/docs/api/upgrade-assistant/reindexing.asciidoc b/docs/api/upgrade-assistant/reindexing.asciidoc index ce5670822e5a..ccb9433ac24b 100644 --- a/docs/api/upgrade-assistant/reindexing.asciidoc +++ b/docs/api/upgrade-assistant/reindexing.asciidoc @@ -4,9 +4,18 @@ Start or resume reindex ++++ -experimental[] Start a new reindex or resume a paused reindex. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] + +Start a new reindex or resume a paused reindex. Following steps are performed during +a reindex task: + +. Setting the index to read-only +. Creating a new index +. {ref}/docs-reindex.html[Reindexing] documents into the new index +. Creating an index alias for the new index +. Deleting the old index + -Start a new reindex or resume a paused reindex. [[start-resume-reindex-request]] ==== Request @@ -40,6 +49,6 @@ The API returns the following: <1> The name of the new index. <2> The reindex status. For more information, refer to <>. <3> The last successfully completed step of the reindex. For more information, refer to <>. -<4> The task ID of the reindex task in {es}. Appears when the reindexing starts. -<5> The progress of the reindexing task in {es}. Appears in decimal form, from 0 to 1. +<4> The task ID of the {ref}/docs-reindex.html[reindex] task in {es}. Appears when the reindexing starts. +<5> The progress of the {ref}/docs-reindex.html[reindexing] task in {es}. Appears in decimal form, from 0 to 1. <6> The error that caused the reindex to fail, if it failed. diff --git a/docs/api/upgrade-assistant/status.asciidoc b/docs/api/upgrade-assistant/status.asciidoc index 42030061c428..b0c11939ca78 100644 --- a/docs/api/upgrade-assistant/status.asciidoc +++ b/docs/api/upgrade-assistant/status.asciidoc @@ -4,7 +4,7 @@ Upgrade readiness status ++++ -experimental[] Check the status of your cluster. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] Check the status of your cluster. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index de679692e7a8..1429ad29be5f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -600,8 +600,7 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona |{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant/README.md[upgradeAssistant] -|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary -purposes are to: +|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. It will only be enabled on the last minor before the next major release. This is controlled via the config: xpack.upgrade_assistant.readonly (#101296). |{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index b8fc9b85f6bd..59d1f13ed89f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -10,6 +10,9 @@ readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -133,7 +136,11 @@ readonly links: { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 1389362168eb..a2f43263655e 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -20,6 +20,7 @@ export class DocLinksService { public start({ injectedMetadata }: StartDeps): DocLinksStart { const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch(); const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; + const STACK_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack/${DOC_LINK_VERSION}/`; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; @@ -34,6 +35,9 @@ export class DocLinksService { links: { settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`, elasticStackGetStarted: `${STACK_GETTING_STARTED}get-started-elastic-stack.html`, + upgrade: { + upgradingElasticStack: `${STACK_DOCS}upgrading-elastic-stack.html`, + }, apm: { kibanaSettings: `${KIBANA_DOCS}apm-settings-in-kibana.html`, supportedServiceMaps: `${KIBANA_DOCS}service-maps.html#service-maps-supported`, @@ -156,7 +160,11 @@ export class DocLinksService { }, addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`, - upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, + upgradeAssistant: { + overview: `${KIBANA_DOCS}upgrade-assistant.html`, + batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`, + remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`, + }, rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, @@ -220,10 +228,11 @@ export class DocLinksService { remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, - setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`, transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, + setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, + apiCompatibilityHeader: `${ELASTICSEARCH_DOCS}api-conventions.html#api-compatibility`, }, siem: { guide: `${SECURITY_SOLUTION_DOCS}index.html`, @@ -287,6 +296,7 @@ export class DocLinksService { outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, + setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, @@ -522,6 +532,9 @@ export interface DocLinksStart { readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -645,7 +658,11 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 612674cc8df3..ebd7c1e14deb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -477,6 +477,9 @@ export interface DocLinksStart { readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -600,7 +603,11 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index 33085bdbf447..1241d6222a38 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -103,8 +103,13 @@ export const useRequest = ( : serializedResponseData; setData(responseData); } - // Setting isLoading to false also acts as a signal for scheduling the next poll request. - setIsLoading(false); + // There can be situations in which a component that consumes this hook gets unmounted when + // the request returns an error. So before changing the isLoading state, check if the component + // is still mounted. + if (isMounted.current === true) { + // Setting isLoading to false also acts as a signal for scheduling the next poll request. + setIsLoading(false); + } }, [requestBody, httpClient, deserializer, clearPollInterval] ); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index a8500f3ebf8b..13caed52ded8 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -8105,44 +8105,6 @@ } } } - }, - "ui_open": { - "properties": { - "elasticsearch": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Elasticsearch deprecations." - } - }, - "overview": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the Overview page." - } - }, - "kibana": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Kibana deprecations" - } - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long" - }, - "open": { - "type": "long" - }, - "start": { - "type": "long" - }, - "stop": { - "type": "long" - } - } } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a9c3924ee420..86e884c512ab 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25101,28 +25101,14 @@ "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "ヘルプ", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "無効な形式:{message}", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}", - "xpack.upgradeAssistant.appTitle": "{version} アップグレードアシスタント", "xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel": "Elasticsearchの廃止予定", "xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel": "Kibanaの廃止予定", "xpack.upgradeAssistant.breadcrumb.overviewLabel": "アップグレードアシスタント", - "xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "より多く表示させるにはフィルターを変更します。", - "xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "重大", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "インデックス別", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "問題別", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "アップグレード前にこの問題を解決してください。", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "重大", - "xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip": "アップグレード前にこの問題を解決することをお勧めしますが、必須ではありません。", - "xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告", - "xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "説明がありません", - "xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "{total} 件中 {numShown} 件を表示中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "キャンセル", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "閉じる", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel": "再インデックスを続ける", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle": "このインデックスを再インデックスするための権限がありません", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail": "再インデックスはバックグラウンドで継続しますが、Kibana がシャットダウンまたは再起動した場合、このページに戻り再インデックスを再開させる必要があります。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.calloutTitle": "インデックスは再インデックス中にドキュメントを投入、更新、または削除できません", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.cantStopDetail": "ドキュメントの更新を停止できない場合、または新しいクラスターに再インデックスする必要がある場合は、異なるアップグレード方法をお勧めします。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.doneLabel": "完了!", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.reindexingLabel": "再インデックス中…", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.resumeLabel": "再開", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel": "再インデックスを実行", @@ -25133,17 +25119,9 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel": "キャンセル中…", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel": "キャンセルできませんでした", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle": "新規インデックスを作成中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.pauseMlStepTitle": "機械学習ジョブを一時停止中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle": "古いインデックスを読み込み専用に設定中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle": "ドキュメントを再インデックス中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeMlStepTitle": "機械学習ジョブを再開中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeWatcherStepTitle": "Watcher を再開中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.stopWatcherStepTitle": "Watcher を停止中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle": "プロセスを再インデックス中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails": "このインデックスは現在閉じています。アップグレードアシスタントが開き、再インデックスを実行してからインデックスを閉じます。 {reindexingMayTakeLongerEmph}。詳細については {docs} をご覧ください。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis": "再インデックスには通常よりも時間がかかることがあります", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutTitle": "インデックスが閉じました", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation": "ドキュメンテーション", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningDetail": "マッピングタイプは8.0ではサポートされていません。アプリケーションコードまたはスクリプトが{mappingType}に依存していないことを確認してください。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningTitle": "マッピングタイプ{mappingType}を{defaultType}で置き換えます", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.deprecatedIndexSettingsWarningDetail": "次の廃止予定のインデックス設定が検出されました。", @@ -25151,16 +25129,6 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail": "続行する前に、インデックスをバックアップしてください。再インデックスを続行するには、各変更を承諾してください。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle": "このインデックスには元に戻すことのできない破壊的な変更が含まれています", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.documentationLinkLabel": "ドキュメント", - "xpack.upgradeAssistant.deprecationGroupItem.docLinkText": "ドキュメンテーションを表示", - "xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel": "修正する手順を表示", - "xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel": "クイック解決", - "xpack.upgradeAssistant.deprecationGroupItemTitle": "'{domainId}'は廃止予定の機能を使用しています", - "xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel": "すべて縮小", - "xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel": "すべて拡張", - "xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel": "フィルター無効:{searchTermError}", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel": "フィルター", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel": "フィルター", - "xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel": "再読み込み", "xpack.upgradeAssistant.emptyPrompt.learnMoreDescription": "{nextMajor}への移行に関する詳細をご覧ください。", "xpack.upgradeAssistant.emptyPrompt.title": "{uaVersion} アップグレードアシスタント", "xpack.upgradeAssistant.emptyPrompt.upgradeAssistantDescription": "アップグレードアシスタントはクラスターの廃止予定の設定を特定し、アップグレード前に問題を解決できるようにします。Elastic {nextMajor}にアップグレードするときにここに戻って確認してください。", @@ -25179,35 +25147,10 @@ "xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradeSnapshotErrorTitle": "スナップショットのアップグレードエラー", "xpack.upgradeAssistant.esDeprecations.pageDescription": "廃止予定のクラスターとインデックス設定をレビューします。アップグレード前に重要な問題を解決する必要があります。", "xpack.upgradeAssistant.esDeprecations.pageTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel": "このクラスター{criticalDeprecations}には重大な廃止予定があります", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsTitle": "重大", - "xpack.upgradeAssistant.esDeprecationStats.loadingText": "Elasticsearchの廃止統計情報を読み込んでいます...", - "xpack.upgradeAssistant.esDeprecationStats.noDeprecationsText": "警告なし。準備ができました。", - "xpack.upgradeAssistant.esDeprecationStats.statsTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTitle": "警告", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription": "エラーについては、Kibanaサーバーログを確認してください。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle": "Kibana廃止予定を取得できませんでした", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription": "エラーについては、Kibanaサーバーログを確認してください。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle": "一部のKibana廃止予定が正常に取得されませんでした", "xpack.upgradeAssistant.kibanaDeprecations.deprecationLabel": "Kibana", "xpack.upgradeAssistant.kibanaDeprecations.docLinkText": "ドキュメント", - "xpack.upgradeAssistant.kibanaDeprecations.errorMessage": "廃止予定の解決エラー", "xpack.upgradeAssistant.kibanaDeprecations.loadingText": "廃止予定を読み込んでいます...", - "xpack.upgradeAssistant.kibanaDeprecations.pageDescription": "アップグレード前に、ここで一覧の問題を確認し、必要な変更を行ってください。アップグレード前に、重大な問題を解決する必要があります。", "xpack.upgradeAssistant.kibanaDeprecations.pageTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel": "キャンセル", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle": "'{domainId}'で廃止予定を解決しますか?", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel": "解決", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel": "閉じる", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel": "ドキュメンテーションを表示", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle": "'{domainId}'で廃止予定を解決", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle": "ステップ{step}", - "xpack.upgradeAssistant.kibanaDeprecations.successMessage": "廃止予定が解決されました", - "xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle": "重大", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage": "Kibana廃止予定の取得中にエラーが発生しました。", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingText": "Kibana廃止予定統計情報を読み込んでいます…", - "xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecationStats.warningDeprecationsTitle": "警告", "xpack.upgradeAssistant.noDeprecationsPrompt.nextStepsDescription": "他のスタック廃止予定については、{overviewButton}を確認してください。", "xpack.upgradeAssistant.noDeprecationsPrompt.overviewLinkText": "概要ページ", "xpack.upgradeAssistant.overview.analyzeTitle": "廃止予定ログを分析", @@ -25227,8 +25170,6 @@ "xpack.upgradeAssistant.overview.observe.observabilityDescription": "使用中のAPIのうち廃止予定のAPIと、更新が必要なアプリケーションを特定できます。", "xpack.upgradeAssistant.overview.pageDescription": "次のバージョンのElastic Stackをお待ちください。", "xpack.upgradeAssistant.overview.pageTitle": "アップグレードアシスタント", - "xpack.upgradeAssistant.overview.reviewStepTitle": "廃止予定設定を確認し、問題を解決", - "xpack.upgradeAssistant.overview.toggleTitle": "Elasticsearch廃止予定警告をログに出力", "xpack.upgradeAssistant.overview.upgradeGuideLink": "アップグレードガイドを表示", "xpack.upgradeAssistant.overview.upgradeStepCloudLink": "クラウドでアップグレード", "xpack.upgradeAssistant.overview.upgradeStepDescription": "重要な問題をすべて解決し、アプリケーションの準備を確認した後に、Elastic Stackをアップグレードできます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index db3c50863f83..c815d95a1655 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25527,28 +25527,14 @@ "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "帮助", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "格式无效:{message}", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}", - "xpack.upgradeAssistant.appTitle": "{version} 升级助手", "xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel": "Elasticsearch 弃用", "xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel": "Kibana 弃用", "xpack.upgradeAssistant.breadcrumb.overviewLabel": "升级助手", - "xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "更改筛选以显示更多内容。", - "xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "紧急", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "按索引", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "按问题", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "请解决此问题后再升级。", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "紧急", - "xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip": "建议在升级之前先解决此问题,但这不是必需的。", - "xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告", - "xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "无弃用内容", - "xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "显示 {numShown} 个,共 {total} 个", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "取消", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "关闭", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel": "继续重新索引", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle": "您没有足够的权限来重新索引此索引", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail": "重新索引将在后台继续,但如果 Kibana 关闭或重新启动,您将需要返回此页,才能恢复重新索引。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.calloutTitle": "在重新索引时,索引无法采集、更新或删除文档", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.cantStopDetail": "如果您无法停止文档更新或需要重新索引到新的集群中,请考虑使用不同的升级策略。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.doneLabel": "已完成!", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.reindexingLabel": "正在重新索引……", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.resumeLabel": "恢复", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel": "运行重新索引", @@ -25559,17 +25545,9 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel": "正在取消……", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel": "无法取消", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle": "正在创建新索引", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.pauseMlStepTitle": "正在暂停 Machine Learning 作业", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle": "正在将旧索引设置为只读", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle": "正在重新索引文档", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeMlStepTitle": "正在恢复 Machine Learning 作业", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeWatcherStepTitle": "正在恢复 Watcher", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.stopWatcherStepTitle": "正在停止 Watcher", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle": "重新索引过程", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails": "此索引当前已关闭。升级助手将打开索引,重新索引,然后关闭索引。{reindexingMayTakeLongerEmph}。请参阅文档{docs}以了解更多信息。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis": "重新索引可能比通常花费更多的时间", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutTitle": "索引已关闭", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation": "文档", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningDetail": "映射类型在 8.0 中不再受支持。确保没有应用程序代码或脚本依赖 {mappingType}。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningTitle": "将映射类型 {mappingType} 替换为 {defaultType}", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.deprecatedIndexSettingsWarningDetail": "检测到以下弃用的索引设置:", @@ -25577,16 +25555,6 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail": "继续前备份索引。要继续重新索引,请接受每个更改。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle": "此索引需要无法恢复的破坏性更改", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.documentationLinkLabel": "文档", - "xpack.upgradeAssistant.deprecationGroupItem.docLinkText": "查看文档", - "xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel": "显示修复步骤", - "xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel": "快速解决", - "xpack.upgradeAssistant.deprecationGroupItemTitle": "“{domainId}”正在使用弃用的功能", - "xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel": "折叠全部", - "xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel": "展开全部", - "xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel": "筛选无效:{searchTermError}", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel": "筛选", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel": "筛选", - "xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel": "重新加载", "xpack.upgradeAssistant.emptyPrompt.learnMoreDescription": "详细了解如何迁移到 {nextMajor}。", "xpack.upgradeAssistant.emptyPrompt.title": "{uaVersion} 升级助手", "xpack.upgradeAssistant.emptyPrompt.upgradeAssistantDescription": "升级助手识别集群中弃用的设置,帮助您在升级前解决问题。需要升级到 Elastic {nextMajor} 时,回到这里查看。", @@ -25605,37 +25573,10 @@ "xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradeSnapshotErrorTitle": "升级快照时出错", "xpack.upgradeAssistant.esDeprecations.pageDescription": "查看已弃用的群集和索引设置。在升级之前必须解决任何紧急问题。", "xpack.upgradeAssistant.esDeprecations.pageTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel": "此集群具有 {criticalDeprecations} 个关键弃用", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsTitle": "紧急", - "xpack.upgradeAssistant.esDeprecationStats.loadingText": "正在加载 Elasticsearch 弃用统计……", - "xpack.upgradeAssistant.esDeprecationStats.noDeprecationsText": "无警告。已就绪!", - "xpack.upgradeAssistant.esDeprecationStats.statsTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTitle": "警告", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription": "请在 Kibana 服务器日志中查看错误。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle": "无法检索 Kibana 弃用", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription": "请在 Kibana 服务器日志中查看错误。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle": "未成功检索全部的 Kibana 弃用", "xpack.upgradeAssistant.kibanaDeprecations.deprecationLabel": "Kibana", "xpack.upgradeAssistant.kibanaDeprecations.docLinkText": "文档", - "xpack.upgradeAssistant.kibanaDeprecations.errorMessage": "解决弃用时出错", "xpack.upgradeAssistant.kibanaDeprecations.loadingText": "正在加载弃用……", - "xpack.upgradeAssistant.kibanaDeprecations.pageDescription": "在升级之前查看此处所列的问题并进行必要的更改。在升级之前必须解决紧急问题。", "xpack.upgradeAssistant.kibanaDeprecations.pageTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel": "取消", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle": "在“{domainId}”中解决弃用?", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel": "解决", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel": "关闭", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel": "查看文档", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle": "在“{domainId}”中解决弃用", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle": "步骤 {step}", - "xpack.upgradeAssistant.kibanaDeprecations.successMessage": "弃用已解决", - "xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsLabel": "Kibana 有 {criticalDeprecations} 个紧急{criticalDeprecations, plural, other {弃用}}", - "xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle": "紧急", - "xpack.upgradeAssistant.kibanaDeprecationStats.getWarningDeprecationsMessage": "Kibana 有 {warningDeprecations} 个警告{warningDeprecations, plural, other {弃用}}", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage": "检索 Kibana 弃用时发生错误。", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingText": "正在加载 Kibana 弃用统计……", - "xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecationStats.warningDeprecationsTitle": "警告", "xpack.upgradeAssistant.noDeprecationsPrompt.nextStepsDescription": "查看{overviewButton}以了解其他 Stack 弃用。", "xpack.upgradeAssistant.noDeprecationsPrompt.overviewLinkText": "“概览”页面", "xpack.upgradeAssistant.overview.analyzeTitle": "分析弃用日志", @@ -25655,8 +25596,6 @@ "xpack.upgradeAssistant.overview.observe.observabilityDescription": "深入了解正在使用哪些已弃用 API 以及需要更新哪些应用程序。", "xpack.upgradeAssistant.overview.pageDescription": "准备使用下一版 Elastic Stack!", "xpack.upgradeAssistant.overview.pageTitle": "升级助手", - "xpack.upgradeAssistant.overview.reviewStepTitle": "复查已弃用设置并解决问题", - "xpack.upgradeAssistant.overview.toggleTitle": "记录 Elasticsearch 弃用警告", "xpack.upgradeAssistant.overview.upgradeGuideLink": "查看升级指南", "xpack.upgradeAssistant.overview.upgradeStepCloudLink": "在 Cloud 上升级", "xpack.upgradeAssistant.overview.upgradeStepDescription": "解决所有关键问题并确认您的应用程序就绪后,便可以升级 Elastic Stack。", diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md index a6cb3b431c82..6570e7f8d761 100644 --- a/x-pack/plugins/upgrade_assistant/README.md +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -2,66 +2,253 @@ ## About -Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary -purposes are to: +Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. It will only be enabled on the last minor before the next major release. This is controlled via the config: `xpack.upgrade_assistant.readonly` ([#101296](https://github.com/elastic/kibana/pull/101296)). -* **Surface deprecations.** Deprecations are features that are currently being used that will be -removed in the next major. Surfacing tells the user that there's a problem preventing them -from upgrading. -* **Migrate from deprecation features to supported features.** This addresses the problem, clearing -the path for the upgrade. Generally speaking, once all deprecations are addressed, the user can -safely upgrade. +Its primary purposes are to: + +* **Surface deprecations.** Deprecations are features that are currently being used that will be removed in the next major. Surfacing tells the user that there's a problem preventing them from upgrading. +* **Migrate from deprecated features to supported features.** This addresses the problem, clearing the path for the upgrade. Generally speaking, once all deprecations are addressed, the user can safely upgrade. ### Deprecations -There are two sources of deprecation information: +There are three sources of deprecation information: -* [**Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html) -This is information about cluster, node, and index level settings that use deprecated features that -will be removed or changed in the next major version. Currently, only cluster and index deprecations -will be surfaced in the Upgrade Assistant. ES server engineers are responsible for adding -deprecations to the Deprecation Info API. -* [**Deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging) +* [**Elasticsearch Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html) +This is information about Elasticsearch cluster, node, Machine Learning, and index-level settings that use deprecated features that will be removed or changed in the next major version. ES server engineers are responsible for adding deprecations to the Deprecation Info API. +* [**Elasticsearch deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging) These surface runtime deprecations, e.g. a Painless script that uses a deprecated accessor or a 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. +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/master/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 -Problems can be fixed at various points in the upgrade process. The Upgrade Assistant supports -various upgrade paths and surfaces various types of upgrade-related issues. - -* **Fixing deprecated cluster settings pre-upgrade.** This generally requires fixing some settings -in `elasticsearch.yml`. -* **Migrating indices data pre-upgrade.** This can involve deleting indices so that ES can rebuild -them in the new version, reindexing them so that they're built using a new Lucene version, or -applying a migration script that reindexes them with new settings/mappings/etc. -* **Migrating indices data post-upgrade.** As was the case with APM in the 6.8->7.x upgrade, -sometimes the new data format isn't forwards-compatible. In these cases, the user will perform the -upgrade first and then use the Upgrade Assistant to reindex their data to be compatible with the new -version. - -Deprecations can be handled in a number of ways: - -* **Reindexing.** When a user's index contains deprecations (e.g. mappings) a reindex solves them. -Upgrade Assistant contains migration scripts that are executed as part of the reindex process. -The user will see a "Reindex" button they can click which will apply this script and perform the -reindex. +#### Elasticsearch + +Elasticsearch deprecations can be handled in a number of ways: + +* **Reindexing.** When a user's index contains deprecations (e.g. mappings) a reindex solves them. Currently, the Upgrade Assistant only automates reindexing for old indices. For example, if you are currently on 7.x, and want to migrate to 8.0, but you still have indices that were created on 6.x. For this scenario, the user will see a "Reindex" button that they can click, which will perform the reindex. * Reindexing is an atomic process in Upgrade Assistant, so that ingestion is never disrupted. It works like this: * Create a new index with a "reindexed-" prefix ([#30114](https://github.com/elastic/kibana/pull/30114)). * Create an index alias pointing from the original index name to the prefixed index name. * Reindex from the original index into the prefixed index. * Delete the old index and rename the prefixed index. - * Some apps might require custom scripts, as was the case with APM ([#29845](https://github.com/elastic/kibana/pull/29845)). - In that case the migration performed a reindex with a Painless script (covered by automated tests) - that made the required changes to the data. -* **Update index settings.** Some index settings will need to be updated, which doesn't require a -reindex. An example of this is the "Fix" button that was added for metricbeat and filebeat indices -([#32829](https://github.com/elastic/kibana/pull/32829), [#33439](https://github.com/elastic/kibana/pull/33439)). +* **Updating index settings.** Some index settings will need to be updated, which doesn't require a +reindex. An example of this is the "Remove deprecated settings" button, which is shown when [deprecated translog settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html#index-modules-translog-retention) are detected ([#93293](https://github.com/elastic/kibana/pull/93293)). +* **Upgrading or deleting snapshots**. This is specific to Machine Learning. If a user has old Machine Learning job model snapshots, they will need to be upgraded or deleted. The Upgrade Assistant provides a way to resolve this automatically for the user ([#100066](https://github.com/elastic/kibana/pull/100066)). * **Following the docs.** The Deprecation Info API provides links to the deprecation docs. Users will follow these docs to address the problem and make these warnings or errors disappear in the Upgrade Assistant. -* **Stopping/restarting tasks and jobs.** Users had to stop watches and ML jobs and restart them as -soon as reindexing was complete ([#29663](https://github.com/elastic/kibana/pull/29663)). \ No newline at end of file + +#### Kibana + +Kibana deprecations can be handled in one of two ways: + +* **Automatic resolution.** Some deprecations can be fixed automatically through Upgrade Assistant via an API call. When this is possible, users will see a "Quick resolve" button in the Upgrade Assistant. +* **Manual steps.** For deprecations that require the user to address manually, the Upgrade Assistant provides a list of steps to follow as well as a link to documentation. Once the deprecation is addressed, it will no longer appear in the Upgrade Assistant. + +### Steps for testing +#### Elasticsearch deprecations + +To test the Elasticsearch deprecations page ([#107053](https://github.com/elastic/kibana/pull/107053)), you will first need to create a set of deprecations that will be returned from the deprecation info API. + +**1. Reindexing** + + The reindex action appears in UA whenever the deprecation `Index created before XX` is encountered. To reproduce, you will need to start up a cluster on the previous major version (e.g., if you are running 7.x, start a 6.8 cluster). Create a handful of indices, for example: + + ``` + PUT my_index + ``` + + Next, point to the 6.x data directory when running from a 7.x cluster. + + ``` + yarn es snapshot -E path.data=./path_to_6.x_indices + ``` + + **Token-based authentication** + + Reindexing should also work using token-based authentication (implemented via [#111451](https://github.com/elastic/kibana/pull/111451)). To simulate, set the following parameters when running ES from a snapshot: + + ``` + yarn es snapshot -E path.data=./path_to_6.x_indices -E xpack.security.authc.token.enabled=true -E xpack.security.authc.api_key.enabled=true + ``` + + Then, update your `kibana.dev.yml` file to include: + + ``` + xpack.security.authc.providers: + token: + token1: + order: 0 + showInSelector: true + enabled: true + ``` + + To verify it's working as expected, kick off a reindex task in UA. Then, navigate to **Security > API keys** and verify an API key was created. The name should be prefixed with `ua_reindex_`. Once the reindex task has completed successfully, the API key should be deleted. + +**2. Upgrading or deleting ML job model snapshots** + + Similar to the reindex action, the ML action requires setting up a cluster on the previous major version. It also requires the trial license to be enabled. Then, you will need to create a few ML jobs in order to trigger snapshots. + + - Add the Kibana sample data. + - Navigate to Machine Learning > Create new job. + - Select `kibana_sample_data_flights` index. + - Select "Single metric job". + - Add an aggregation, field, and job ID. Change the time range to "Absolute" and select a subset of time. + - Click "Create job" + - View the job created and click the "Start datafeed" action associated with it. Select a subset of time and click "Start". You should now have two snapshots created. If you want to add more, repeat the steps above. + + Next, point to the 6.x data directory when running from a 7.x cluster. + + ``` + yarn es snapshot --license trial -E path.data=./path_to_6.x_ml_snapshots + ``` + +**3. Removing deprecated index settings** + + The Upgrade Assistant currently only supports fixing deprecated translog index settings. However [the code](https://github.com/elastic/kibana/blob/master/x-pack/plugins/upgrade_assistant/common/constants.ts#L22) is written in a way to add support for more if necessary. Run the following Console command to trigger the deprecation warning: + + ``` + PUT deprecated_settings + { + "settings": { + "translog.retention.size": "1b", + "translog.retention.age": "5m", + "index.soft_deletes.enabled": true, + } + } + ``` + +**4. Other deprecations with no automatic resolutions** + + Many deprecations emitted from the deprecation info API are too complex to provide an automatic resolution for in UA. In this case, UA provides details about the deprecation as well as a link to documentation. The following requests will emit deprecations from the deprecation info API. This list is *not* exhaustive of all possible deprecations. You can find the full list of [7.x deprecations in the Elasticsearch repo](https://github.com/elastic/elasticsearch/tree/7.x/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation) by grepping `new DeprecationIssue` in the code. + + ``` + PUT /nested_multi_fields + { + "mappings":{ + "properties":{ + "text":{ + "type":"text", + "fields":{ + "english":{ + "type":"text", + "analyzer":"english", + "fields":{ + "english":{ + "type":"text", + "analyzer":"english" + } + } + } + } + } + } + } + } + ``` + + ``` + PUT field_names_enabled + { + "mappings": { + "_field_names": { + "enabled": false + } + } + } + ``` + + ``` + PUT /_cluster/settings + { + "persistent" : { + "indices.lifecycle.poll_interval" : "500ms" + } + } + ``` + + ``` + PUT _template/field_names_enabled + { + "index_patterns": ["foo"], + "mappings": { + "_field_names": { + "enabled": false + } + } + } + ``` + + ``` + // This is only applicable for indices created prior to 7.x + PUT joda_time + { + "mappings" : { + "properties" : { + "datetime": { + "type": "date", + "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis" + } + } + } + } + ``` + +#### Kibana deprecations +To test the Kibana deprecations page, you will first need to create a set of deprecations that will be returned from the Kibana deprecations API. + +`reporting` is currently one of the only plugins that is registering a deprecation with an automated resolution (implemented via [#104303](https://github.com/elastic/kibana/pull/104303)). To trigger this deprecation: + +1. Add Kibana sample data. +2. Create a PDF report from the Dashboard (**Dashboard > Share > PDF reports > Generate PDFs**). This requires a trial license. +3. Issue the following request in Console: + +``` +PUT .reporting-*/_settings +{ + "settings": { + "index.lifecycle.name": null + } +} +``` + +For a complete list of Kibana deprecations, refer to the [8.0 Kibana deprecations meta issue](https://github.com/elastic/kibana/issues/109166). + +### Errors + +This is a non-exhaustive list of different error scenarios in Upgrade Assistant. It's recommended to use the [tweak browser extension](https://chrome.google.com/webstore/detail/tweak-mock-api-calls/feahianecghpnipmhphmfgmpdodhcapi?hl=en), or something similar, to mock the API calls. + +- **Error loading deprecation logging status.** Mock a `404` status code to `GET /api/upgrade_assistant/deprecation_logging`. Alternatively, edit [this line](https://github.com/elastic/kibana/blob/545c1420c285af8f5eee56f414bd6eca735aea11/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts#L70) locally and replace `deprecation_logging` with `fake_deprecation_logging`. +- **Error updating deprecation logging status.** Mock a `404` status code to `PUT /api/upgrade_assistant/deprecation_logging`. Alternatively, edit [this line](https://github.com/elastic/kibana/blob/545c1420c285af8f5eee56f414bd6eca735aea11/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts#L77) locally and replace `deprecation_logging` with `fake_deprecation_logging`. +- **Unauthorized error fetching ES deprecations.** Mock a `403` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 403 }` +- **Partially upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": false } }` +- **Upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": true } }` + +### Telemetry + +The Upgrade Assistant tracks several triggered events in the UI, using Kibana Usage Collection service's [UI counters](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#ui-counters). + +**Overview page** +- Component loaded +- Click event for "Create snapshot" button +- Click event for "View deprecation logs in Observability" link +- Click event for "Analyze logs in Discover" link +- Click event for "Reset counter" button + +**ES deprecations page** +- Component loaded +- Click events for starting and stopping reindex tasks +- Click events for upgrading or deleting a Machine Learning snapshot +- Click event for deleting a deprecated index setting + +**Kibana deprecations page** +- Component loaded +- Click event for "Quick resolve" button + +In addition to UI counters, the Upgrade Assistant has a [custom usage collector](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#custom-collector). It currently is only responsible for tracking whether the user has deprecation logging enabled or not. + +For testing instructions, refer to the [Kibana Usage Collection service README](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#testing). \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.tsx new file mode 100644 index 000000000000..23726e05b895 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.tsx @@ -0,0 +1,50 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; + +import { App } from '../../../public/application/app'; +import { WithAppDependencies } from '../helpers'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`/overview`], + componentRoutePath: '/overview', + }, + doMountAsync: true, +}; + +export type AppTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const clickDeprecationToggle = async () => { + const { find, component } = testBed; + + await act(async () => { + find('deprecationLoggingToggle').simulate('click'); + }); + + component.update(); + }; + + return { + clickDeprecationToggle, + }; +}; + +export const setupAppPage = async (overrides?: Record): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(App, overrides), testBedConfig); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.tsx new file mode 100644 index 000000000000..043c649b39bc --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from '../helpers'; +import { AppTestBed, setupAppPage } from './app.helpers'; + +describe('Cluster upgrade', () => { + let testBed: AppTestBed; + let server: ReturnType['server']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; + + beforeEach(() => { + ({ server, httpRequestsMockHelpers } = setupEnvironment()); + }); + + afterEach(() => { + server.restore(); + }); + + describe('when user is still preparing for upgrade', () => { + beforeEach(async () => { + testBed = await setupAppPage(); + }); + + test('renders overview', () => { + const { exists } = testBed; + expect(exists('overview')).toBe(true); + expect(exists('isUpgradingMessage')).toBe(false); + expect(exists('isUpgradeCompleteMessage')).toBe(false); + }); + }); + + describe('when cluster is in the process of a rolling upgrade', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, { + statusCode: 426, + message: '', + attributes: { + allNodesUpgraded: false, + }, + }); + + await act(async () => { + testBed = await setupAppPage(); + }); + }); + + test('renders rolling upgrade message', async () => { + const { component, exists } = testBed; + component.update(); + expect(exists('overview')).toBe(false); + expect(exists('isUpgradingMessage')).toBe(true); + expect(exists('isUpgradeCompleteMessage')).toBe(false); + }); + }); + + describe('when cluster has been upgraded', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, { + statusCode: 426, + message: '', + attributes: { + allNodesUpgraded: true, + }, + }); + + await act(async () => { + testBed = await setupAppPage(); + }); + }); + + test('renders upgrade complete message', () => { + const { component, exists } = testBed; + component.update(); + expect(exists('overview')).toBe(false); + expect(exists('isUpgradingMessage')).toBe(false); + expect(exists('isUpgradeCompleteMessage')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts index 917fac8ef666..fdd8a1c99393 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts @@ -7,8 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; - +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; describe('Default deprecation flyout', () => { @@ -35,16 +35,19 @@ describe('Default deprecation flyout', () => { testBed.component.update(); }); - it('renders a flyout with deprecation details', async () => { + test('renders a flyout with deprecation details', async () => { const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2]; const { actions, find, exists } = testBed; - await actions.clickDefaultDeprecationAt(0); + await actions.table.clickDeprecationRowAt('default', 0); expect(exists('defaultDeprecationDetails')).toBe(true); expect(find('defaultDeprecationDetails.flyoutTitle').text()).toContain( multiFieldsDeprecation.message ); + expect(find('defaultDeprecationDetails.documentationLink').props().href).toBe( + multiFieldsDeprecation.url + ); expect(find('defaultDeprecationDetails.flyoutDescription').text()).toContain( multiFieldsDeprecation.index ); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts index ceebc528f0bc..3b8a756b8e64 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts @@ -9,7 +9,8 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import type { MlAction } from '../../../common/types'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, @@ -17,7 +18,7 @@ import { createEsDeprecationsMockResponse, } from './mocked_responses'; -describe('Deprecations table', () => { +describe('ES deprecations table', () => { let testBed: ElasticsearchTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -56,31 +57,49 @@ describe('Deprecations table', () => { const { actions } = testBed; const totalRequests = server.requests.length; - await actions.clickRefreshButton(); + await actions.table.clickRefreshButton(); const mlDeprecation = esDeprecationsMockResponse.deprecations[0]; const reindexDeprecation = esDeprecationsMockResponse.deprecations[3]; - // Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 3 requests made - expect(server.requests.length).toBe(totalRequests + 3); - expect(server.requests[server.requests.length - 3].url).toBe( + // Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 4 requests made + expect(server.requests.length).toBe(totalRequests + 4); + expect(server.requests[server.requests.length - 4].url).toBe( `${API_BASE_PATH}/es_deprecations` ); - expect(server.requests[server.requests.length - 2].url).toBe( + expect(server.requests[server.requests.length - 3].url).toBe( `${API_BASE_PATH}/ml_snapshots/${(mlDeprecation.correctiveAction as MlAction).jobId}/${ (mlDeprecation.correctiveAction as MlAction).snapshotId }` ); - expect(server.requests[server.requests.length - 1].url).toBe( + expect(server.requests[server.requests.length - 2].url).toBe( `${API_BASE_PATH}/reindex/${reindexDeprecation.index}` ); + + expect(server.requests[server.requests.length - 1].url).toBe( + `${API_BASE_PATH}/ml_upgrade_mode` + ); + }); + + it('shows critical and warning deprecations count', () => { + const { find } = testBed; + const criticalDeprecations = esDeprecationsMockResponse.deprecations.filter( + (deprecation) => deprecation.isCritical + ); + const warningDeprecations = esDeprecationsMockResponse.deprecations.filter( + (deprecation) => deprecation.isCritical === false + ); + + expect(find('criticalDeprecationsCount').text()).toContain(criticalDeprecations.length); + + expect(find('warningDeprecationsCount').text()).toContain(warningDeprecations.length); }); describe('search bar', () => { it('filters results by "critical" status', async () => { const { find, actions } = testBed; - await actions.clickCriticalFilterButton(); + await actions.searchBar.clickCriticalFilterButton(); const criticalDeprecations = esDeprecationsMockResponse.deprecations.filter( (deprecation) => deprecation.isCritical @@ -88,7 +107,7 @@ describe('Deprecations table', () => { expect(find('deprecationTableRow').length).toEqual(criticalDeprecations.length); - await actions.clickCriticalFilterButton(); + await actions.searchBar.clickCriticalFilterButton(); expect(find('deprecationTableRow').length).toEqual( esDeprecationsMockResponse.deprecations.length @@ -98,7 +117,7 @@ describe('Deprecations table', () => { it('filters results by type', async () => { const { component, find, actions } = testBed; - await actions.clickTypeFilterDropdownAt(0); + await actions.searchBar.clickTypeFilterDropdownAt(0); // We need to read the document "body" as the filter dropdown options are added there and not inside // the component DOM tree. @@ -125,7 +144,7 @@ describe('Deprecations table', () => { const { find, actions } = testBed; const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2]; - await actions.setSearchInputValue(multiFieldsDeprecation.message); + await actions.searchBar.setSearchInputValue(multiFieldsDeprecation.message); expect(find('deprecationTableRow').length).toEqual(1); expect(find('deprecationTableRow').at(0).text()).toContain(multiFieldsDeprecation.message); @@ -134,7 +153,7 @@ describe('Deprecations table', () => { it('shows error for invalid search queries', async () => { const { find, exists, actions } = testBed; - await actions.setSearchInputValue('%'); + await actions.searchBar.setSearchInputValue('%'); expect(exists('invalidSearchQueryMessage')).toBe(true); expect(find('invalidSearchQueryMessage').text()).toContain('Invalid search'); @@ -143,7 +162,7 @@ describe('Deprecations table', () => { it('shows message when search query does not return results', async () => { const { find, actions, exists } = testBed; - await actions.setSearchInputValue('foobarbaz'); + await actions.searchBar.setSearchInputValue('foobarbaz'); expect(exists('noDeprecationsRow')).toBe(true); expect(find('noDeprecationsRow').text()).toContain( @@ -183,7 +202,7 @@ describe('Deprecations table', () => { expect(find('deprecationTableRow').length).toEqual(50); // Navigate to the next page - await actions.clickPaginationAt(1); + await actions.pagination.clickPaginationAt(1); // On the second (last) page, we expect to see the remaining deprecations expect(find('deprecationTableRow').length).toEqual(deprecations.length - 50); @@ -192,7 +211,7 @@ describe('Deprecations table', () => { it('allows the number of viewable rows to change', async () => { const { find, actions, component } = testBed; - await actions.clickRowsPerPageDropdown(); + await actions.pagination.clickRowsPerPageDropdown(); // We need to read the document "body" as the rows-per-page dropdown options are added there and not inside // the component DOM tree. @@ -219,7 +238,7 @@ describe('Deprecations table', () => { const criticalDeprecations = deprecations.filter((deprecation) => deprecation.isCritical); - await actions.clickCriticalFilterButton(); + await actions.searchBar.clickCriticalFilterButton(); // Only 40 critical deprecations, so only one page should show expect(find('esDeprecationsPagination').find('.euiPagination__item').length).toEqual(1); @@ -232,7 +251,7 @@ describe('Deprecations table', () => { (deprecation) => deprecation.correctiveAction?.type === 'reindex' ); - await actions.setSearchInputValue('Index created before 7.0'); + await actions.searchBar.setSearchInputValue('Index created before 7.0'); // Only 20 deprecations that match, so only one page should show expect(find('esDeprecationsPagination').find('.euiPagination__item').length).toEqual(1); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts index 8d3616a1b9d6..2f0c8f0597ec 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts @@ -7,7 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; describe('Error handling', () => { let testBed: ElasticsearchTestBed; @@ -30,13 +31,10 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('permissionsError')).toBe(true); - expect(find('permissionsError').text()).toContain( - 'You are not authorized to view Elasticsearch deprecations.' + expect(find('deprecationsPageLoadingError').text()).toContain( + 'You are not authorized to view Elasticsearch deprecation issues.' ); }); @@ -58,12 +56,11 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('upgradedCallout')).toBe(true); - expect(find('upgradedCallout').text()).toContain('All Elasticsearch nodes have been upgraded.'); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'All Elasticsearch nodes have been upgraded.' + ); }); it('shows partially upgrade error when nodes are running different versions', async () => { @@ -82,12 +79,9 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('partiallyUpgradedWarning')).toBe(true); - expect(find('partiallyUpgradedWarning').text()).toContain( + expect(find('deprecationsPageLoadingError').text()).toContain( 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' ); }); @@ -105,11 +99,10 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('requestError')).toBe(true); - expect(find('requestError').text()).toContain('Could not retrieve Elasticsearch deprecations.'); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'Could not retrieve Elasticsearch deprecation issues.' + ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts new file mode 100644 index 000000000000..9bb44a9314c5 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts @@ -0,0 +1,161 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { EsDeprecations } from '../../../public/application/components/es_deprecations'; +import { WithAppDependencies } from '../helpers'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/es_deprecations'], + componentRoutePath: '/es_deprecations', + }, + doMountAsync: true, +}; + +export type ElasticsearchTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { component, find } = testBed; + + /** + * User Actions + */ + + const table = { + clickRefreshButton: async () => { + await act(async () => { + find('refreshButton').simulate('click'); + }); + + component.update(); + }, + clickDeprecationRowAt: async ( + deprecationType: 'mlSnapshot' | 'indexSetting' | 'reindex' | 'default', + index: number + ) => { + await act(async () => { + find(`deprecation-${deprecationType}`).at(index).simulate('click'); + }); + + component.update(); + }, + }; + + const searchBar = { + clickTypeFilterDropdownAt: async (index: number) => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('searchBarContainer') + .find('.euiPopover') + .find('.euiFilterButton') + .at(index) + .simulate('click'); + }); + + component.update(); + }, + setSearchInputValue: async (searchValue: string) => { + await act(async () => { + find('searchBarContainer') + .find('input') + .simulate('keyup', { target: { value: searchValue } }); + }); + + component.update(); + }, + clickCriticalFilterButton: async () => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('searchBarContainer').find('.euiFilterButton').at(0).simulate('click'); + }); + + component.update(); + }, + }; + + const pagination = { + clickPaginationAt: async (index: number) => { + await act(async () => { + find(`pagination-button-${index}`).simulate('click'); + }); + + component.update(); + }, + clickRowsPerPageDropdown: async () => { + await act(async () => { + find('tablePaginationPopoverButton').simulate('click'); + }); + + component.update(); + }, + }; + + const mlDeprecationFlyout = { + clickUpgradeSnapshot: async () => { + await act(async () => { + find('mlSnapshotDetails.upgradeSnapshotButton').simulate('click'); + }); + + component.update(); + }, + clickDeleteSnapshot: async () => { + await act(async () => { + find('mlSnapshotDetails.deleteSnapshotButton').simulate('click'); + }); + + component.update(); + }, + }; + + const indexSettingsDeprecationFlyout = { + clickDeleteSettingsButton: async () => { + await act(async () => { + find('deleteSettingsButton').simulate('click'); + }); + + component.update(); + }, + }; + + const reindexDeprecationFlyout = { + clickReindexButton: async () => { + await act(async () => { + find('startReindexingButton').simulate('click'); + }); + + component.update(); + }, + }; + + return { + table, + searchBar, + pagination, + mlDeprecationFlyout, + reindexDeprecationFlyout, + indexSettingsDeprecationFlyout, + }; +}; + +export const setupElasticsearchPage = async ( + overrides?: Record +): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(EsDeprecations, overrides), + testBedConfig + ); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts index efeb78a50716..f62d24081ed5 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts @@ -7,8 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; - +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; describe('Index settings deprecation flyout', () => { @@ -33,27 +33,34 @@ describe('Index settings deprecation flyout', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { find, exists, actions, component } = testBed; - + const { actions, component } = testBed; component.update(); + await actions.table.clickDeprecationRowAt('indexSetting', 0); + }); - await actions.clickIndexSettingsDeprecationAt(0); + test('renders a flyout with deprecation details', async () => { + const { find, exists } = testBed; expect(exists('indexSettingsDetails')).toBe(true); expect(find('indexSettingsDetails.flyoutTitle').text()).toContain( indexSettingDeprecation.message ); + expect(find('indexSettingsDetails.documentationLink').props().href).toBe( + indexSettingDeprecation.url + ); expect(exists('removeSettingsPrompt')).toBe(true); }); it('removes deprecated index settings', async () => { - const { find, actions } = testBed; + const { find, actions, exists } = testBed; httpRequestsMockHelpers.setUpdateIndexSettingsResponse({ acknowledged: true, }); - await actions.clickDeleteSettingsButton(); + expect(exists('indexSettingsDetails.warningDeprecationBadge')).toBe(true); + + await actions.indexSettingsDeprecationFlyout.clickDeleteSettingsButton(); const request = server.requests[server.requests.length - 1]; @@ -69,12 +76,14 @@ describe('Index settings deprecation flyout', () => { ); // Reopen the flyout - await actions.clickIndexSettingsDeprecationAt(0); + await actions.table.clickDeprecationRowAt('indexSetting', 0); // Verify prompt to remove setting no longer displays expect(find('removeSettingsPrompt').length).toEqual(0); // Verify the action button no longer displays expect(find('indexSettingsDetails.deleteSettingsButton').length).toEqual(0); + // Verify the badge got marked as resolved + expect(exists('indexSettingsDetails.resolvedDeprecationBadge')).toBe(true); }); it('handles failure', async () => { @@ -87,7 +96,7 @@ describe('Index settings deprecation flyout', () => { httpRequestsMockHelpers.setUpdateIndexSettingsResponse(undefined, error); - await actions.clickDeleteSettingsButton(); + await actions.indexSettingsDeprecationFlyout.clickDeleteSettingsButton(); const request = server.requests[server.requests.length - 1]; @@ -103,7 +112,7 @@ describe('Index settings deprecation flyout', () => { ); // Reopen the flyout - await actions.clickIndexSettingsDeprecationAt(0); + await actions.table.clickDeprecationRowAt('indexSetting', 0); // Verify the flyout shows an error message expect(find('indexSettingsDetails.deleteSettingsError').text()).toContain( diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts index 909976355cd3..b24cd5a69a28 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts @@ -8,7 +8,8 @@ import { act } from 'react-dom/test-utils'; import type { MlAction } from '../../../common/types'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; describe('Machine learning deprecation flyout', () => { @@ -22,6 +23,7 @@ describe('Machine learning deprecation flyout', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsMockResponse); + httpRequestsMockHelpers.setLoadMlUpgradeModeResponse({ mlUpgradeModeEnabled: false }); httpRequestsMockHelpers.setUpgradeMlSnapshotStatusResponse({ nodeId: 'my_node', snapshotId: MOCK_SNAPSHOT_ID, @@ -33,16 +35,19 @@ describe('Machine learning deprecation flyout', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { find, exists, actions, component } = testBed; - + const { actions, component } = testBed; component.update(); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + }); - await actions.clickMlDeprecationAt(0); + test('renders a flyout with deprecation details', async () => { + const { find, exists } = testBed; expect(exists('mlSnapshotDetails')).toBe(true); expect(find('mlSnapshotDetails.flyoutTitle').text()).toContain( 'Upgrade or delete model snapshot' ); + expect(find('mlSnapshotDetails.documentationLink').props().href).toBe(mlDeprecation.url); }); describe('upgrade snapshots', () => { @@ -63,9 +68,10 @@ describe('Machine learning deprecation flyout', () => { status: 'complete', }); + expect(exists('mlSnapshotDetails.criticalDeprecationBadge')).toBe(true); expect(find('mlSnapshotDetails.upgradeSnapshotButton').text()).toEqual('Upgrade'); - await actions.clickUpgradeMlSnapshot(); + await actions.mlDeprecationFlyout.clickUpgradeSnapshot(); // First, we expect a POST request to upgrade the snapshot const upgradeRequest = server.requests[server.requests.length - 2]; @@ -83,11 +89,13 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').text()).toContain('Upgrade complete'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); - // Flyout actions should not be visible if deprecation was resolved + // Flyout actions should be hidden if deprecation was resolved expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + // Badge should be updated in flyout title + expect(exists('mlSnapshotDetails.resolvedDeprecationBadge')).toBe(true); }); it('handles upgrade failure', async () => { @@ -108,7 +116,7 @@ describe('Machine learning deprecation flyout', () => { error, }); - await actions.clickUpgradeMlSnapshot(); + await actions.mlDeprecationFlyout.clickUpgradeSnapshot(); const upgradeRequest = server.requests[server.requests.length - 1]; expect(upgradeRequest.method).toBe('POST'); @@ -118,7 +126,7 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').text()).toContain('Upgrade failed'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); // Verify the flyout shows an error message expect(find('mlSnapshotDetails.resolveSnapshotError').text()).toContain( @@ -127,19 +135,41 @@ describe('Machine learning deprecation flyout', () => { // Verify the upgrade button text changes expect(find('mlSnapshotDetails.upgradeSnapshotButton').text()).toEqual('Retry upgrade'); }); + + it('Disables actions if ml_upgrade_mode is enabled', async () => { + httpRequestsMockHelpers.setLoadMlUpgradeModeResponse({ mlUpgradeModeEnabled: true }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + const { actions, exists, component } = testBed; + + component.update(); + + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + + // Shows an error callout with a docs link + expect(exists('mlSnapshotDetails.mlUpgradeModeEnabledError')).toBe(true); + expect(exists('mlSnapshotDetails.setUpgradeModeDocsLink')).toBe(true); + // Flyout actions should be hidden + expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); + expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + }); }); describe('delete snapshots', () => { it('successfully deletes snapshots', async () => { - const { find, actions } = testBed; + const { find, actions, exists } = testBed; httpRequestsMockHelpers.setDeleteMlSnapshotResponse({ acknowledged: true, }); + expect(exists('mlSnapshotDetails.criticalDeprecationBadge')).toBe(true); expect(find('mlSnapshotDetails.deleteSnapshotButton').text()).toEqual('Delete'); - await actions.clickDeleteMlSnapshot(); + await actions.mlDeprecationFlyout.clickDeleteSnapshot(); const request = server.requests[server.requests.length - 1]; @@ -154,7 +184,13 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').at(0).text()).toEqual('Deletion complete'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + + // Flyout actions should be hidden if deprecation was resolved + expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); + expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + // Badge should be updated in flyout title + expect(exists('mlSnapshotDetails.resolvedDeprecationBadge')).toBe(true); }); it('handles delete failure', async () => { @@ -168,7 +204,7 @@ describe('Machine learning deprecation flyout', () => { httpRequestsMockHelpers.setDeleteMlSnapshotResponse(undefined, error); - await actions.clickDeleteMlSnapshot(); + await actions.mlDeprecationFlyout.clickDeleteSnapshot(); const request = server.requests[server.requests.length - 1]; @@ -183,7 +219,7 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').at(0).text()).toEqual('Deletion failed'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); // Verify the flyout shows an error message expect(find('mlSnapshotDetails.resolveSnapshotError').text()).toContain( diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts index c93cdcb1f4d9..3c6fe0e5f532 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts @@ -7,9 +7,10 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; - +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; +import { ReindexStatus, ReindexStep } from '../../../common/types'; // Note: The reindexing flyout UX is subject to change; more tests should be added here once functionality is built out describe('Reindex deprecation flyout', () => { @@ -40,11 +41,163 @@ describe('Reindex deprecation flyout', () => { const reindexDeprecation = esDeprecationsMockResponse.deprecations[3]; const { actions, find, exists } = testBed; - await actions.clickReindexDeprecationAt(0); + await actions.table.clickDeprecationRowAt('reindex', 0); expect(exists('reindexDetails')).toBe(true); expect(find('reindexDetails.flyoutTitle').text()).toContain( `Reindex ${reindexDeprecation.index}` ); }); + + it('renders error callout when reindex fails', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: null, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + + const { actions, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + httpRequestsMockHelpers.setStartReindexingResponse(undefined, { + statusCode: 404, + message: 'no such index [test]', + }); + + await actions.reindexDeprecationFlyout.clickReindexButton(); + + expect(exists('reindexDetails.reindexingFailedCallout')).toBe(true); + }); + + it('renders error callout when fetch status fails', async () => { + httpRequestsMockHelpers.setReindexStatusResponse(undefined, { + statusCode: 404, + message: 'no such index [test]', + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + + const { actions, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + 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({ + reindexOp: { + status: ReindexStatus.inProgress, + lastCompletedStep: ReindexStep.readonly, + reindexTaskPercComplete: null, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 5%'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + + it('has started reindexing documents', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.inProgress, + lastCompletedStep: ReindexStep.reindexStarted, + reindexTaskPercComplete: 0.25, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 31%'); + expect(exists('cancelReindexingDocumentsButton')).toBe(true); + }); + + it('has completed reindexing documents', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.inProgress, + lastCompletedStep: ReindexStep.reindexCompleted, + reindexTaskPercComplete: 1, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 95%'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + + it('has completed', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.completed, + lastCompletedStep: ReindexStep.aliasCreated, + reindexTaskPercComplete: 1, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing process'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts new file mode 100644 index 000000000000..3fa6be18a9b3 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts @@ -0,0 +1,86 @@ +/* + * 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 SemVer from 'semver/classes/semver'; +import { + deprecationsServiceMock, + docLinksServiceMock, + notificationServiceMock, + applicationServiceMock, + httpServiceMock, + coreMock, + scopedHistoryMock, +} from 'src/core/public/mocks'; +import { sharePluginMock } from 'src/plugins/share/public/mocks'; + +import { apiService } from '../../../public/application/lib/api'; +import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { cloudMock } from '../../../../../../x-pack/plugins/cloud/public/mocks'; + +const servicesMock = { + api: apiService, + breadcrumbs: breadcrumbService, + data: dataPluginMock.createStartContract(), +}; + +// We'll mock these values to avoid testing the locators themselves. +const idToUrlMap = { + SNAPSHOT_RESTORE_LOCATOR: 'snapshotAndRestoreUrl', + DISCOVER_APP_LOCATOR: 'discoverUrl', +}; +type IdKey = keyof typeof idToUrlMap; + +const stringifySearchParams = (params: Record) => { + const stringifiedParams = Object.keys(params).reduce((list, key) => { + const value = typeof params[key] === 'object' ? JSON.stringify(params[key]) : params[key]; + + return { ...list, [key]: value }; + }, {}); + + return new URLSearchParams(stringifiedParams).toString(); +}; + +const shareMock = sharePluginMock.createSetupContract(); +// @ts-expect-error This object is missing some properties that we're not using in the UI +shareMock.url.locators.get = (id: IdKey) => ({ + useUrl: (): string | undefined => idToUrlMap[id], + getUrl: (params: Record): string | undefined => + `${idToUrlMap[id]}?${stringifySearchParams(params)}`, +}); + +export const getAppContextMock = (kibanaVersion: SemVer) => ({ + isReadOnlyMode: false, + kibanaVersionInfo: { + currentMajor: kibanaVersion.major, + prevMajor: kibanaVersion.major - 1, + nextMajor: kibanaVersion.major + 1, + }, + services: { + ...servicesMock, + core: { + ...coreMock.createStart(), + http: httpServiceMock.createSetupContract(), + deprecations: deprecationsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + docLinks: docLinksServiceMock.createStartContract(), + history: scopedHistoryMock.create(), + application: applicationServiceMock.createStartContract(), + }, + }, + plugins: { + share: shareMock, + infra: undefined, + cloud: { + ...cloudMock.createSetup(), + isCloudEnabled: false, + }, + }, + clusterUpgradeState: 'isPreparingForUpgrade', + isClusterUpgradeStateError: () => {}, + handleClusterUpgradeStateError: () => {}, +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts deleted file mode 100644 index 86737d492592..000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts +++ /dev/null @@ -1,171 +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 { act } from 'react-dom/test-utils'; - -import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { EsDeprecations } from '../../../public/application/components/es_deprecations'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: ['/es_deprecations'], - componentRoutePath: '/es_deprecations', - }, - doMountAsync: true, -}; - -export type ElasticsearchTestBed = TestBed & { - actions: ReturnType; -}; - -const createActions = (testBed: TestBed) => { - const { component, find } = testBed; - - /** - * User Actions - */ - const clickRefreshButton = async () => { - await act(async () => { - find('refreshButton').simulate('click'); - }); - - component.update(); - }; - - const clickMlDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-mlSnapshot').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickUpgradeMlSnapshot = async () => { - await act(async () => { - find('mlSnapshotDetails.upgradeSnapshotButton').simulate('click'); - }); - - component.update(); - }; - - const clickDeleteMlSnapshot = async () => { - await act(async () => { - find('mlSnapshotDetails.deleteSnapshotButton').simulate('click'); - }); - - component.update(); - }; - - const clickIndexSettingsDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-indexSetting').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickDeleteSettingsButton = async () => { - await act(async () => { - find('deleteSettingsButton').simulate('click'); - }); - - component.update(); - }; - - const clickReindexDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-reindex').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickDefaultDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-default').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickCriticalFilterButton = async () => { - await act(async () => { - // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector - find('searchBarContainer').find('.euiFilterButton').at(0).simulate('click'); - }); - - component.update(); - }; - - const clickTypeFilterDropdownAt = async (index: number) => { - await act(async () => { - // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector - find('searchBarContainer') - .find('.euiPopover') - .find('.euiFilterButton') - .at(index) - .simulate('click'); - }); - - component.update(); - }; - - const setSearchInputValue = async (searchValue: string) => { - await act(async () => { - find('searchBarContainer') - .find('input') - .simulate('keyup', { target: { value: searchValue } }); - }); - - component.update(); - }; - - const clickPaginationAt = async (index: number) => { - await act(async () => { - find(`pagination-button-${index}`).simulate('click'); - }); - - component.update(); - }; - - const clickRowsPerPageDropdown = async () => { - await act(async () => { - find('tablePaginationPopoverButton').simulate('click'); - }); - - component.update(); - }; - - return { - clickRefreshButton, - clickMlDeprecationAt, - clickUpgradeMlSnapshot, - clickDeleteMlSnapshot, - clickIndexSettingsDeprecationAt, - clickDeleteSettingsButton, - clickReindexDeprecationAt, - clickDefaultDeprecationAt, - clickCriticalFilterButton, - clickTypeFilterDropdownAt, - setSearchInputValue, - clickPaginationAt, - clickRowsPerPageDropdown, - }; -}; - -export const setup = async (overrides?: Record): Promise => { - const initTestBed = registerTestBed( - WithAppDependencies(EsDeprecations, overrides), - testBedConfig - ); - const testBed = await initTestBed(); - - return { - ...testBed, - actions: createActions(testBed), - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index d0c93d74f31f..7903ca58ac18 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -6,12 +6,31 @@ */ import sinon, { SinonFakeServer } from 'sinon'; + import { API_BASE_PATH } from '../../../common/constants'; -import { ESUpgradeStatus, DeprecationLoggingStatus } from '../../../common/types'; -import { ResponseError } from '../../../public/application/lib/api'; +import { + CloudBackupStatus, + ESUpgradeStatus, + DeprecationLoggingStatus, + ResponseError, +} from '../../../common/types'; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setLoadCloudBackupStatusResponse = ( + response?: CloudBackupStatus, + error?: ResponseError + ) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/cloud_backup_status`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setLoadEsDeprecationsResponse = (response?: ESUpgradeStatus, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -37,6 +56,30 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadDeprecationLogsCountResponse = ( + response?: { count: number }, + error?: ResponseError + ) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/deprecation_logging/count`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setDeleteLogsCacheResponse = (response?: string, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + server.respondWith('DELETE', `${API_BASE_PATH}/deprecation_logging/cache`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setUpdateDeprecationLoggingResponse = ( response?: DeprecationLoggingStatus, error?: ResponseError @@ -83,6 +126,28 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setReindexStatusResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/reindex/:indexName`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setStartReindexingResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('POST', `${API_BASE_PATH}/reindex/:indexName`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setDeleteMlSnapshotResponse = (response?: object, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -94,7 +159,41 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadSystemIndicesMigrationStatus = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/system_indices_migration`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setLoadMlUpgradeModeResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/ml_upgrade_mode`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setSystemIndicesMigrationResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('POST', `${API_BASE_PATH}/system_indices_migration`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { + setLoadCloudBackupStatusResponse, setLoadEsDeprecationsResponse, setLoadDeprecationLoggingResponse, setUpdateDeprecationLoggingResponse, @@ -102,6 +201,13 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setUpgradeMlSnapshotResponse, setDeleteMlSnapshotResponse, setUpgradeMlSnapshotStatusResponse, + setLoadDeprecationLogsCountResponse, + setLoadSystemIndicesMigrationStatus, + setSystemIndicesMigrationResponse, + setDeleteLogsCacheResponse, + setStartReindexingResponse, + setReindexStatusResponse, + setLoadMlUpgradeModeResponse, }; }; @@ -116,8 +222,19 @@ export const init = () => { const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + const setServerAsync = (isAsync: boolean, timeout: number = 200) => { + if (isAsync) { + server.autoRespond = true; + server.autoRespondAfter = 1000; + server.respondImmediately = false; + } else { + server.respondImmediately = true; + } + }; + return { server, + setServerAsync, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts index 2d3fff9d43e2..f70bfd00e9c0 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts @@ -5,11 +5,5 @@ * 2.0. */ -export type { OverviewTestBed } from './overview.helpers'; -export { setup as setupOverviewPage } from './overview.helpers'; -export type { ElasticsearchTestBed } from './elasticsearch.helpers'; -export { setup as setupElasticsearchPage } from './elasticsearch.helpers'; -export type { KibanaTestBed } from './kibana.helpers'; -export { setup as setupKibanaPage } from './kibana.helpers'; - -export { setupEnvironment, kibanaVersion } from './setup_environment'; +export { setupEnvironment, WithAppDependencies, kibanaVersion } from './setup_environment'; +export { advanceTime } from './time_manipulation'; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts deleted file mode 100644 index 370679d7d1a7..000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts +++ /dev/null @@ -1,59 +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 { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { KibanaDeprecationsContent } from '../../../public/application/components/kibana_deprecations'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: ['/kibana_deprecations'], - componentRoutePath: '/kibana_deprecations', - }, - doMountAsync: true, -}; - -export type KibanaTestBed = TestBed & { - actions: ReturnType; -}; - -const createActions = (testBed: TestBed) => { - /** - * User Actions - */ - - const clickExpandAll = () => { - const { find } = testBed; - find('expandAll').simulate('click'); - }; - - return { - clickExpandAll, - }; -}; - -export const setup = async (overrides?: Record): Promise => { - const initTestBed = registerTestBed( - WithAppDependencies(KibanaDeprecationsContent, overrides), - testBedConfig - ); - const testBed = await initTestBed(); - - return { - ...testBed, - actions: createActions(testBed), - }; -}; - -export type KibanaTestSubjects = - | 'expandAll' - | 'noDeprecationsPrompt' - | 'kibanaPluginError' - | 'kibanaDeprecationsContent' - | 'kibanaDeprecationItem' - | 'kibanaRequestError' - | string; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts deleted file mode 100644 index 893b97c5a0ca..000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts +++ /dev/null @@ -1,30 +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 { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -import { discoverPluginMock } from '../../../../../../src/plugins/discover/public/mocks'; -import { applicationServiceMock } from '../../../../../../src/core/public/application/application_service.mock'; - -const discoverMock = discoverPluginMock.createStartContract(); - -export const servicesMock = { - data: dataPluginMock.createStartContract(), - application: applicationServiceMock.createStartContract(), - discover: { - ...discoverMock, - locator: { - ...discoverMock.locator, - getLocation: jest.fn(() => - Promise.resolve({ - app: '/discover', - path: 'logs', - state: {}, - }) - ), - }, - }, -}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index fbbbc0e07853..0e4af0b697a4 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -7,24 +7,21 @@ import React from 'react'; import axios from 'axios'; +import SemVer from 'semver/classes/semver'; +import { merge } from 'lodash'; // @ts-ignore import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import SemVer from 'semver/classes/semver'; -import { - deprecationsServiceMock, - docLinksServiceMock, - notificationServiceMock, - applicationServiceMock, -} from 'src/core/public/mocks'; -import { HttpSetup } from 'src/core/public'; -import { KibanaContextProvider } from '../../../public/shared_imports'; +import { HttpSetup } from 'src/core/public'; import { MAJOR_VERSION } from '../../../common/constants'; + +import { AuthorizationContext, Authorization, Privileges } from '../../../public/shared_imports'; import { AppContextProvider } from '../../../public/application/app_context'; import { apiService } from '../../../public/application/lib/api'; import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; import { GlobalFlyout } from '../../../public/shared_imports'; -import { servicesMock } from './services_mock'; +import { AppDependencies } from '../../../public/types'; +import { getAppContextMock } from './app_context.mock'; import { init as initHttpRequests } from './http_requests'; const { GlobalFlyoutProvider } = GlobalFlyout; @@ -33,46 +30,40 @@ const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); export const kibanaVersion = new SemVer(MAJOR_VERSION); +const createAuthorizationContextValue = (privileges: Privileges) => { + return { + isLoading: false, + privileges: privileges ?? { hasAllPrivileges: false, missingPrivileges: {} }, + } as Authorization; +}; + export const WithAppDependencies = - (Comp: any, overrides: Record = {}) => + (Comp: any, { privileges, ...overrides }: Record = {}) => (props: Record) => { apiService.setup(mockHttpClient as unknown as HttpSetup); breadcrumbService.setup(() => ''); - const contextValue = { - http: mockHttpClient as unknown as HttpSetup, - docLinks: docLinksServiceMock.createStartContract(), - kibanaVersionInfo: { - currentMajor: kibanaVersion.major, - prevMajor: kibanaVersion.major - 1, - nextMajor: kibanaVersion.major + 1, - }, - notifications: notificationServiceMock.createStartContract(), - isReadOnlyMode: false, - api: apiService, - breadcrumbs: breadcrumbService, - getUrlForApp: applicationServiceMock.createStartContract().getUrlForApp, - deprecations: deprecationsServiceMock.createStartContract(), - }; - - const { servicesOverrides, ...contextOverrides } = overrides; + const appContextMock = getAppContextMock(kibanaVersion) as unknown as AppDependencies; return ( - - + + - + ); }; export const setupEnvironment = () => { - const { server, httpRequestsMockHelpers } = initHttpRequests(); + const { server, setServerAsync, httpRequestsMockHelpers } = initHttpRequests(); return { server, + setServerAsync, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts new file mode 100644 index 000000000000..65cec1954973 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts @@ -0,0 +1,24 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +/** + * These helpers are intended to be used in conjunction with jest.useFakeTimers(). + */ + +const flushPromiseJobQueue = async () => { + // See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function + await Promise.resolve(); +}; + +export const advanceTime = async (ms: number) => { + await act(async () => { + jest.advanceTimersByTime(ms); + await flushPromiseJobQueue(); + }); +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts deleted file mode 100644 index ffac7a14804a..000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from 'src/core/public/mocks'; - -import { KibanaTestBed, setupKibanaPage, setupEnvironment } from './helpers'; - -describe('Kibana deprecations', () => { - let testBed: KibanaTestBed; - const { server } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - describe('With deprecations', () => { - const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ - { - title: 'mock-deprecation-title', - correctiveActions: { - manualSteps: ['Step 1', 'Step 2', 'Step 3'], - api: { - method: 'POST', - path: '/test', - }, - }, - domainId: 'test_domain', - level: 'critical', - message: 'Test deprecation message', - }, - ]; - - beforeEach(async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockReturnValue(kibanaDeprecationsMockResponse); - - testBed = await setupKibanaPage({ - deprecations: deprecationService, - }); - }); - - testBed.component.update(); - }); - - test('renders deprecations', () => { - const { exists, find } = testBed; - expect(exists('kibanaDeprecationsContent')).toBe(true); - expect(find('kibanaDeprecationItem').length).toEqual(1); - }); - - describe('manual steps modal', () => { - test('renders modal with a list of steps to fix a deprecation', async () => { - const { component, actions, exists, find } = testBed; - const deprecation = kibanaDeprecationsMockResponse[0]; - - expect(exists('kibanaDeprecationsContent')).toBe(true); - - // Open all deprecations - actions.clickExpandAll(); - - const accordionTestSubj = `${deprecation.domainId}Deprecation`; - - await act(async () => { - find(`${accordionTestSubj}.stepsButton`).simulate('click'); - }); - - component.update(); - - // We need to read the document "body" as the modal is added there and not inside - // the component DOM tree. - let modal = document.body.querySelector('[data-test-subj="stepsModal"]'); - - expect(modal).not.toBeNull(); - expect(modal!.textContent).toContain(`Resolve deprecation in '${deprecation.domainId}'`); - - const steps: NodeListOf | null = modal!.querySelectorAll( - '[data-test-subj="fixDeprecationSteps"] .euiStep' - ); - - expect(steps).not.toBe(null); - expect(steps.length).toEqual(deprecation!.correctiveActions!.manualSteps!.length); - - await act(async () => { - const closeButton: HTMLButtonElement | null = modal!.querySelector( - '[data-test-subj="closeButton"]' - ); - - closeButton!.click(); - }); - - component.update(); - - // Confirm modal closed and no longer appears in the DOM - modal = document.body.querySelector('[data-test-subj="stepsModal"]'); - expect(modal).toBe(null); - }); - }); - - describe('resolve modal', () => { - test('renders confirmation modal to resolve a deprecation', async () => { - const { component, actions, exists, find } = testBed; - const deprecation = kibanaDeprecationsMockResponse[0]; - - expect(exists('kibanaDeprecationsContent')).toBe(true); - - // Open all deprecations - actions.clickExpandAll(); - - const accordionTestSubj = `${deprecation.domainId}Deprecation`; - - await act(async () => { - find(`${accordionTestSubj}.resolveButton`).simulate('click'); - }); - - component.update(); - - // We need to read the document "body" as the modal is added there and not inside - // the component DOM tree. - let modal = document.body.querySelector('[data-test-subj="resolveModal"]'); - - expect(modal).not.toBe(null); - expect(modal!.textContent).toContain(`Resolve deprecation in '${deprecation.domainId}'`); - - const confirmButton: HTMLButtonElement | null = modal!.querySelector( - '[data-test-subj="confirmModalConfirmButton"]' - ); - - await act(async () => { - confirmButton!.click(); - }); - - component.update(); - - // Confirm modal should close and no longer appears in the DOM - modal = document.body.querySelector('[data-test-subj="resolveModal"]'); - expect(modal).toBe(null); - }); - }); - }); - - describe('No deprecations', () => { - beforeEach(async () => { - await act(async () => { - testBed = await setupKibanaPage({ isReadOnlyMode: false }); - }); - - const { component } = testBed; - - component.update(); - }); - - test('renders prompt', () => { - const { exists, find } = testBed; - expect(exists('noDeprecationsPrompt')).toBe(true); - expect(find('noDeprecationsPrompt').text()).toContain( - 'Your Kibana configuration is up to date' - ); - }); - }); - - describe('Error handling', () => { - test('handles request error', async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockRejectedValue(new Error('Internal Server Error')); - - testBed = await setupKibanaPage({ - deprecations: deprecationService, - }); - }); - - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('kibanaRequestError')).toBe(true); - expect(find('kibanaRequestError').text()).toContain('Could not retrieve Kibana deprecations'); - }); - - test('handles deprecation service error', async () => { - const domainId = 'test'; - const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ - { - domainId, - title: `Failed to fetch deprecations for ${domainId}`, - message: `Failed to get deprecations info for plugin "${domainId}".`, - level: 'fetch_error', - correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], - }, - }, - ]; - - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockReturnValue(kibanaDeprecationsMockResponse); - - testBed = await setupKibanaPage({ - deprecations: deprecationService, - }); - }); - - const { component, exists, find, actions } = testBed; - component.update(); - - // Verify top-level callout renders - expect(exists('kibanaPluginError')).toBe(true); - expect(find('kibanaPluginError').text()).toContain( - 'Not all Kibana deprecations were retrieved successfully' - ); - - // Open all deprecations - actions.clickExpandAll(); - - // Verify callout also displays for deprecation with error - expect(exists(`${domainId}Error`)).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts new file mode 100644 index 000000000000..9677104a6e55 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../service.mock'; +import { KibanaTestBed, setupKibanaPage } from '../kibana_deprecations.helpers'; + +describe('Kibana deprecations - Deprecation details flyout', () => { + let testBed: KibanaTestBed; + const { server } = setupEnvironment(); + const { + defaultMockedResponses: { mockedKibanaDeprecations }, + } = kibanaDeprecationsServiceHelpers; + const deprecationService = deprecationsServiceMock.createStartContract(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + describe('Deprecation with manual steps', () => { + test('renders flyout with single manual step as a standalone paragraph', async () => { + const { find, exists, actions } = testBed; + const manualDeprecation = mockedKibanaDeprecations[1]; + + await actions.table.clickDeprecationAt(0); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); + expect(find('manualStep').length).toBe(1); + }); + + test('renders flyout with multiple manual steps as a list', async () => { + const { find, exists, actions } = testBed; + const manualDeprecation = mockedKibanaDeprecations[1]; + + await actions.table.clickDeprecationAt(1); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); + expect(find('manualStepsListItem').length).toBe(3); + }); + + test(`doesn't show corrective actions title and steps if there aren't any`, async () => { + const { find, exists, actions } = testBed; + const manualDeprecation = mockedKibanaDeprecations[2]; + + await actions.table.clickDeprecationAt(2); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(exists('kibanaDeprecationDetails.manualStepsTitle')).toBe(false); + expect(exists('manualStepsListItem')).toBe(false); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); + }); + }); + + test('Shows documentationUrl when present', async () => { + const { find, actions } = testBed; + const deprecation = mockedKibanaDeprecations[1]; + + await actions.table.clickDeprecationAt(1); + + expect(find('kibanaDeprecationDetails.documentationLink').props().href).toBe( + deprecation.documentationUrl + ); + }); + + describe('Deprecation with automatic resolution', () => { + test('resolves deprecation successfully', async () => { + const { find, exists, actions } = testBed; + const quickResolveDeprecation = mockedKibanaDeprecations[0]; + + await actions.table.clickDeprecationAt(0); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe( + quickResolveDeprecation.title + ); + + // Quick resolve callout and button should display + expect(exists('quickResolveCallout')).toBe(true); + expect(exists('resolveButton')).toBe(true); + + await actions.flyout.clickResolveButton(); + + // Flyout should close after button click + expect(exists('kibanaDeprecationDetails')).toBe(false); + + // Reopen the flyout + await actions.table.clickDeprecationAt(0); + + // Resolve information should not display and Quick resolve button should be disabled + expect(exists('resolveSection')).toBe(false); + expect(exists('resolveButton')).toBe(false); + // Badge should be updated in flyout title + expect(exists('kibanaDeprecationDetails.resolvedDeprecationBadge')).toBe(true); + }); + + test('handles resolve failure', async () => { + const { find, exists, actions } = testBed; + const quickResolveDeprecation = mockedKibanaDeprecations[0]; + + kibanaDeprecationsServiceHelpers.setResolveDeprecations({ + deprecationService, + status: 'fail', + }); + + await actions.table.clickDeprecationAt(0); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe( + quickResolveDeprecation.title + ); + + // Quick resolve callout and button should display + expect(exists('quickResolveCallout')).toBe(true); + expect(exists('resolveButton')).toBe(true); + + await actions.flyout.clickResolveButton(); + + // Flyout should close after button click + expect(exists('kibanaDeprecationDetails')).toBe(false); + + // Reopen the flyout + await actions.table.clickDeprecationAt(0); + + // Verify error displays + expect(exists('quickResolveError')).toBe(true); + // Resolve information should display and Quick resolve button should be enabled + expect(exists('resolveSection')).toBe(true); + // Badge should remain the same + expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); + expect(find('resolveButton').props().disabled).toBe(false); + expect(find('resolveButton').text()).toContain('Try again'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts new file mode 100644 index 000000000000..a14d6e087b01 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts @@ -0,0 +1,127 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; +import type { DeprecationsServiceStart } from 'kibana/public'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../service.mock'; +import { KibanaTestBed, setupKibanaPage } from '../kibana_deprecations.helpers'; + +describe('Kibana deprecations - Deprecations table', () => { + let testBed: KibanaTestBed; + let deprecationService: jest.Mocked; + + const { server } = setupEnvironment(); + const { + mockedKibanaDeprecations, + mockedCriticalKibanaDeprecations, + mockedWarningKibanaDeprecations, + mockedConfigKibanaDeprecations, + } = kibanaDeprecationsServiceHelpers.defaultMockedResponses; + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + deprecationService = deprecationsServiceMock.createStartContract(); + + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + test('renders deprecations', () => { + const { exists, table } = testBed; + + expect(exists('kibanaDeprecations')).toBe(true); + + const { tableCellsValues } = table.getMetaData('kibanaDeprecationsTable'); + + expect(tableCellsValues.length).toEqual(mockedKibanaDeprecations.length); + }); + + it('refreshes deprecation data', async () => { + const { actions } = testBed; + + expect(deprecationService.getAllDeprecations).toHaveBeenCalledTimes(1); + + await actions.table.clickRefreshButton(); + + expect(deprecationService.getAllDeprecations).toHaveBeenCalledTimes(2); + }); + + it('shows critical and warning deprecations count', () => { + const { find } = testBed; + + expect(find('criticalDeprecationsCount').text()).toContain( + mockedCriticalKibanaDeprecations.length + ); + expect(find('warningDeprecationsCount').text()).toContain( + mockedWarningKibanaDeprecations.length + ); + }); + + describe('Search bar', () => { + it('filters by "critical" status', async () => { + const { actions, table } = testBed; + + // Show only critical deprecations + await actions.searchBar.clickCriticalFilterButton(); + const { rows: criticalRows } = table.getMetaData('kibanaDeprecationsTable'); + expect(criticalRows.length).toEqual(mockedCriticalKibanaDeprecations.length); + + // Show all deprecations + await actions.searchBar.clickCriticalFilterButton(); + const { rows: allRows } = table.getMetaData('kibanaDeprecationsTable'); + expect(allRows.length).toEqual(mockedKibanaDeprecations.length); + }); + + it('filters by type', async () => { + const { table, actions } = testBed; + + await actions.searchBar.openTypeFilterDropdown(); + await actions.searchBar.filterByConfigType(); + + const { rows: configRows } = table.getMetaData('kibanaDeprecationsTable'); + + expect(configRows.length).toEqual(mockedConfigKibanaDeprecations.length); + }); + }); + + describe('No deprecations', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setupKibanaPage({ isReadOnlyMode: false }); + }); + + const { component } = testBed; + + component.update(); + }); + + test('renders prompt', () => { + const { exists, find } = testBed; + expect(exists('noDeprecationsPrompt')).toBe(true); + expect(find('noDeprecationsPrompt').text()).toContain( + 'Your Kibana configuration is up to date' + ); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts new file mode 100644 index 000000000000..918ee759a0f4 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../service.mock'; +import { KibanaTestBed, setupKibanaPage } from '../kibana_deprecations.helpers'; + +describe('Kibana deprecations - Deprecations table - Error handling', () => { + let testBed: KibanaTestBed; + const { server } = setupEnvironment(); + const deprecationService = deprecationsServiceMock.createStartContract(); + + afterAll(() => { + server.restore(); + }); + + test('handles plugin errors', async () => { + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ + deprecationService, + response: [ + ...kibanaDeprecationsServiceHelpers.defaultMockedResponses.mockedKibanaDeprecations, + { + domainId: 'failed_plugin_id_1', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: `Failed to get deprecations info for plugin "failed_plugin_id".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + { + domainId: 'failed_plugin_id_1', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: `Failed to get deprecations info for plugin "failed_plugin_id".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + { + domainId: 'failed_plugin_id_2', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: `Failed to get deprecations info for plugin "failed_plugin_id".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ], + }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('kibanaDeprecationErrors')).toBe(true); + expect(find('kibanaDeprecationErrors').text()).toContain( + 'Failed to get deprecation issues for these plugins: failed_plugin_id_1, failed_plugin_id_2.' + ); + }); + + test('handles request error', async () => { + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ + deprecationService, + mockRequestErrorMessage: 'Internal Server Error', + }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'Could not retrieve Kibana deprecation issues' + ); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts new file mode 100644 index 000000000000..345a06d3d80a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts @@ -0,0 +1,127 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { registerTestBed, TestBed, TestBedConfig, findTestSubject } from '@kbn/test/jest'; +import { KibanaDeprecations } from '../../../public/application/components'; +import { WithAppDependencies } from '../helpers'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/kibana_deprecations'], + componentRoutePath: '/kibana_deprecations', + }, + doMountAsync: true, +}; + +export type KibanaTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { component, find, table } = testBed; + + /** + * User Actions + */ + const tableActions = { + clickRefreshButton: async () => { + await act(async () => { + find('refreshButton').simulate('click'); + }); + + component.update(); + }, + + clickDeprecationAt: async (index: number) => { + const { rows } = table.getMetaData('kibanaDeprecationsTable'); + + const deprecationDetailsLink = findTestSubject( + rows[index].reactWrapper, + 'deprecationDetailsLink' + ); + + await act(async () => { + deprecationDetailsLink.simulate('click'); + }); + component.update(); + }, + }; + + const searchBarActions = { + openTypeFilterDropdown: async () => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('kibanaDeprecations') + .find('.euiSearchBar__filtersHolder') + .find('.euiPopover') + .find('.euiFilterButton') + .at(0) + .simulate('click'); + }); + + component.update(); + }, + + clickCriticalFilterButton: async () => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('kibanaDeprecations') + .find('.euiSearchBar__filtersHolder') + .find('.euiFilterButton') + .at(0) + .simulate('click'); + }); + + component.update(); + }, + + filterByConfigType: async () => { + // We need to read the document "body" as the filter dropdown options are added there and not inside + // the component DOM tree. The "Config" option is expected to be the first item. + const configTypeFilterButton: HTMLButtonElement | null = document.body.querySelector( + '.euiFilterSelect__items .euiFilterSelectItem' + ); + + await act(async () => { + configTypeFilterButton!.click(); + }); + + component.update(); + }, + }; + + const flyoutActions = { + clickResolveButton: async () => { + await act(async () => { + find('resolveButton').simulate('click'); + }); + + component.update(); + }, + }; + + return { + table: tableActions, + flyout: flyoutActions, + searchBar: searchBarActions, + }; +}; + +export const setupKibanaPage = async ( + overrides?: Record +): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(KibanaDeprecations, overrides), + testBedConfig + ); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts new file mode 100644 index 000000000000..6a3d376aceca --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeprecationsServiceStart, DomainDeprecationDetails } from 'kibana/public'; + +const kibanaDeprecations: DomainDeprecationDetails[] = [ + { + correctiveActions: { + // Only has one manual step. + manualSteps: ['Step 1'], + api: { + method: 'POST', + path: '/test', + }, + }, + domainId: 'test_domain_1', + level: 'critical', + title: 'Test deprecation title 1', + message: 'Test deprecation message 1', + deprecationType: 'config', + configPath: 'test', + }, + { + correctiveActions: { + // Has multiple manual steps. + manualSteps: ['Step 1', 'Step 2', 'Step 3'], + }, + domainId: 'test_domain_2', + level: 'warning', + title: 'Test deprecation title 1', + documentationUrl: 'https://', + message: 'Test deprecation message 2', + deprecationType: 'feature', + }, + { + correctiveActions: { + // Has no manual steps. + manualSteps: [], + }, + domainId: 'test_domain_3', + level: 'warning', + title: 'Test deprecation title 3', + message: 'Test deprecation message 3', + deprecationType: 'feature', + }, +]; + +const setLoadDeprecations = ({ + deprecationService, + response, + mockRequestErrorMessage, +}: { + deprecationService: jest.Mocked; + response?: DomainDeprecationDetails[]; + mockRequestErrorMessage?: string; +}) => { + const mockResponse = response ? response : kibanaDeprecations; + + if (mockRequestErrorMessage) { + return deprecationService.getAllDeprecations.mockRejectedValue( + new Error(mockRequestErrorMessage) + ); + } + + return deprecationService.getAllDeprecations.mockReturnValue(Promise.resolve(mockResponse)); +}; + +const setResolveDeprecations = ({ + deprecationService, + status, +}: { + deprecationService: jest.Mocked; + status: 'ok' | 'fail'; +}) => { + if (status === 'fail') { + return deprecationService.resolveDeprecation.mockReturnValue( + Promise.resolve({ + status, + reason: 'resolve failed', + }) + ); + } + + return deprecationService.resolveDeprecation.mockReturnValue( + Promise.resolve({ + status, + }) + ); +}; + +export const kibanaDeprecationsServiceHelpers = { + setLoadDeprecations, + setResolveDeprecations, + defaultMockedResponses: { + mockedKibanaDeprecations: kibanaDeprecations, + mockedCriticalKibanaDeprecations: kibanaDeprecations.filter( + (deprecation) => deprecation.level === 'critical' + ), + mockedWarningKibanaDeprecations: kibanaDeprecations.filter( + (deprecation) => deprecation.level === 'warning' + ), + mockedConfigKibanaDeprecations: kibanaDeprecations.filter( + (deprecation) => deprecation.deprecationType === 'config' + ), + }, +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx new file mode 100644 index 000000000000..3dcc55adbe61 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx @@ -0,0 +1,176 @@ +/* + * 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 { CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS } from '../../../../common/constants'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; + +describe('Overview - Backup Step', () => { + let testBed: OverviewTestBed; + let server: ReturnType['server']; + let setServerAsync: ReturnType['setServerAsync']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; + + beforeEach(() => { + ({ server, setServerAsync, httpRequestsMockHelpers } = setupEnvironment()); + }); + + afterEach(() => { + server.restore(); + }); + + describe('On-prem', () => { + beforeEach(async () => { + testBed = await setupOverviewPage(); + }); + + test('Shows link to Snapshot and Restore', () => { + const { exists, find } = testBed; + expect(exists('snapshotRestoreLink')).toBe(true); + expect(find('snapshotRestoreLink').props().href).toBe('snapshotAndRestoreUrl'); + }); + + test('renders step as incomplete ', () => { + const { exists } = testBed; + expect(exists('backupStep-incomplete')).toBe(true); + }); + }); + + describe('On Cloud', () => { + const setupCloudOverviewPage = async () => + setupOverviewPage({ + plugins: { + cloud: { + isCloudEnabled: true, + deploymentUrl: 'deploymentUrl', + }, + }, + }); + + describe('initial loading state', () => { + beforeEach(async () => { + // We don't want the request to load backup status to resolve immediately. + setServerAsync(true); + testBed = await setupCloudOverviewPage(); + }); + + afterEach(() => { + setServerAsync(false); + }); + + test('is rendered', () => { + const { exists } = testBed; + expect(exists('cloudBackupLoading')).toBe(true); + }); + }); + + describe('error state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('is rendered', () => { + const { exists } = testBed; + testBed.component.update(); + expect(exists('cloudBackupErrorCallout')).toBe(true); + }); + + test('lets the user attempt to reload backup status', () => { + const { exists } = testBed; + testBed.component.update(); + expect(exists('cloudBackupRetryButton')).toBe(true); + }); + }); + + describe('success state', () => { + describe('when data is backed up', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: true, + lastBackupTime: '2021-08-25T19:59:59.863Z', + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('renders link to Cloud backups and last backup time ', () => { + const { exists, find } = testBed; + expect(exists('dataBackedUpStatus')).toBe(true); + expect(exists('cloudSnapshotsLink')).toBe(true); + expect(find('dataBackedUpStatus').text()).toContain('Last snapshot created on'); + }); + + test('renders step as complete ', () => { + const { exists } = testBed; + expect(exists('backupStep-complete')).toBe(true); + }); + }); + + describe(`when data isn't backed up`, () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: false, + lastBackupTime: undefined, + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('renders link to Cloud backups and "not backed up" status', () => { + const { exists } = testBed; + expect(exists('dataNotBackedUpStatus')).toBe(true); + expect(exists('cloudSnapshotsLink')).toBe(true); + }); + + test('renders step as incomplete ', () => { + const { exists } = testBed; + expect(exists('backupStep-incomplete')).toBe(true); + }); + }); + }); + + describe('poll for new status', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request will succeed. + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: true, + lastBackupTime: '2021-08-25T19:59:59.863Z', + }); + + testBed = await setupCloudOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as incomplete when a success state is followed by an error state', async () => { + const { exists } = testBed; + expect(exists('backupStep-complete')).toBe(true); + + // Second request will error. + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + // Resolve the polling timeout. + await advanceTime(CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('backupStep-incomplete')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx deleted file mode 100644 index 3db75ba0a342..000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx +++ /dev/null @@ -1,153 +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 { act } from 'react-dom/test-utils'; - -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../../helpers'; -import { DeprecationLoggingStatus } from '../../../../common/types'; -import { DEPRECATION_LOGS_SOURCE_ID } from '../../../../common/constants'; - -const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({ - isDeprecationLogIndexingEnabled: toggle, - isDeprecationLoggingEnabled: toggle, -}); - -describe('Overview - Fix deprecation logs step', () => { - let testBed: OverviewTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); - testBed = await setupOverviewPage(); - - const { component } = testBed; - component.update(); - }); - - afterAll(() => { - server.restore(); - }); - - describe('Step 1 - Toggle log writing and collecting', () => { - test('toggles deprecation logging', async () => { - const { find, actions } = testBed; - - httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: false, - isDeprecationLoggingEnabled: false, - }); - - expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(true); - - await actions.clickDeprecationToggle(); - - const latestRequest = server.requests[server.requests.length - 1]; - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ isEnabled: false }); - expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(false); - }); - - test('shows callout when only loggerDeprecation is enabled', async () => { - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: false, - isDeprecationLoggingEnabled: true, - }); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { exists, component } = testBed; - - component.update(); - - expect(exists('deprecationWarningCallout')).toBe(true); - }); - - test('handles network error when updating logging state', async () => { - const error = { - statusCode: 500, - error: 'Internal server error', - message: 'Internal server error', - }; - - const { actions, exists } = testBed; - - httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(undefined, error); - - await actions.clickDeprecationToggle(); - - expect(exists('updateLoggingError')).toBe(true); - }); - - test('handles network error when fetching logging state', async () => { - const error = { - statusCode: 500, - error: 'Internal server error', - message: 'Internal server error', - }; - - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('fetchLoggingError')).toBe(true); - }); - }); - - describe('Step 2 - Analyze logs', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: true, - isDeprecationLoggingEnabled: true, - }); - }); - - test('Has a link to see logs in observability app', async () => { - await act(async () => { - testBed = await setupOverviewPage({ - http: { - basePath: { - prepend: (url: string) => url, - }, - }, - }); - }); - - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('viewObserveLogs')).toBe(true); - expect(find('viewObserveLogs').props().href).toBe( - `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}` - ); - }); - - test('Has a link to see logs in discover app', async () => { - await act(async () => { - testBed = await setupOverviewPage({ - getUrlForApp: jest.fn((app, options) => { - return `${app}/${options.path}`; - }), - }); - }); - - const { exists, component, find } = testBed; - - component.update(); - - expect(exists('viewDiscoverLogs')).toBe(true); - expect(find('viewDiscoverLogs').props().href).toBe('/discover/logs'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx new file mode 100644 index 000000000000..e1cef64dfb20 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx @@ -0,0 +1,193 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../../kibana_deprecations/service.mock'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { + esCriticalAndWarningDeprecations, + esCriticalOnlyDeprecations, + esNoDeprecations, +} from './mock_es_issues'; + +describe('Overview - Fix deprecation issues step - Elasticsearch deprecations', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('When load succeeds', () => { + const setup = async () => { + // Set up with no Kibana deprecations. + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response: [] }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component } = testBed; + component.update(); + }; + + describe('when there are critical and warning issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esCriticalAndWarningDeprecations); + await setup(); + }); + + test('renders counts for both', () => { + const { exists, find } = testBed; + expect(exists('esStatsPanel')).toBe(true); + expect(find('esStatsPanel.warningDeprecations').text()).toContain('1'); + expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1'); + }); + + test('panel links to ES deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('esStatsPanel').find('a').props().href).toBe('/es_deprecations'); + }); + }); + + describe('when there are critical but no warning issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esCriticalOnlyDeprecations); + await setup(); + }); + + test('renders a count for critical issues and success state for warning issues', () => { + const { exists, find } = testBed; + expect(exists('esStatsPanel')).toBe(true); + expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1'); + expect(exists('esStatsPanel.noWarningDeprecationIssues')).toBe(true); + }); + + test('panel links to ES deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('esStatsPanel').find('a').props().href).toBe('/es_deprecations'); + }); + }); + + describe('when there no critical or warning issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esNoDeprecations); + await setup(); + }); + + test('renders a count for critical issues and success state for warning issues', () => { + const { exists } = testBed; + expect(exists('esStatsPanel')).toBe(true); + expect(exists('esStatsPanel.noDeprecationIssues')).toBe(true); + }); + + test(`panel doesn't link to ES deprecations page`, () => { + const { component, find } = testBed; + component.update(); + expect(find('esStatsPanel').find('a').length).toBe(0); + }); + }); + }); + + describe(`When there's a load error`, () => { + test('handles network failure', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'Could not retrieve Elasticsearch deprecation issues.' + ); + }); + + test('handles unauthorized error', async () => { + const error = { + statusCode: 403, + error: 'Forbidden', + message: 'Forbidden', + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'You are not authorized to view Elasticsearch deprecation issues.' + ); + }); + + test('handles partially upgraded error', async () => { + const error = { + statusCode: 426, + error: 'Upgrade required', + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + allNodesUpgraded: false, + }, + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage({ isReadOnlyMode: false }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' + ); + }); + + test('handles upgrade error', async () => { + const error = { + statusCode: 426, + error: 'Upgrade required', + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + allNodesUpgraded: true, + }, + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage({ isReadOnlyMode: false }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe('All Elasticsearch nodes have been upgraded.'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx new file mode 100644 index 000000000000..b7c417fbfcb8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx @@ -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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../../kibana_deprecations/service.mock'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { esCriticalAndWarningDeprecations, esNoDeprecations } from './mock_es_issues'; + +describe('Overview - Fix deprecation issues step', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('when there are critical issues in one panel', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esCriticalAndWarningDeprecations); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response: [] }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + test('renders step as incomplete', async () => { + const { exists } = testBed; + expect(exists(`fixIssuesStep-incomplete`)).toBe(true); + }); + }); + + describe('when there are no critical issues for either panel', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esNoDeprecations); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response: [] }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + test('renders step as complete', async () => { + const { exists } = testBed; + expect(exists(`fixIssuesStep-complete`)).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx new file mode 100644 index 000000000000..c11a1481b68b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx @@ -0,0 +1,133 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; +import type { DomainDeprecationDetails } from 'kibana/public'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../../kibana_deprecations/service.mock'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { esNoDeprecations } from './mock_es_issues'; + +describe('Overview - Fix deprecation issues step - Kibana deprecations', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { mockedKibanaDeprecations, mockedCriticalKibanaDeprecations } = + kibanaDeprecationsServiceHelpers.defaultMockedResponses; + + afterAll(() => { + server.restore(); + }); + + describe('When load succeeds', () => { + const setup = async (response: DomainDeprecationDetails[]) => { + // Set up with no ES deprecations. + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esNoDeprecations); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component } = testBed; + component.update(); + }; + + describe('when there are critical and warning issues', () => { + beforeEach(async () => { + await setup(mockedKibanaDeprecations); + }); + + test('renders counts for both', () => { + const { exists, find } = testBed; + + expect(exists('kibanaStatsPanel')).toBe(true); + expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain(1); + expect(find('kibanaStatsPanel.warningDeprecations').text()).toContain(2); + }); + + test('panel links to Kibana deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('kibanaStatsPanel').find('a').props().href).toBe('/kibana_deprecations'); + }); + }); + + describe('when there are critical but no warning issues', () => { + beforeEach(async () => { + await setup(mockedCriticalKibanaDeprecations); + }); + + test('renders a count for critical issues and success state for warning issues', () => { + const { exists, find } = testBed; + + expect(exists('kibanaStatsPanel')).toBe(true); + expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain(1); + expect(exists('kibanaStatsPanel.noWarningDeprecationIssues')).toBe(true); + }); + + test('panel links to Kibana deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('kibanaStatsPanel').find('a').props().href).toBe('/kibana_deprecations'); + }); + }); + + describe('when there no critical or warning issues', () => { + beforeEach(async () => { + await setup([]); + }); + + test('renders a success state for the panel', () => { + const { exists } = testBed; + expect(exists('kibanaStatsPanel')).toBe(true); + expect(exists('kibanaStatsPanel.noDeprecationIssues')).toBe(true); + }); + + test(`panel doesn't link to Kibana deprecations page`, () => { + const { component, find } = testBed; + component.update(); + expect(find('kibanaStatsPanel').find('a').length).toBe(0); + }); + }); + }); + + describe(`When there's a load error`, () => { + test('Handles network failure', async () => { + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ + deprecationService, + mockRequestErrorMessage: 'Internal Server Error', + }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'Could not retrieve Kibana deprecation issues.' + ); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/mock_es_issues.ts similarity index 66% rename from x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts rename to x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/mock_es_issues.ts index 57373dbf0726..13505b47c5a7 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/mock_es_issues.ts @@ -5,10 +5,9 @@ * 2.0. */ -import type { DomainDeprecationDetails } from 'kibana/public'; import { ESUpgradeStatus } from '../../../../common/types'; -export const esDeprecations: ESUpgradeStatus = { +export const esCriticalAndWarningDeprecations: ESUpgradeStatus = { totalCriticalDeprecations: 1, deprecations: [ { @@ -33,24 +32,22 @@ export const esDeprecations: ESUpgradeStatus = { ], }; -export const esDeprecationsEmpty: ESUpgradeStatus = { +export const esCriticalOnlyDeprecations: ESUpgradeStatus = { + totalCriticalDeprecations: 1, + deprecations: [ + { + isCritical: true, + type: 'cluster_settings', + resolveDuringUpgrade: false, + message: 'Index Lifecycle Management poll interval is set too low', + url: 'https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html#ilm-poll-interval-limit', + details: + 'The Index Lifecycle Management poll interval setting [indices.lifecycle.poll_interval] is currently set to [500ms], but must be 1s or greater', + }, + ], +}; + +export const esNoDeprecations: ESUpgradeStatus = { totalCriticalDeprecations: 0, deprecations: [], }; - -export const kibanaDeprecations: DomainDeprecationDetails[] = [ - { - title: 'mock-deprecation-title', - correctiveActions: { manualSteps: ['test-step'] }, - domainId: 'xpack.spaces', - level: 'critical', - message: 'Sample warning deprecation', - }, - { - title: 'mock-deprecation-title', - correctiveActions: { manualSteps: ['test-step'] }, - domainId: 'xpack.spaces', - level: 'warning', - message: 'Sample warning deprecation', - }, -]; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx new file mode 100644 index 000000000000..8b68f5ee449a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx @@ -0,0 +1,473 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +// Once the logs team register the kibana locators in their app, we should be able +// to remove this mock and follow a similar approach to how discover link is tested. +// See: https://github.com/elastic/kibana/issues/104855 +const MOCKED_TIME = '2021-09-05T10:49:01.805Z'; +jest.mock('../../../../public/application/lib/logs_checkpoint', () => { + const originalModule = jest.requireActual('../../../../public/application/lib/logs_checkpoint'); + + return { + __esModule: true, + ...originalModule, + loadLogsCheckpoint: jest.fn().mockReturnValue('2021-09-05T10:49:01.805Z'), + }; +}); + +import { DeprecationLoggingStatus } from '../../../../common/types'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { + DEPRECATION_LOGS_INDEX, + DEPRECATION_LOGS_SOURCE_ID, + DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, +} from '../../../../common/constants'; + +const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({ + isDeprecationLogIndexingEnabled: toggle, + isDeprecationLoggingEnabled: toggle, +}); + +describe('Overview - Fix deprecation logs step', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + testBed = await setupOverviewPage(); + + const { component } = testBed; + component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + describe('Step status', () => { + test(`It's complete when there are no deprecation logs since last checkpoint`, async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 0 }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-complete`)).toBe(true); + }); + + test(`It's incomplete when there are deprecation logs since last checkpoint`, async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 5 }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-incomplete`)).toBe(true); + }); + + test(`It's incomplete when log collection is disabled `, async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 0 }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { actions, exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-complete`)).toBe(true); + + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(false)); + + await actions.clickDeprecationToggle(); + + expect(exists(`fixLogsStep-incomplete`)).toBe(true); + }); + }); + + describe('Step 1 - Toggle log writing and collecting', () => { + test('toggles deprecation logging', async () => { + const { find, actions } = testBed; + + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(false)); + + expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(true); + + await actions.clickDeprecationToggle(); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ isEnabled: false }); + expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(false); + }); + + test('shows callout when only loggerDeprecation is enabled', async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: false, + isDeprecationLoggingEnabled: true, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('deprecationWarningCallout')).toBe(true); + }); + + test('handles network error when updating logging state', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + const { actions, exists } = testBed; + + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(undefined, error); + + await actions.clickDeprecationToggle(); + + expect(exists('updateLoggingError')).toBe(true); + }); + + test('handles network error when fetching logging state', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, exists } = testBed; + + component.update(); + + expect(exists('fetchLoggingError')).toBe(true); + }); + + test('It doesnt show external links and deprecations count when toggle is disabled', async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: false, + isDeprecationLoggingEnabled: false, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('externalLinksTitle')).toBe(false); + expect(exists('deprecationsCountTitle')).toBe(false); + expect(exists('apiCompatibilityNoteTitle')).toBe(false); + }); + }); + + describe('Step 2 - Analyze logs', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + }); + + test('Has a link to see logs in observability app', async () => { + await act(async () => { + testBed = await setupOverviewPage({ + http: { + basePath: { + prepend: (url: string) => url, + }, + }, + plugins: { + infra: {}, + }, + }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('viewObserveLogs')).toBe(true); + expect(find('viewObserveLogs').props().href).toBe( + `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}&logPosition=(end:now,start:'${MOCKED_TIME}')` + ); + }); + + test(`Doesn't show observability app link if infra app is not available`, async () => { + const { component, exists } = testBed; + + component.update(); + + expect(exists('viewObserveLogs')).toBe(false); + }); + + test('Has a link to see logs in discover app', async () => { + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component, find } = testBed; + + component.update(); + + expect(exists('viewDiscoverLogs')).toBe(true); + + const decodedUrl = decodeURIComponent(find('viewDiscoverLogs').props().href); + expect(decodedUrl).toContain('discoverUrl'); + ['"language":"kuery"', '"query":"@timestamp+>'].forEach((param) => { + expect(decodedUrl).toContain(param); + }); + }); + }); + + describe('Step 3 - Resolve log issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + httpRequestsMockHelpers.setDeleteLogsCacheResponse('ok'); + }); + + test('With deprecation warnings', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 10, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { find, exists, component } = testBed; + + component.update(); + + expect(exists('hasWarningsCallout')).toBe(true); + expect(find('hasWarningsCallout').text()).toContain('10'); + }); + + test('No deprecation issues', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { find, exists, component } = testBed; + + component.update(); + + expect(exists('noWarningsCallout')).toBe(true); + expect(find('noWarningsCallout').text()).toContain('No deprecation issues'); + }); + + test('Handles errors and can retry', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, actions, component } = testBed; + + component.update(); + + expect(exists('errorCallout')).toBe(true); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await actions.clickRetryButton(); + + expect(exists('noWarningsCallout')).toBe(true); + }); + + test('Allows user to reset last stored date', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 10, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, actions, component } = testBed; + + component.update(); + + expect(exists('hasWarningsCallout')).toBe(true); + expect(exists('resetLastStoredDate')).toBe(true); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await actions.clickResetButton(); + + expect(exists('noWarningsCallout')).toBe(true); + }); + + test('Shows a toast if deleting cache fails', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setDeleteLogsCacheResponse(undefined, error); + // Initially we want to have the callout to have a warning state + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 10 }); + + const addDanger = jest.fn(); + await act(async () => { + testBed = await setupOverviewPage({ + services: { + core: { + notifications: { + toasts: { + addDanger, + }, + }, + }, + }, + }); + }); + + const { exists, actions, component } = testBed; + + component.update(); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 0 }); + + await actions.clickResetButton(); + + // The toast should always be shown if the delete logs cache fails. + expect(addDanger).toHaveBeenCalled(); + // Even though we changed the response of the getLogsCountResponse, when the + // deleteLogsCache fails the getLogsCount api should not be called and the + // status of the callout should remain the same it initially was. + expect(exists('hasWarningsCallout')).toBe(true); + }); + + describe('Poll for logs count', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request should make the step be complete + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + testBed = await setupOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as incomplete when a success state is followed by an error state', async () => { + const { exists } = testBed; + + expect(exists('fixLogsStep-complete')).toBe(true); + + // second request will error + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse(undefined, error); + + // Resolve the polling timeout. + await advanceTime(DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('fixLogsStep-incomplete')).toBe(true); + }); + }); + }); + + describe('Step 4 - API compatibility header', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + }); + + test('It shows copy with compatibility api header advice', async () => { + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('apiCompatibilityNoteTitle')).toBe(true); + }); + }); + + describe('Privileges check', () => { + test(`permissions warning callout is hidden if user has the right privileges`, async () => { + const { exists } = testBed; + + // Index privileges warning callout should not be shown + expect(exists('noIndexPermissionsCallout')).toBe(false); + // Analyze logs and Resolve logs sections should be shown + expect(exists('externalLinksTitle')).toBe(true); + expect(exists('deprecationsCountTitle')).toBe(true); + }); + + test(`doesn't show analyze and resolve logs if it doesn't have the right privileges`, async () => { + await act(async () => { + testBed = await setupOverviewPage({ + privileges: { + hasAllPrivileges: false, + missingPrivileges: { + index: [DEPRECATION_LOGS_INDEX], + }, + }, + }); + }); + + const { exists, component } = testBed; + + component.update(); + + // No index privileges warning callout should be shown + expect(exists('noIndexPermissionsCallout')).toBe(true); + // Analyze logs and Resolve logs sections should be hidden + expect(exists('externalLinksTitle')).toBe(false); + expect(exists('deprecationsCountTitle')).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap new file mode 100644 index 000000000000..2a512e8569d9 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Overview - Migrate system indices - Flyout shows correct features in flyout table 1`] = ` +Array [ + Array [ + "Security", + "Migration failed", + ], + Array [ + "Machine Learning", + "Migration in progress", + ], + Array [ + "Kibana", + "Migration required", + ], + Array [ + "Logstash", + "Migration complete", + ], +] +`; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts new file mode 100644 index 000000000000..1e74a966b393 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment } from '../../helpers'; +import { systemIndicesMigrationStatus } from './mocks'; + +describe('Overview - Migrate system indices - Flyout', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus(systemIndicesMigrationStatus); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + testBed.component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + test('shows correct features in flyout table', async () => { + const { actions, table } = testBed; + + await actions.clickViewSystemIndicesState(); + + const { tableCellsValues } = table.getMetaData('flyoutDetails'); + + expect(tableCellsValues.length).toBe(systemIndicesMigrationStatus.features.length); + expect(tableCellsValues).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx new file mode 100644 index 000000000000..e3f6d747deae --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; + +describe('Overview - Migrate system indices', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + testBed = await setupOverviewPage(); + testBed.component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + describe('Error state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupOverviewPage(); + }); + + test('Is rendered', () => { + const { exists, component } = testBed; + component.update(); + + expect(exists('systemIndicesStatusErrorCallout')).toBe(true); + }); + + test('Lets the user attempt to reload migration status', async () => { + const { exists, component, actions } = testBed; + component.update(); + + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + await actions.clickRetrySystemIndicesButton(); + + expect(exists('noMigrationNeededSection')).toBe(true); + }); + }); + + test('No migration needed', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + testBed = await setupOverviewPage(); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('noMigrationNeededSection')).toBe(true); + expect(exists('startSystemIndicesMigrationButton')).toBe(false); + expect(exists('viewSystemIndicesStateButton')).toBe(false); + }); + + test('Migration in progress', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'IN_PROGRESS', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + component.update(); + + // Start migration is disabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(true); + // But we keep view system indices CTA + expect(exists('viewSystemIndicesStateButton')).toBe(true); + }); + + describe('Migration needed', () => { + test('Initial state', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'MIGRATION_NEEDED', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + component.update(); + + // Start migration should be enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(false); + // Same for view system indices status + expect(exists('viewSystemIndicesStateButton')).toBe(true); + }); + + test('Handles errors when migrating', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'MIGRATION_NEEDED', + }); + httpRequestsMockHelpers.setSystemIndicesMigrationResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + await act(async () => { + find('startSystemIndicesMigrationButton').simulate('click'); + }); + + component.update(); + + // Error is displayed + expect(exists('startSystemIndicesMigrationCalloutError')).toBe(true); + // CTA is enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(false); + }); + + test('Handles errors from migration', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'ERROR', + features: [ + { + feature_name: 'kibana', + indices: [ + { + index: '.kibana', + migration_status: 'ERROR', + failure_cause: { + error: { + type: 'mapper_parsing_exception', + }, + }, + }, + ], + }, + ], + }); + + testBed = await setupOverviewPage(); + + const { exists } = testBed; + + // Error is displayed + expect(exists('migrationFailedCallout')).toBe(true); + // CTA is enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts new file mode 100644 index 000000000000..a810799c434e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts @@ -0,0 +1,58 @@ +/* + * 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 { SystemIndicesMigrationStatus } from '../../../../common/types'; + +export const systemIndicesMigrationStatus: SystemIndicesMigrationStatus = { + migration_status: 'MIGRATION_NEEDED', + features: [ + { + feature_name: 'security', + minimum_index_version: '7.1.1', + migration_status: 'ERROR', + indices: [ + { + index: '.security-7', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.2', + migration_status: 'IN_PROGRESS', + indices: [ + { + index: '.ml-config', + version: '7.1.2', + }, + ], + }, + { + feature_name: 'kibana', + minimum_index_version: '7.1.3', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.kibana', + version: '7.1.3', + }, + ], + }, + { + feature_name: 'logstash', + minimum_index_version: '7.1.4', + migration_status: 'NO_MIGRATION_NEEDED', + indices: [ + { + index: '.logstash-config', + version: '7.1.4', + }, + ], + }, + ], +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts new file mode 100644 index 000000000000..9eb0831c3c7a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS } from '../../../../common/constants'; + +describe('Overview - Migrate system indices - Step completion', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + test(`It's complete when no upgrade is needed`, async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`migrateSystemIndicesStep-complete`)).toBe(true); + }); + + test(`It's incomplete when migration is needed`, async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'MIGRATION_NEEDED', + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`migrateSystemIndicesStep-incomplete`)).toBe(true); + }); + + describe('Poll for new status', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request should make the step be incomplete + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'IN_PROGRESS', + }); + + testBed = await setupOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as complete when a upgraded needed status is followed by a no upgrade needed', async () => { + const { exists } = testBed; + + expect(exists('migrateSystemIndicesStep-incomplete')).toBe(true); + + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + // Resolve the polling timeout. + await advanceTime(SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('migrateSystemIndicesStep-complete')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/overview.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts similarity index 50% rename from x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/overview.helpers.ts rename to x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts index 96e12d4806ee..242d6893d151 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/overview.helpers.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; import { Overview } from '../../../public/application/components/overview'; -import { WithAppDependencies } from './setup_environment'; +import { WithAppDependencies } from '../helpers'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -18,7 +18,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -export type OverviewTestBed = TestBed & { +export type OverviewTestBed = TestBed & { actions: ReturnType; }; @@ -37,12 +37,58 @@ const createActions = (testBed: TestBed) => { component.update(); }; + const clickRetryButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('retryButton').simulate('click'); + }); + + component.update(); + }; + + const clickResetButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('resetLastStoredDate').simulate('click'); + }); + + component.update(); + }; + + const clickViewSystemIndicesState = async () => { + const { find, component } = testBed; + + await act(async () => { + find('viewSystemIndicesStateButton').simulate('click'); + }); + + component.update(); + }; + + const clickRetrySystemIndicesButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('systemIndicesStatusRetryButton').simulate('click'); + }); + + component.update(); + }; + return { clickDeprecationToggle, + clickRetryButton, + clickResetButton, + clickViewSystemIndicesState, + clickRetrySystemIndicesButton, }; }; -export const setup = async (overrides?: Record): Promise => { +export const setupOverviewPage = async ( + overrides?: Record +): Promise => { const initTestBed = registerTestBed(WithAppDependencies(Overview, overrides), testBedConfig); const testBed = await initTestBed(); @@ -51,31 +97,3 @@ export const setup = async (overrides?: Record): Promise { let testBed: OverviewTestBed; @@ -21,12 +22,11 @@ describe('Overview Page', () => { }); describe('Documentation links', () => { - test('Has a whatsNew link and it references nextMajor version', () => { + test('Has a whatsNew link and it references target version', () => { const { exists, find } = testBed; - const nextMajor = kibanaVersion.major + 1; expect(exists('whatsNewLink')).toBe(true); - expect(find('whatsNewLink').text()).toContain(`${nextMajor}.0`); + expect(find('whatsNewLink').text()).toContain('8'); }); test('Has a link for upgrade assistant in page header', () => { diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx deleted file mode 100644 index 2afffe989ed1..000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx +++ /dev/null @@ -1,233 +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 { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from 'src/core/public/mocks'; - -import * as mockedResponses from './mocked_responses'; -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../../helpers'; - -describe('Overview - Fix deprecated settings step', () => { - let testBed: OverviewTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(mockedResponses.esDeprecations); - - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockReturnValue(mockedResponses.kibanaDeprecations); - - testBed = await setupOverviewPage({ - deprecations: deprecationService, - }); - }); - - const { component } = testBed; - component.update(); - }); - - afterAll(() => { - server.restore(); - }); - - describe('ES deprecations', () => { - test('Shows deprecation warning and critical counts', () => { - const { exists, find } = testBed; - - expect(exists('esStatsPanel')).toBe(true); - expect(find('esStatsPanel.warningDeprecations').text()).toContain('1'); - expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1'); - }); - - test('Handles network failure', async () => { - const error = { - statusCode: 500, - error: 'Cant retrieve deprecations error', - message: 'Cant retrieve deprecations error', - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('esRequestErrorIconTip')).toBe(true); - }); - - test('Hides deprecation counts if it doesnt have any', async () => { - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(mockedResponses.esDeprecationsEmpty); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { exists } = testBed; - - expect(exists('noDeprecationsLabel')).toBe(true); - }); - - test('Stats panel navigates to deprecations list if clicked', () => { - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('esStatsPanel')).toBe(true); - expect(find('esStatsPanel').find('a').props().href).toBe('/es_deprecations'); - }); - - describe('Renders ES errors', () => { - test('handles network failure', async () => { - const error = { - statusCode: 500, - error: 'Internal server error', - message: 'Internal server error', - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('esRequestErrorIconTip')).toBe(true); - }); - - test('handles unauthorized error', async () => { - const error = { - statusCode: 403, - error: 'Forbidden', - message: 'Forbidden', - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('unauthorizedErrorIconTip')).toBe(true); - }); - - test('handles partially upgraded error', async () => { - const error = { - statusCode: 426, - error: 'Upgrade required', - message: 'There are some nodes running a different version of Elasticsearch', - attributes: { - allNodesUpgraded: false, - }, - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage({ isReadOnlyMode: false }); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('partiallyUpgradedErrorIconTip')).toBe(true); - }); - - test('handles upgrade error', async () => { - const error = { - statusCode: 426, - error: 'Upgrade required', - message: 'There are some nodes running a different version of Elasticsearch', - attributes: { - allNodesUpgraded: true, - }, - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage({ isReadOnlyMode: false }); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('upgradedErrorIconTip')).toBe(true); - }); - }); - }); - - describe('Kibana deprecations', () => { - test('Show deprecation warning and critical counts', () => { - const { exists, find } = testBed; - - expect(exists('kibanaStatsPanel')).toBe(true); - expect(find('kibanaStatsPanel.warningDeprecations').text()).toContain('1'); - expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain('1'); - }); - - test('Handles network failure', async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockRejectedValue(new Error('Internal Server Error')); - - testBed = await setupOverviewPage({ - deprecations: deprecationService, - }); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('kibanaRequestErrorIconTip')).toBe(true); - }); - - test('Hides deprecation count if it doesnt have any', async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest.fn().mockRejectedValue([]); - - testBed = await setupOverviewPage({ - deprecations: deprecationService, - }); - }); - - const { exists } = testBed; - - expect(exists('noDeprecationsLabel')).toBe(true); - expect(exists('kibanaStatsPanel.warningDeprecations')).toBe(false); - expect(exists('kibanaStatsPanel.criticalDeprecations')).toBe(false); - }); - - test('Stats panel navigates to deprecations list if clicked', () => { - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('kibanaStatsPanel')).toBe(true); - expect(find('kibanaStatsPanel').find('a').props().href).toBe('/kibana_deprecations'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx index 21daed29acac..601ed8992aa4 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx @@ -7,7 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../../helpers'; +import { setupEnvironment } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; describe('Overview - Upgrade Step', () => { let testBed: OverviewTestBed; @@ -22,22 +23,24 @@ describe('Overview - Upgrade Step', () => { server.restore(); }); - describe('Step 3 - Upgrade stack', () => { - test('Shows link to setup upgrade docs for on-prem installations', () => { + describe('On-prem', () => { + test('Shows link to setup upgrade docs', () => { const { exists } = testBed; expect(exists('upgradeSetupDocsLink')).toBe(true); expect(exists('upgradeSetupCloudLink')).toBe(false); }); + }); - test('Shows upgrade cta and link to docs for cloud installations', async () => { + describe('On Cloud', () => { + test('Shows upgrade CTA and link to docs', async () => { await act(async () => { testBed = await setupOverviewPage({ - servicesOverrides: { + plugins: { cloud: { isCloudEnabled: true, - baseUrl: 'https://test.com', - cloudId: '1234', + deploymentUrl: + 'https://cloud.elastic.co./deployments/bfdad4ef99a24212a06d387593686d63', }, }, }); @@ -46,10 +49,12 @@ describe('Overview - Upgrade Step', () => { const { component, exists, find } = testBed; component.update(); - expect(exists('upgradeSetupCloudLink')).toBe(true); expect(exists('upgradeSetupDocsLink')).toBe(true); + expect(exists('upgradeSetupCloudLink')).toBe(true); - expect(find('upgradeSetupCloudLink').props().href).toBe('https://test.com/deployments/1234'); + expect(find('upgradeSetupCloudLink').props().href).toBe( + 'https://cloud.elastic.co./deployments/bfdad4ef99a24212a06d387593686d63?show_upgrade=true' + ); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/common/constants.ts b/x-pack/plugins/upgrade_assistant/common/constants.ts index 68a6b9e9cdb8..9f67786c85ba 100644 --- a/x-pack/plugins/upgrade_assistant/common/constants.ts +++ b/x-pack/plugins/upgrade_assistant/common/constants.ts @@ -24,6 +24,20 @@ export const indexSettingDeprecations = { export const API_BASE_PATH = '/api/upgrade_assistant'; +// Telemetry constants +export const UPGRADE_ASSISTANT_TELEMETRY = 'upgrade-assistant-telemetry'; + +/** + * This is the repository where Cloud stores its backup snapshots. + */ +export const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots'; + export const DEPRECATION_WARNING_UPPER_LIMIT = 999999; export const DEPRECATION_LOGS_SOURCE_ID = 'deprecation_logs'; +export const DEPRECATION_LOGS_INDEX = '.logs-deprecation.elasticsearch-default'; export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-default'; + +export const CLUSTER_UPGRADE_STATUS_POLL_INTERVAL_MS = 45000; +export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000; +export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 15000; +export const SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS = 15000; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index a296e158481f..89afa05dfe22 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -8,16 +8,26 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; +export type DeprecationSource = 'Kibana' | 'Elasticsearch'; + +export type ClusterUpgradeState = 'isPreparingForUpgrade' | 'isUpgrading' | 'isUpgradeComplete'; + +export interface ResponseError { + statusCode: number; + message: string | Error; + attributes?: { + allNodesUpgraded: boolean; + }; +} + export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. created = 0, - indexGroupServicesStopped = 10, readonly = 20, newIndexCreated = 30, reindexStarted = 40, reindexCompleted = 50, aliasCreated = 60, - indexGroupServicesStarted = 70, } export enum ReindexStatus { @@ -26,6 +36,9 @@ export enum ReindexStatus { failed, paused, cancelled, + // Used by the UI to differentiate if there was a failure retrieving + // the status from the server API + fetchFailed, } export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation'; @@ -109,14 +122,7 @@ export interface ReindexWarning { }; } -export enum IndexGroup { - ml = '___ML_REINDEX_LOCK___', - watcher = '___WATCHER_REINDEX_LOCK___', -} - // Telemetry types -export const UPGRADE_ASSISTANT_TYPE = 'upgrade-assistant-telemetry'; -export const UPGRADE_ASSISTANT_DOC_ID = 'upgrade-assistant-telemetry'; export type UIOpenOption = 'overview' | 'elasticsearch' | 'kibana'; export type UIReindexOption = 'close' | 'open' | 'start' | 'stop'; @@ -133,32 +139,7 @@ export interface UIReindex { stop: boolean; } -export interface UpgradeAssistantTelemetrySavedObject { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; -} - export interface UpgradeAssistantTelemetry { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; features: { deprecation_logging: { enabled: boolean; @@ -166,10 +147,6 @@ export interface UpgradeAssistantTelemetry { }; } -export interface UpgradeAssistantTelemetrySavedObjectAttributes { - [key: string]: any; -} - export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; @@ -215,6 +192,11 @@ export interface EnrichedDeprecationInfo resolveDuringUpgrade: boolean; } +export interface CloudBackupStatus { + isBackedUp: boolean; + lastBackupTime?: string; +} + export interface ESUpgradeStatus { totalCriticalDeprecations: number; deprecations: EnrichedDeprecationInfo[]; @@ -247,3 +229,29 @@ export interface DeprecationLoggingStatus { isDeprecationLogIndexingEnabled: boolean; isDeprecationLoggingEnabled: boolean; } + +export type MIGRATION_STATUS = 'MIGRATION_NEEDED' | 'NO_MIGRATION_NEEDED' | 'IN_PROGRESS' | 'ERROR'; +export interface SystemIndicesMigrationFeature { + id?: string; + feature_name: string; + minimum_index_version: string; + migration_status: MIGRATION_STATUS; + indices: Array<{ + index: string; + version: string; + failure_cause?: { + error: { + type: string; + reason: string; + }; + }; + }>; +} +export interface SystemIndicesMigrationStatus { + features: SystemIndicesMigrationFeature[]; + migration_status: MIGRATION_STATUS; +} +export interface SystemIndicesMigrationStarted { + features: SystemIndicesMigrationFeature[]; + accepted: boolean; +} diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index e66f25318a28..41789b393b68 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -8,7 +8,7 @@ "githubTeam": "kibana-stack-management" }, "configPath": ["xpack", "upgrade_assistant"], - "requiredPlugins": ["management", "discover", "data", "licensing", "features", "infra"], - "optionalPlugins": ["usageCollection", "cloud"], - "requiredBundles": ["esUiShared", "kibanaReact"] + "requiredPlugins": ["management", "data", "licensing", "features", "share"], + "optionalPlugins": ["usageCollection", "cloud", "security", "infra"], + "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils"] } diff --git a/x-pack/plugins/upgrade_assistant/public/application/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/_index.scss deleted file mode 100644 index 841415620d69..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'components/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx index 864be6e5d996..9ac90e5d81f4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -5,73 +5,171 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Router, Switch, Route, Redirect } from 'react-router-dom'; -import { I18nStart, ScopedHistory } from 'src/core/public'; -import { ApplicationStart } from 'kibana/public'; -import { GlobalFlyout } from '../shared_imports'; - -import { KibanaContextProvider } from '../shared_imports'; -import { AppServicesContext } from '../types'; -import { AppContextProvider, ContextValue, useAppContext } from './app_context'; -import { ComingSoonPrompt } from './components/coming_soon_prompt'; -import { EsDeprecations } from './components/es_deprecations'; -import { KibanaDeprecationsContent } from './components/kibana_deprecations'; -import { Overview } from './components/overview'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiPageContent, EuiLoadingSpinner } from '@elastic/eui'; +import { ScopedHistory } from 'src/core/public'; + import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; +import { API_BASE_PATH } from '../../common/constants'; +import { ClusterUpgradeState } from '../../common/types'; +import { APP_WRAPPER_CLASS, GlobalFlyout, AuthorizationProvider } from '../shared_imports'; +import { AppDependencies } from '../types'; +import { AppContextProvider, useAppContext } from './app_context'; +import { EsDeprecations, ComingSoonPrompt, KibanaDeprecations, Overview } from './components'; const { GlobalFlyoutProvider } = GlobalFlyout; -export interface AppDependencies extends ContextValue { - i18n: I18nStart; - history: ScopedHistory; - application: ApplicationStart; - services: AppServicesContext; -} -const App: React.FunctionComponent = () => { - const { isReadOnlyMode } = useAppContext(); +const AppHandlingClusterUpgradeState: React.FunctionComponent = () => { + const { + isReadOnlyMode, + services: { api }, + } = useAppContext(); + + const [clusterUpgradeState, setClusterUpradeState] = + useState('isPreparingForUpgrade'); + + useEffect(() => { + api.onClusterUpgradeStateChange((newClusterUpgradeState: ClusterUpgradeState) => { + setClusterUpradeState(newClusterUpgradeState); + }); + }, [api]); // Read-only mode will be enabled up until the last minor before the next major release if (isReadOnlyMode) { return ; } + if (clusterUpgradeState === 'isUpgrading') { + return ( + + + + + } + body={ +

+ +

+ } + data-test-subj="emptyPrompt" + /> +
+ ); + } + + if (clusterUpgradeState === 'isUpgradeComplete') { + return ( + + + + + } + body={ +

+ +

+ } + data-test-subj="emptyPrompt" + /> +
+ ); + } + return ( - + ); }; -export const AppWithRouter = ({ history }: { history: ScopedHistory }) => { +export const App = ({ history }: { history: ScopedHistory }) => { + const { + services: { api }, + } = useAppContext(); + + // Poll the API to detect when the cluster is either in the middle of + // a rolling upgrade or has completed one. We need to create two separate + // components: one to call this hook and one to handle state changes. + // This is because the implementation of this hook calls the state-change + // callbacks on every render, which will get the UI stuck in an infinite + // render loop if the same component both called the hook and handled + // the state changes it triggers. + const { isLoading, isInitialRequest } = api.useLoadClusterUpgradeStatus(); + + // Prevent flicker of the underlying UI while we wait for the status to fetch. + if (isLoading && isInitialRequest) { + return ( + + } /> + + ); + } + return ( - + ); }; -export const RootComponent = ({ - i18n, - history, - services, - application, - ...contextValue -}: AppDependencies) => { +export const RootComponent = (dependencies: AppDependencies) => { + const { + history, + core: { i18n, application, http }, + } = dependencies.services; + return ( - - - - + + + + - + - - + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx index 88b5bd4721c3..8b11b20ed185 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx @@ -5,43 +5,17 @@ * 2.0. */ -import { - CoreStart, - DeprecationsServiceStart, - DocLinksStart, - HttpSetup, - NotificationsStart, -} from 'src/core/public'; import React, { createContext, useContext } from 'react'; -import { ApiService } from './lib/api'; -import { BreadcrumbService } from './lib/breadcrumbs'; +import { AppDependencies } from '../types'; -export interface KibanaVersionContext { - currentMajor: number; - prevMajor: number; - nextMajor: number; -} - -export interface ContextValue { - http: HttpSetup; - docLinks: DocLinksStart; - kibanaVersionInfo: KibanaVersionContext; - notifications: NotificationsStart; - isReadOnlyMode: boolean; - api: ApiService; - breadcrumbs: BreadcrumbService; - getUrlForApp: CoreStart['application']['getUrlForApp']; - deprecations: DeprecationsServiceStart; -} - -export const AppContext = createContext({} as any); +export const AppContext = createContext(undefined); export const AppContextProvider = ({ children, value, }: { children: React.ReactNode; - value: ContextValue; + value: AppDependencies; }) => { return {children}; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss deleted file mode 100644 index 8f900ca8dc05..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'es_deprecations/index'; -@import 'overview/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx index 14627f0b138b..883a8675e0ce 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx @@ -11,7 +11,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useAppContext } from '../app_context'; export const ComingSoonPrompt: React.FunctionComponent = () => { - const { kibanaVersionInfo, docLinks } = useAppContext(); + const { + kibanaVersionInfo, + services: { + core: { docLinks }, + }, + } = useAppContext(); + const { nextMajor, currentMajor } = kibanaVersionInfo; const { ELASTIC_WEBSITE_URL } = docLinks; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx index c7f974fab6a8..34850e6c9754 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx @@ -5,30 +5,8 @@ * 2.0. */ -import { IconColor } from '@elastic/eui'; -import { invert } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from '../../../common/types'; - -export const LEVEL_MAP: { [level: string]: number } = { - warning: 0, - critical: 1, -}; - -interface ReverseLevelMap { - [idx: number]: DeprecationInfo['level']; -} - -export const REVERSE_LEVEL_MAP: ReverseLevelMap = invert(LEVEL_MAP) as ReverseLevelMap; - -export const COLOR_MAP: { [level: string]: IconColor } = { - warning: 'default', - critical: 'danger', -}; - -export const DEPRECATIONS_PER_PAGE = 25; - export const DEPRECATION_TYPE_MAP = { cluster_settings: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.clusterDeprecationTypeLabel', @@ -49,3 +27,8 @@ export const DEPRECATION_TYPE_MAP = { defaultMessage: 'Machine Learning', }), }; + +export const PAGINATION_CONFIG = { + initialPageSize: 50, + pageSizeOptions: [50, 100, 200], +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss deleted file mode 100644 index 4865e977f526..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'deprecation_types/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss deleted file mode 100644 index c3e842941a25..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'reindex/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx index 439062e02765..6ec05b0c4fc9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx @@ -17,10 +17,11 @@ import { EuiTitle, EuiText, EuiTextColor, - EuiLink, + EuiSpacer, } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; export interface DefaultDeprecationFlyoutProps { deprecation: EnrichedDeprecationInfo; @@ -38,12 +39,6 @@ const i18nTexts = { }, } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), closeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel', { @@ -61,8 +56,10 @@ export const DefaultDeprecationFlyout = ({ return ( <> + + -

{message}

+

{message}

{index && ( @@ -74,11 +71,9 @@ export const DefaultDeprecationFlyout = ({
-

{details}

+

{details}

- - {i18nTexts.learnMoreLinkLabel} - +

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx index d4bacb21238c..e7fc1bb7772d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx @@ -42,6 +42,7 @@ export const DefaultTableRow: React.FunctionComponent = ({ rowFieldNames, }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'defaultDeprecationDetails', 'aria-labelledby': 'defaultDeprecationDetailsFlyoutTitle', }, @@ -60,8 +61,8 @@ export const DefaultTableRow: React.FunctionComponent = ({ rowFieldNames, > setShowFlyout(true)} deprecation={deprecation} + openFlyout={() => setShowFlyout(true)} /> ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx index 1567562db53e..a6add8cccdd2 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, EuiButtonEmpty, @@ -19,13 +20,18 @@ import { EuiTitle, EuiText, EuiTextColor, - EuiLink, EuiSpacer, EuiCallOut, } from '@elastic/eui'; -import { EnrichedDeprecationInfo, IndexSettingAction } from '../../../../../../common/types'; -import type { ResponseError } from '../../../../lib/api'; + +import { + EnrichedDeprecationInfo, + IndexSettingAction, + ResponseError, +} from '../../../../../../common/types'; +import { uiMetricService, UIM_INDEX_SETTINGS_DELETE_CLICK } from '../../../../lib/ui_metric'; import type { Status } from '../../../types'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; export interface RemoveIndexSettingsFlyoutProps { deprecation: EnrichedDeprecationInfo; @@ -48,12 +54,6 @@ const i18nTexts = { }, } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), removeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.removeButtonLabel', { @@ -106,11 +106,21 @@ export const RemoveIndexSettingsFlyout = ({ // Flag used to hide certain parts of the UI if the deprecation has been resolved or is in progress const isResolvable = ['idle', 'error'].includes(statusType); + const onRemoveSettings = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_INDEX_SETTINGS_DELETE_CLICK); + removeIndexSettings(index!, (correctiveAction as IndexSettingAction).deprecatedSettings); + }, [correctiveAction, index, removeIndexSettings]); + return ( <> + + -

{message}

+

{message}

@@ -136,9 +146,7 @@ export const RemoveIndexSettingsFlyout = ({

{details}

- - {i18nTexts.learnMoreLinkLabel} - +

@@ -184,12 +192,7 @@ export const RemoveIndexSettingsFlyout = ({ fill data-test-subj="deleteSettingsButton" color="danger" - onClick={() => - removeIndexSettings( - index!, - (correctiveAction as IndexSettingAction).deprecatedSettings - ) - } + onClick={onRemoveSettings} > {statusType === 'error' ? i18nTexts.retryRemoveButtonLabel diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx index a5a586927c81..f982e84dce6d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx @@ -47,7 +47,7 @@ const i18nTexts = { 'xpack.upgradeAssistant.esDeprecations.indexSettings.resolutionTooltipLabel', { defaultMessage: - 'Resolve this deprecation by removing settings from this index. This is an automated resolution.', + 'Resolve this issue by removing settings from this index. This issue can be resolved automatically.', } ), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx index b118d01a2d54..28fb11334fb3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx @@ -7,10 +7,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { EnrichedDeprecationInfo, ResponseError } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; -import type { ResponseError } from '../../../../lib/api'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { DeprecationTableColumns, Status } from '../../../types'; import { IndexSettingsResolutionCell } from './resolution_table_cell'; @@ -33,7 +32,9 @@ export const IndexSettingsTableRow: React.FunctionComponent = ({ details?: ResponseError; }>({ statusType: 'idle' }); - const { api } = useAppContext(); + const { + services: { api }, + } = useAppContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -71,6 +72,7 @@ export const IndexSettingsTableRow: React.FunctionComponent = ({ }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'indexSettingsDetails', 'aria-labelledby': 'indexSettingsDetailsFlyoutTitle', }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx index 972d640d18c5..3a81c7f1cc8e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx @@ -12,6 +12,7 @@ import { useSnapshotState, SnapshotState } from './use_snapshot_state'; export interface MlSnapshotContext { snapshotState: SnapshotState; + mlUpgradeModeEnabled: boolean; upgradeSnapshot: () => Promise; deleteSnapshot: () => Promise; } @@ -31,12 +32,14 @@ interface Props { children: React.ReactNode; snapshotId: string; jobId: string; + mlUpgradeModeEnabled: boolean; } export const MlSnapshotsStatusProvider: React.FunctionComponent = ({ api, snapshotId, jobId, + mlUpgradeModeEnabled, children, }) => { const { updateSnapshotStatus, snapshotState, upgradeSnapshot, deleteSnapshot } = useSnapshotState( @@ -57,6 +60,7 @@ export const MlSnapshotsStatusProvider: React.FunctionComponent = ({ snapshotState, upgradeSnapshot, deleteSnapshot, + mlUpgradeModeEnabled, }} > {children} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx index ba72faf2f8c3..a5830cf1ca65 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, @@ -24,7 +26,15 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { + uiMetricService, + UIM_ML_SNAPSHOT_UPGRADE_CLICK, + UIM_ML_SNAPSHOT_DELETE_CLICK, +} from '../../../../lib/ui_metric'; +import { useAppContext } from '../../../../app_context'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; import { MlSnapshotContext } from './context'; +import { SnapshotState } from './use_snapshot_state'; export interface FixSnapshotsFlyoutProps extends MlSnapshotContext { deprecation: EnrichedDeprecationInfo; @@ -38,6 +48,12 @@ const i18nTexts = { defaultMessage: 'Upgrade', } ), + upgradingButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradingButtonLabel', + { + defaultMessage: 'Upgrading…', + } + ), retryUpgradeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.retryUpgradeButtonLabel', { @@ -56,6 +72,12 @@ const i18nTexts = { defaultMessage: 'Delete', } ), + deletingButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.deletingButtonLabel', + { + defaultMessage: 'Deleting…', + } + ), retryDeleteButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.retryDeleteButtonLabel', { @@ -77,12 +99,62 @@ const i18nTexts = { defaultMessage: 'Error upgrading snapshot', } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.learnMoreLinkLabel', + upgradeModeEnabledErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledErrorTitle', { - defaultMessage: 'Learn more about this deprecation', + defaultMessage: 'Machine Learning upgrade mode is enabled', } ), + upgradeModeEnabledErrorDescription: (docsLink: string) => ( + + + + ), + }} + /> + ), +}; + +const getDeleteButtonLabel = (snapshotState: SnapshotState) => { + if (snapshotState.action === 'delete') { + if (snapshotState.error) { + return i18nTexts.retryDeleteButtonLabel; + } + + switch (snapshotState.status) { + case 'in_progress': + return i18nTexts.deletingButtonLabel; + case 'idle': + default: + return i18nTexts.deleteButtonLabel; + } + } + return i18nTexts.deleteButtonLabel; +}; + +const getUpgradeButtonLabel = (snapshotState: SnapshotState) => { + if (snapshotState.action === 'upgrade') { + if (snapshotState.error) { + return i18nTexts.retryUpgradeButtonLabel; + } + + switch (snapshotState.status) { + case 'in_progress': + return i18nTexts.upgradingButtonLabel; + case 'idle': + default: + return i18nTexts.upgradeButtonLabel; + } + } + return i18nTexts.upgradeButtonLabel; }; export const FixSnapshotsFlyout = ({ @@ -91,16 +163,23 @@ export const FixSnapshotsFlyout = ({ snapshotState, upgradeSnapshot, deleteSnapshot, + mlUpgradeModeEnabled, }: FixSnapshotsFlyoutProps) => { - // Flag used to hide certain parts of the UI if the deprecation has been resolved or is in progress - const isResolvable = ['idle', 'error'].includes(snapshotState.status); + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + const isResolved = snapshotState.status === 'complete'; const onUpgradeSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_UPGRADE_CLICK); upgradeSnapshot(); closeFlyout(); }; const onDeleteSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_DELETE_CLICK); deleteSnapshot(); closeFlyout(); }; @@ -108,12 +187,14 @@ export const FixSnapshotsFlyout = ({ return ( <> + + -

{i18nTexts.flyoutTitle}

+

{i18nTexts.flyoutTitle}

- {snapshotState.error && ( + {snapshotState.error && !isResolved && ( <> )} + + {mlUpgradeModeEnabled && ( + <> + +

+ {i18nTexts.upgradeModeEnabledErrorDescription(docLinks.links.ml.setUpgradeMode)} +

+
+ + + )} +

{deprecation.details}

- - {i18nTexts.learnMoreLinkLabel} - +

@@ -147,7 +243,7 @@ export const FixSnapshotsFlyout = ({ - {isResolvable && ( + {!isResolved && !mlUpgradeModeEnabled && ( @@ -155,23 +251,25 @@ export const FixSnapshotsFlyout = ({ data-test-subj="deleteSnapshotButton" color="danger" onClick={onDeleteSnapshot} - isLoading={false} + isLoading={ + snapshotState.action === 'delete' && snapshotState.status === 'in_progress' + } + isDisabled={snapshotState.status === 'in_progress'} > - {snapshotState.action === 'delete' && snapshotState.error - ? i18nTexts.retryDeleteButtonLabel - : i18nTexts.deleteButtonLabel} + {getDeleteButtonLabel(snapshotState)} - {snapshotState.action === 'upgrade' && snapshotState.error - ? i18nTexts.retryUpgradeButtonLabel - : i18nTexts.upgradeButtonLabel} + {getUpgradeButtonLabel(snapshotState)} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx index 7963701b5c54..1c3e23d0b6ca 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx @@ -66,7 +66,7 @@ const i18nTexts = { 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.resolutionTooltipLabel', { defaultMessage: - 'Resolve this deprecation by upgrading or deleting a job model snapshot. This is an automated resolution.', + 'Resolve this issue by upgrading or deleting a job model snapshot. This issue can be resolved automatically.', } ), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx index 9d961aed8ffc..37dddd8171c8 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx @@ -21,6 +21,7 @@ const { useGlobalFlyout } = GlobalFlyout; interface TableRowProps { deprecation: EnrichedDeprecationInfo; rowFieldNames: DeprecationTableColumns[]; + mlUpgradeModeEnabled: boolean; } export const MlSnapshotsTableRowCells: React.FunctionComponent = ({ @@ -50,6 +51,7 @@ export const MlSnapshotsTableRowCells: React.FunctionComponent = }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'mlSnapshotDetails', 'aria-labelledby': 'mlSnapshotDetailsFlyoutTitle', }, @@ -76,12 +78,15 @@ export const MlSnapshotsTableRowCells: React.FunctionComponent = }; export const MlSnapshotsTableRow: React.FunctionComponent = (props) => { - const { api } = useAppContext(); + const { + services: { api }, + } = useAppContext(); return ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx index a724922563e0..6725ba098e3c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx @@ -7,7 +7,8 @@ import { useRef, useCallback, useState, useEffect } from 'react'; -import { ApiService, ResponseError } from '../../../../lib/api'; +import { ResponseError } from '../../../../../../common/types'; +import { ApiService } from '../../../../lib/api'; import { Status } from '../../../types'; const POLL_INTERVAL_MS = 1000; @@ -68,7 +69,7 @@ export const useSnapshotState = ({ return; } - setSnapshotState(data); + setSnapshotState({ ...data, action: 'upgrade' }); // Only keep polling if it exists and is in progress. if (data?.status === 'in_progress') { @@ -97,7 +98,7 @@ export const useSnapshotState = ({ return; } - setSnapshotState(data); + setSnapshotState({ ...data, action: 'upgrade' }); updateSnapshotStatus(); }, [api, jobId, snapshotId, updateSnapshotStatus]); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss deleted file mode 100644 index 4cd55614ab4e..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'flyout/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap index 9357e7d2d9b6..f3a1723c9c6e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap @@ -3,44 +3,32 @@ exports[`ChecklistFlyout renders 1`] = ` - - } - > +

+ Learn more + , + } + } />

-
+ - -

- -

-
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap index d085e5ecc20e..2f68d35b6750 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap @@ -2,28 +2,7 @@ exports[`WarningsFlyoutStep renders 1`] = ` - - - } - > -

- -

-
- -
+ @@ -47,13 +26,13 @@ exports[`WarningsFlyoutStep renders 1`] = ` grow={false} > diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss deleted file mode 100644 index 1c9fd599b13a..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'step_progress'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss index a754541c2ff8..4d8ee5def30e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss @@ -18,7 +18,7 @@ $stepStatusToCallOutColor: ( failed: 'danger', complete: 'success', paused: 'warning', - cancelled: 'danger', + cancelled: 'warning', ); .upgStepProgress__status--circle { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx index a3a0f15188fc..705b4aa906bf 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx @@ -14,6 +14,24 @@ import { LoadingState } from '../../../../types'; import type { ReindexState } from '../use_reindex_state'; import { ChecklistFlyoutStep } from './checklist_step'; +jest.mock('../../../../../app_context', () => { + const { docLinksServiceMock } = jest.requireActual( + '../../../../../../../../../../src/core/public/doc_links/doc_links_service.mock' + ); + + return { + useAppContext: () => { + return { + services: { + core: { + docLinks: docLinksServiceMock.createStartContract(), + }, + }, + }; + }, + }; +}); + describe('ChecklistFlyout', () => { const defaultProps = { indexName: 'myIndex', @@ -22,6 +40,11 @@ describe('ChecklistFlyout', () => { onConfirmInputChange: jest.fn(), startReindex: jest.fn(), cancelReindex: jest.fn(), + http: { + basePath: { + prepend: jest.fn(), + }, + } as any, renderGlobalCallouts: jest.fn(), reindexState: { loadingState: LoadingState.Success, @@ -45,11 +68,35 @@ describe('ChecklistFlyout', () => { expect((wrapper.find('EuiButton').props() as any).isLoading).toBe(true); }); - it('disables button if hasRequiredPrivileges is false', () => { + it('hides button if hasRequiredPrivileges is false', () => { const props = cloneDeep(defaultProps); props.reindexState.hasRequiredPrivileges = false; const wrapper = shallow(); - expect(wrapper.find('EuiButton').props().disabled).toBe(true); + expect(wrapper.exists('EuiButton')).toBe(false); + }); + + it('hides button if has error', () => { + const props = cloneDeep(defaultProps); + props.reindexState.status = ReindexStatus.fetchFailed; + props.reindexState.errorMessage = 'Index not found'; + const wrapper = shallow(); + expect(wrapper.exists('EuiButton')).toBe(false); + }); + + it('shows get status error callout', () => { + const props = cloneDeep(defaultProps); + props.reindexState.status = ReindexStatus.fetchFailed; + props.reindexState.errorMessage = 'Index not found'; + const wrapper = shallow(); + expect(wrapper.exists('[data-test-subj="fetchFailedCallout"]')).toBe(true); + }); + + it('shows reindexing callout', () => { + const props = cloneDeep(defaultProps); + props.reindexState.status = ReindexStatus.failed; + props.reindexState.errorMessage = 'Index not found'; + const wrapper = shallow(); + expect(wrapper.exists('[data-test-subj="reindexingFailedCallout"]')).toBe(true); }); it('calls startReindex when button is clicked', () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx index 856e2a57649d..e0b9b25d7323 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx @@ -15,15 +15,18 @@ import { EuiFlexItem, EuiFlyoutBody, EuiFlyoutFooter, + EuiLink, EuiSpacer, - EuiTitle, + EuiText, } from '@elastic/eui'; 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 { ReindexProgress } from './progress'; +import { useAppContext } from '../../../../../app_context'; const buttonLabel = (status?: ReindexStatus) => { switch (status) { @@ -41,25 +44,25 @@ const buttonLabel = (status?: ReindexStatus) => { defaultMessage="Reindexing…" /> ); - case ReindexStatus.completed: + case ReindexStatus.paused: return ( ); - case ReindexStatus.paused: + case ReindexStatus.cancelled: return ( ); default: return ( ); } @@ -69,45 +72,27 @@ const buttonLabel = (status?: ReindexStatus) => { * Displays a flyout that shows the current reindexing status for a given index. */ export const ChecklistFlyoutStep: React.FunctionComponent<{ - renderGlobalCallouts: () => React.ReactNode; closeFlyout: () => void; reindexState: ReindexState; startReindex: () => void; cancelReindex: () => void; -}> = ({ closeFlyout, reindexState, startReindex, cancelReindex, renderGlobalCallouts }) => { +}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + const { loadingState, status, hasRequiredPrivileges } = reindexState; const loading = loadingState === LoadingState.Loading || status === ReindexStatus.inProgress; + const isCompleted = status === ReindexStatus.completed; + const hasFetchFailed = status === ReindexStatus.fetchFailed; + const hasReindexingFailed = status === ReindexStatus.failed; return ( - {renderGlobalCallouts()} - - } - color="warning" - iconType="alert" - > -

- -

-

- -

-
- {!hasRequiredPrivileges && ( + {hasRequiredPrivileges === false && ( )} - - -

+ {(hasFetchFailed || hasReindexingFailed) && ( + <> + + + ) : ( + + ) + } + > + {reindexState.errorMessage} + + + )} + +

+ + {i18n.translate( + 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> +

+

-

-
+

+ +
@@ -143,18 +171,21 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ />
- - - {buttonLabel(status)} - - + {!hasFetchFailed && !isCompleted && hasRequiredPrivileges && ( + + + {buttonLabel(status)} + + + )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx index f10e7b4cc687..82d0f57c22a5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx @@ -5,74 +5,28 @@ * 2.0. */ -import React, { useState } from 'react'; -import { DocLinksStart } from 'kibana/public'; -import { i18n } from '@kbn/i18n'; +import React, { useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiFlyoutHeader, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; -import { - EnrichedDeprecationInfo, - ReindexAction, - ReindexStatus, -} from '../../../../../../../common/types'; -import { useAppContext } from '../../../../../app_context'; +import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types'; import type { ReindexStateContext } from '../context'; import { ChecklistFlyoutStep } from './checklist_step'; import { WarningsFlyoutStep } from './warnings_step'; - -enum ReindexFlyoutStep { - reindexWarnings, - checklist, -} +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; } -const getOpenAndCloseIndexDocLink = (docLinks: DocLinksStart) => ( - - {i18n.translate( - 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation', - { defaultMessage: 'documentation' } - )} - -); - -const getIndexClosedCallout = (docLinks: DocLinksStart) => ( - <> - -

- - {i18n.translate( - 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis', - { defaultMessage: 'Reindexing may take longer than usual' } - )} - - ), - }} - /> -

-
- - -); - export const ReindexFlyout: React.FunctionComponent = ({ reindexState, startReindex, @@ -81,53 +35,60 @@ export const ReindexFlyout: React.FunctionComponent = ({ deprecation, }) => { const { status, reindexWarnings } = reindexState; - const { index, correctiveAction } = deprecation; - const { docLinks } = useAppContext(); - // If there are any warnings and we haven't started reindexing, show the warnings step first. - const [currentFlyoutStep, setCurrentFlyoutStep] = useState( - reindexWarnings && reindexWarnings.length > 0 && status === undefined - ? ReindexFlyoutStep.reindexWarnings - : ReindexFlyoutStep.checklist - ); + const { index } = deprecation; - let flyoutContents: React.ReactNode; + const [showWarningsStep, setShowWarningsStep] = useState(false); - const globalCallout = - (correctiveAction as ReindexAction).blockerForReindexing === 'index-closed' && - reindexState.status !== ReindexStatus.completed - ? getIndexClosedCallout(docLinks) - : undefined; - switch (currentFlyoutStep) { - case ReindexFlyoutStep.reindexWarnings: - flyoutContents = ( - globalCallout} - closeFlyout={closeFlyout} - warnings={reindexState.reindexWarnings!} - advanceNextStep={() => setCurrentFlyoutStep(ReindexFlyoutStep.checklist)} - /> - ); - break; - case ReindexFlyoutStep.checklist: - flyoutContents = ( - globalCallout} - closeFlyout={closeFlyout} - reindexState={reindexState} - startReindex={startReindex} - cancelReindex={cancelReindex} - /> - ); - break; - default: - throw new Error(`Invalid flyout step: ${currentFlyoutStep}`); - } + 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 ? ( + setShowWarningsStep(false)} + continueReindex={() => { + setShowWarningsStep(false); + onStartReindex(); + }} + /> + ) : ( + + ); return ( <> + + -

+

= ({

+ {flyoutContents} ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx index b49d81630221..1ee4cf2453bd 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import type { ReindexState } from '../use_reindex_state'; import { ReindexProgress } from './progress'; @@ -29,45 +29,69 @@ describe('ReindexProgress', () => { ); expect(wrapper).toMatchInlineSnapshot(` -, - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - ] - } -/> -`); + + +

+ +

+
+ , + }, + Object { + "status": "incomplete", + "title": , + }, + Object { + "status": "incomplete", + "title": , + }, + Object { + "status": "incomplete", + "title": , + }, + ] + } + /> +
+ `); }); it('displays errors in the step that failed', () => { @@ -84,104 +108,9 @@ describe('ReindexProgress', () => { cancelReindex={jest.fn()} /> ); - - const aliasStep = wrapper.props().steps[3]; + const aliasStep = (wrapper.find('StepProgress').props() as any).steps[3]; expect(aliasStep.children.props.errorMessage).toEqual( `This is an error that happened on alias switch` ); }); - - it('shows reindexing document progress bar', () => { - const wrapper = shallow( - - ); - - const reindexStep = wrapper.props().steps[2]; - expect(reindexStep.children.type.name).toEqual('ReindexProgressBar'); - expect(reindexStep.children.props.reindexState.reindexTaskPercComplete).toEqual(0.25); - }); - - it('adds steps for index groups', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchInlineSnapshot(` -, - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - ] - } -/> -`); - }); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx index 65a790fe9669..cf32a8bb3ab6 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx @@ -5,22 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; -import { - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiText, -} from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; +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'; const ErrorCallout: React.FunctionComponent<{ errorMessage: string | null }> = ({ errorMessage, @@ -39,22 +33,34 @@ const PausedCallout = () => ( /> ); -const ReindexProgressBar: React.FunctionComponent<{ +const ReindexingDocumentsStepTitle: React.FunctionComponent<{ reindexState: ReindexState; cancelReindex: () => void; -}> = ({ - reindexState: { lastCompletedStep, status, reindexTaskPercComplete, cancelLoadingState }, - cancelReindex, -}) => { - const progressBar = reindexTaskPercComplete ? ( - - ) : ( - - ); +}> = ({ reindexState: { lastCompletedStep, status, cancelLoadingState }, cancelReindex }) => { + if (status === ReindexStatus.cancelled) { + return ( + <> + + + ); + } + + // step is in progress after the new index is created and while it's not completed yet + const stepInProgress = + status === ReindexStatus.inProgress && + (lastCompletedStep === ReindexStep.newIndexCreated || + lastCompletedStep === ReindexStep.reindexStarted); + // but the reindex can only be cancelled after it has started + const showCancelLink = + status === ReindexStatus.inProgress && lastCompletedStep === ReindexStep.reindexStarted; let cancelText: React.ReactNode; switch (cancelLoadingState) { - case LoadingState.Loading: + case CancelLoadingState.Requested: + case CancelLoadingState.Loading: cancelText = ( ); break; - case LoadingState.Success: + case CancelLoadingState.Success: cancelText = ( ); break; - case LoadingState.Error: - cancelText = 'Could not cancel'; + case CancelLoadingState.Error: cancelText = ( - {progressBar} + - - {cancelText} - + {stepInProgress ? ( + + ) : ( + + )} + {showCancelLink && ( + + + {cancelText} + + + )} ); }; const orderedSteps = Object.values(ReindexStep).sort() as number[]; +const getStepTitle = (step: ReindexStep, inProgress?: boolean): ReactNode => { + if (step === ReindexStep.readonly) { + return inProgress ? ( + + ) : ( + + ); + } + if (step === ReindexStep.newIndexCreated) { + return inProgress ? ( + + ) : ( + + ); + } + if (step === ReindexStep.aliasCreated) { + return inProgress ? ( + + ) : ( + + ); + } +}; /** * Displays a list of steps in the reindex operation, the current status, a progress bar, @@ -118,48 +174,53 @@ export const ReindexProgress: React.FunctionComponent<{ reindexState: ReindexState; cancelReindex: () => void; }> = (props) => { - const { errorMessage, indexGroup, lastCompletedStep = -1, status } = props.reindexState; - const stepDetails = (thisStep: ReindexStep): Pick => { + const { + errorMessage, + lastCompletedStep = -1, + status, + reindexTaskPercComplete, + } = props.reindexState; + const getProgressStep = (thisStep: ReindexStep): StepProgressStep => { const previousStep = orderedSteps[orderedSteps.indexOf(thisStep) - 1]; if (status === ReindexStatus.failed && lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep), status: 'failed', children: , }; } else if (status === ReindexStatus.paused && lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep), status: 'paused', children: , }; } else if (status === ReindexStatus.cancelled && lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep), status: 'cancelled', }; } else if (status === undefined || lastCompletedStep < previousStep) { return { + title: getStepTitle(thisStep), status: 'incomplete', }; } else if (lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep, true), status: 'inProgress', }; } else { return { + title: getStepTitle(thisStep), status: 'complete', }; } }; - // The reindexing step is special because it combines the starting and complete statuses into a single UI - // with a progress bar. + // The reindexing step is special because it generally lasts longer and can be cancelled mid-flight const reindexingDocsStep = { - title: ( - - ), + title: , } as StepProgressStep; if ( @@ -189,82 +250,38 @@ export const ReindexProgress: React.FunctionComponent<{ lastCompletedStep === ReindexStep.reindexStarted ) { reindexingDocsStep.status = 'inProgress'; - reindexingDocsStep.children = ; } else { reindexingDocsStep.status = 'complete'; } const steps = [ - { - title: ( - - ), - ...stepDetails(ReindexStep.readonly), - }, - { - title: ( - - ), - ...stepDetails(ReindexStep.newIndexCreated), - }, + getProgressStep(ReindexStep.readonly), + getProgressStep(ReindexStep.newIndexCreated), reindexingDocsStep, - { - title: ( - - ), - ...stepDetails(ReindexStep.aliasCreated), - }, + getProgressStep(ReindexStep.aliasCreated), ]; - // If this index is part of an index group, add the approriate group services steps. - if (indexGroup === IndexGroup.ml) { - steps.unshift({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStopped), - }); - steps.push({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStarted), - }); - } else if (indexGroup === IndexGroup.watcher) { - steps.unshift({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStopped), - }); - steps.push({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStarted), - }); - } - - return ; + return ( + <> + +

+ {status === ReindexStatus.inProgress ? ( + + ) : ( + + )} +

+
+ + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx index 0973f721a537..01b4fe4eb84f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx @@ -10,6 +10,8 @@ import React, { Fragment, ReactNode } from '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 }) => { @@ -54,18 +56,14 @@ const Step: React.FunctionComponent = ({ }) => { const titleClassName = classNames('upgStepProgress__title', { // eslint-disable-next-line @typescript-eslint/naming-convention - 'upgStepProgress__title--currentStep': - status === 'inProgress' || - status === 'paused' || - status === 'failed' || - status === 'cancelled', + 'upgStepProgress__title--currentStep': status === 'inProgress', }); return (
-

{title}

+
{title}
{children &&
{children}
}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx index d2cafd69e94e..35e4a4b0b843 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx @@ -16,11 +16,6 @@ import { MAJOR_VERSION } from '../../../../../../../common/constants'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; const kibanaVersion = new SemVer(MAJOR_VERSION); -const mockKibanaVersionInfo = { - currentMajor: kibanaVersion.major, - prevMajor: kibanaVersion.major - 1, - nextMajor: kibanaVersion.major + 1, -}; jest.mock('../../../../../app_context', () => { const { docLinksServiceMock } = jest.requireActual( @@ -30,8 +25,11 @@ jest.mock('../../../../../app_context', () => { return { useAppContext: () => { return { - docLinks: docLinksServiceMock.createStartContract(), - kibanaVersionInfo: mockKibanaVersionInfo, + services: { + core: { + docLinks: docLinksServiceMock.createStartContract(), + }, + }, }; }, }; @@ -39,10 +37,9 @@ jest.mock('../../../../../app_context', () => { describe('WarningsFlyoutStep', () => { const defaultProps = { - advanceNextStep: jest.fn(), warnings: [] as ReindexWarning[], - closeFlyout: jest.fn(), - renderGlobalCallouts: jest.fn(), + hideWarningsStep: jest.fn(), + continueReindex: jest.fn(), }; it('renders', () => { @@ -76,7 +73,7 @@ describe('WarningsFlyoutStep', () => { const button = wrapper.find('EuiButton'); button.simulate('click'); - expect(defaultPropsWithWarnings.advanceNextStep).not.toHaveBeenCalled(); + expect(defaultPropsWithWarnings.continueReindex).not.toHaveBeenCalled(); // first warning (customTypeName) wrapper.find(`input#${idForWarning(0)}`).simulate('change'); @@ -84,7 +81,7 @@ describe('WarningsFlyoutStep', () => { wrapper.find(`input#${idForWarning(1)}`).simulate('change'); button.simulate('click'); - expect(defaultPropsWithWarnings.advanceNextStep).toHaveBeenCalled(); + expect(defaultPropsWithWarnings.continueReindex).toHaveBeenCalled(); }); } }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx index a5e326016721..904e9a5e1fec 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx @@ -105,7 +105,7 @@ export const CustomTypeNameWarningCheckbox: React.FunctionComponent{meta!.typeName as string}, }} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx index 4415811f6bf3..d8909d4ea039 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx @@ -16,6 +16,7 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiSpacer, + EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -40,10 +41,9 @@ const warningToComponentMap: { export const idForWarning = (id: number) => `reindexWarning-${id}`; interface WarningsConfirmationFlyoutProps { - renderGlobalCallouts: () => React.ReactNode; - closeFlyout: () => void; + hideWarningsStep: () => void; + continueReindex: () => void; warnings: ReindexWarning[]; - advanceNextStep: () => void; } /** @@ -52,11 +52,14 @@ interface WarningsConfirmationFlyoutProps { */ export const WarningsFlyoutStep: React.FunctionComponent = ({ warnings, - renderGlobalCallouts, - closeFlyout, - advanceNextStep, + hideWarningsStep, + continueReindex, }) => { - const { docLinks } = useAppContext(); + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); const { links } = docLinks; const [checkedIds, setCheckedIds] = useState( @@ -83,57 +86,66 @@ export const WarningsFlyoutStep: React.FunctionComponent - {renderGlobalCallouts()} - - - } - color="danger" - iconType="alert" - > -

- -

-
- - - - {warnings.map((warning, index) => { - const WarningCheckbox = warningToComponentMap[warning.warningType]; - return ( - - ); - })} + {warnings.length > 0 && ( + <> + + } + color="warning" + iconType="alert" + > +

+ +

+
+ + +

+ +

+
+ + {warnings.map((warning, index) => { + const WarningCheckbox = warningToComponentMap[warning.warningType]; + return ( + + ); + })} + + )}
- + - + diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx index 6ea9a0277059..b181e666c17e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx @@ -17,6 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { ReindexStatus } from '../../../../../../common/types'; +import { getReindexProgressLabel } from '../../../../lib/utils'; import { LoadingState } from '../../../types'; import { useReindexContext } from './context'; @@ -45,10 +46,16 @@ const i18nTexts = { defaultMessage: 'Reindex failed', } ), + reindexFetchFailedText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.reindex.reindexFetchFailedText', + { + defaultMessage: 'Reindex status not available', + } + ), reindexCanceledText: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.reindex.reindexCanceledText', { - defaultMessage: 'Reindex canceled', + defaultMessage: 'Reindex cancelled', } ), reindexPausedText: i18n.translate( @@ -64,7 +71,7 @@ const i18nTexts = { 'xpack.upgradeAssistant.esDeprecations.reindex.resolutionTooltipLabel', { defaultMessage: - 'Resolve this deprecation by reindexing this index. This is an automated resolution.', + 'Resolve this issue by reindexing this index. This issue can be resolved automatically.', } ), }; @@ -93,7 +100,13 @@ export const ReindexResolutionCell: React.FunctionComponent = () => { - {i18nTexts.reindexInProgressText} + + {i18nTexts.reindexInProgressText}{' '} + {getReindexProgressLabel( + reindexState.reindexTaskPercComplete, + reindexState.lastCompletedStep + )} + ); @@ -119,25 +132,25 @@ export const ReindexResolutionCell: React.FunctionComponent = () => { ); - case ReindexStatus.paused: + case ReindexStatus.fetchFailed: return ( - {i18nTexts.reindexPausedText} + {i18nTexts.reindexFetchFailedText} ); - case ReindexStatus.cancelled: + case ReindexStatus.paused: return ( - {i18nTexts.reindexCanceledText} + {i18nTexts.reindexPausedText} ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx index 1cf555b6cb34..1059720e66a5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx @@ -7,9 +7,15 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; +import { + uiMetricService, + UIM_REINDEX_CLOSE_FLYOUT_CLICK, + UIM_REINDEX_OPEN_FLYOUT_CLICK, +} from '../../../../lib/ui_metric'; import { DeprecationTableColumns } from '../../../types'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { ReindexResolutionCell } from './resolution_table_cell'; @@ -29,7 +35,6 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }) => { const [showFlyout, setShowFlyout] = useState(false); const reindexState = useReindexContext(); - const { api } = useAppContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -37,8 +42,8 @@ const ReindexTableRowCells: React.FunctionComponent = ({ const closeFlyout = useCallback(async () => { removeContentFromGlobalFlyout('reindexFlyout'); setShowFlyout(false); - await api.sendReindexTelemetryData({ close: true }); - }, [api, removeContentFromGlobalFlyout]); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_CLOSE_FLYOUT_CLICK); + }, [removeContentFromGlobalFlyout]); useEffect(() => { if (showFlyout) { @@ -52,6 +57,7 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'reindexDetails', 'aria-labelledby': 'reindexDetailsFlyoutTitle', }, @@ -61,13 +67,9 @@ const ReindexTableRowCells: React.FunctionComponent = ({ useEffect(() => { if (showFlyout) { - async function sendTelemetry() { - await api.sendReindexTelemetryData({ open: true }); - } - - sendTelemetry(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_OPEN_FLYOUT_CLICK); } - }, [showFlyout, api]); + }, [showFlyout]); return ( <> @@ -92,7 +94,9 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }; export const ReindexTableRow: React.FunctionComponent = (props) => { - const { api } = useAppContext(); + const { + services: { api }, + } = useAppContext(); return ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx index b87a509d25a5..e3a747e6615b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx @@ -8,39 +8,36 @@ import { useRef, useCallback, useState, useEffect } from 'react'; import { - IndexGroup, ReindexOperation, ReindexStatus, ReindexStep, ReindexWarning, } from '../../../../../../common/types'; -import { LoadingState } from '../../../types'; +import { CancelLoadingState, LoadingState } from '../../../types'; import { ApiService } from '../../../../lib/api'; const POLL_INTERVAL = 1000; export interface ReindexState { loadingState: LoadingState; - cancelLoadingState?: LoadingState; + cancelLoadingState?: CancelLoadingState; lastCompletedStep?: ReindexStep; status?: ReindexStatus; reindexTaskPercComplete: number | null; errorMessage: string | null; reindexWarnings?: ReindexWarning[]; hasRequiredPrivileges?: boolean; - indexGroup?: IndexGroup; } interface StatusResponse { warnings?: ReindexWarning[]; reindexOp?: ReindexOperation; hasRequiredPrivileges?: boolean; - indexGroup?: IndexGroup; } const getReindexState = ( reindexState: ReindexState, - { reindexOp, warnings, hasRequiredPrivileges, indexGroup }: StatusResponse + { reindexOp, warnings, hasRequiredPrivileges }: StatusResponse ) => { const newReindexState = { ...reindexState, @@ -55,10 +52,6 @@ const getReindexState = ( newReindexState.hasRequiredPrivileges = hasRequiredPrivileges; } - if (indexGroup) { - newReindexState.indexGroup = indexGroup; - } - if (reindexOp) { // Prevent the UI flickering back to inProgress after cancelling newReindexState.lastCompletedStep = reindexOp.lastCompletedStep; @@ -66,8 +59,21 @@ const getReindexState = ( newReindexState.reindexTaskPercComplete = reindexOp.reindexTaskPercComplete; newReindexState.errorMessage = reindexOp.errorMessage; - if (reindexOp.status === ReindexStatus.cancelled) { - newReindexState.cancelLoadingState = LoadingState.Success; + // if reindex cancellation was "requested" or "loading" and the reindex task is now cancelled, + // then reindex cancellation has completed, set it to "success" + if ( + (reindexState.cancelLoadingState === CancelLoadingState.Requested || + reindexState.cancelLoadingState === CancelLoadingState.Loading) && + reindexOp.status === ReindexStatus.cancelled + ) { + newReindexState.cancelLoadingState = CancelLoadingState.Success; + } else if ( + // if reindex cancellation has been requested and the reindex task is still in progress, + // then reindex cancellation has not completed yet, set it to "loading" + reindexState.cancelLoadingState === CancelLoadingState.Requested && + reindexOp.status === ReindexStatus.inProgress + ) { + newReindexState.cancelLoadingState = CancelLoadingState.Loading; } } @@ -97,75 +103,81 @@ export const useReindexStatus = ({ indexName, api }: { indexName: string; api: A const { data, error } = await api.getReindexStatus(indexName); if (error) { - setReindexState({ - ...reindexState, - loadingState: LoadingState.Error, - status: ReindexStatus.failed, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: ReindexStatus.fetchFailed, + }; }); return; } - setReindexState(getReindexState(reindexState, data)); + setReindexState((prevValue: ReindexState) => { + return getReindexState(prevValue, data); + }); // Only keep polling if it exists and is in progress. if (data.reindexOp && data.reindexOp.status === ReindexStatus.inProgress) { pollIntervalIdRef.current = setTimeout(updateStatus, POLL_INTERVAL); } - }, [clearPollInterval, api, indexName, reindexState]); + }, [clearPollInterval, api, indexName]); const startReindex = useCallback(async () => { - const currentReindexState = { - ...reindexState, - }; - - setReindexState({ - ...currentReindexState, - // Only reset last completed step if we aren't currently paused - lastCompletedStep: - currentReindexState.status === ReindexStatus.paused - ? currentReindexState.lastCompletedStep - : undefined, - status: ReindexStatus.inProgress, - reindexTaskPercComplete: null, - errorMessage: null, - cancelLoadingState: undefined, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + // Only reset last completed step if we aren't currently paused + lastCompletedStep: + prevValue.status === ReindexStatus.paused ? prevValue.lastCompletedStep : undefined, + status: ReindexStatus.inProgress, + reindexTaskPercComplete: null, + errorMessage: null, + cancelLoadingState: undefined, + }; }); - api.sendReindexTelemetryData({ start: true }); - - const { data, error } = await api.startReindexTask(indexName); + const { data: reindexOp, error } = await api.startReindexTask(indexName); if (error) { - setReindexState({ - ...reindexState, - loadingState: LoadingState.Error, - status: ReindexStatus.failed, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: ReindexStatus.failed, + }; }); return; } - setReindexState(getReindexState(reindexState, data)); + setReindexState((prevValue: ReindexState) => { + return getReindexState(prevValue, { reindexOp }); + }); updateStatus(); - }, [api, indexName, reindexState, updateStatus]); + }, [api, indexName, updateStatus]); const cancelReindex = useCallback(async () => { - api.sendReindexTelemetryData({ stop: true }); + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Requested, + }; + }); const { error } = await api.cancelReindexTask(indexName); - setReindexState({ - ...reindexState, - cancelLoadingState: LoadingState.Loading, - }); - if (error) { - setReindexState({ - ...reindexState, - cancelLoadingState: LoadingState.Error, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Error, + }; }); return; } - }, [api, indexName, reindexState]); + }, [api, indexName]); useEffect(() => { isMounted.current = true; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx deleted file mode 100644 index 5e3c7a5fe6ce..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx +++ /dev/null @@ -1,50 +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 from 'react'; - -import { EuiCallOut } from '@elastic/eui'; - -import { ResponseError } from '../../lib/api'; -import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; -interface Props { - error: ResponseError; -} - -export const EsDeprecationErrors: React.FunctionComponent = ({ error }) => { - const { code: errorType, message } = getEsDeprecationError(error); - - switch (errorType) { - case 'unauthorized_error': - return ( - - ); - case 'partially_upgraded_error': - return ( - - ); - case 'upgraded_error': - return ; - case 'request_error': - default: - return ( - - {error.message} - - ); - } -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index 38367bd3cfaf..270f597cb964 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -5,60 +5,110 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { EuiPageHeader, EuiSpacer, EuiPageContent } from '@elastic/eui'; +import { EuiPageHeader, EuiSpacer, EuiPageContent, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLinksStart } from 'kibana/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { EnrichedDeprecationInfo } from '../../../../common/types'; import { SectionLoading } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_ES_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; +import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; +import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; import { EsDeprecationsTable } from './es_deprecations_table'; -import { EsDeprecationErrors } from './es_deprecation_errors'; -import { NoDeprecationsPrompt } from '../shared'; + +const getDeprecationCountByLevel = (deprecations: EnrichedDeprecationInfo[]) => { + const criticalDeprecations: EnrichedDeprecationInfo[] = []; + const warningDeprecations: EnrichedDeprecationInfo[] = []; + + deprecations.forEach((deprecation) => { + if (deprecation.isCritical) { + criticalDeprecations.push(deprecation); + return; + } + warningDeprecations.push(deprecation); + }); + + return { + criticalDeprecations: criticalDeprecations.length, + warningDeprecations: warningDeprecations.length, + }; +}; const i18nTexts = { pageTitle: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageTitle', { - defaultMessage: 'Elasticsearch deprecation warnings', + defaultMessage: 'Elasticsearch deprecation issues', }), pageDescription: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageDescription', { defaultMessage: - 'You must resolve all critical issues before upgrading. Back up recommended. Make sure you have a current snapshot before modifying your configuration or reindexing.', + 'Resolve all critical issues before upgrading. Before making changes, ensure you have a current snapshot of your cluster. Indices created before 7.0 must be reindexed or removed. To start multiple reindexing tasks in a single request, use the Kibana batch reindexing API.', }), isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', { - defaultMessage: 'Loading deprecations…', + defaultMessage: 'Loading deprecation issues…', }), }; -export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { - const { api, breadcrumbs } = useAppContext(); +const getBatchReindexLink = (docLinks: DocLinksStart) => { + return ( + + {i18n.translate('xpack.upgradeAssistant.esDeprecations.batchReindexingDocsLink', { + defaultMessage: 'batch reindexing API', + })} + + ), + }} + /> + ); +}; +export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { const { - data: esDeprecations, - isLoading, - error, - resendRequest, - isInitialRequest, - } = api.useLoadEsDeprecations(); + services: { + api, + breadcrumbs, + core: { docLinks }, + }, + } = useAppContext(); + + const { data: esDeprecations, isLoading, error, resendRequest } = api.useLoadEsDeprecations(); + + const deprecationsCountByLevel: { + warningDeprecations: number; + criticalDeprecations: number; + } = useMemo( + () => getDeprecationCountByLevel(esDeprecations?.deprecations || []), + [esDeprecations?.deprecations] + ); useEffect(() => { breadcrumbs.setBreadcrumbs('esDeprecations'); }, [breadcrumbs]); useEffect(() => { - if (isLoading === false && isInitialRequest) { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - elasticsearch: true, - }); - } - - sendTelemetryData(); - } - }, [api, isLoading, isInitialRequest]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_ES_DEPRECATIONS_PAGE_LOAD); + }, []); if (error) { - return ; + return ( + + ); } if (isLoading) { @@ -82,7 +132,20 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { return (
- + + {i18nTexts.pageDescription} + {getBatchReindexLink(docLinks)} + + } + > + + diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx index f1654b203016..3d9b554913c5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx @@ -26,6 +26,7 @@ import { Query, } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../common/types'; +import { useAppContext } from '../../app_context'; import { MlSnapshotsTableRow, DefaultTableRow, @@ -33,7 +34,7 @@ import { ReindexTableRow, } from './deprecation_types'; import { DeprecationTableColumns } from '../types'; -import { DEPRECATION_TYPE_MAP } from '../constants'; +import { DEPRECATION_TYPE_MAP, PAGINATION_CONFIG } from '../constants'; const i18nTexts = { refreshButtonLabel: i18n.translate( @@ -99,12 +100,21 @@ const cellToLabelMap = { }; const cellTypes = Object.keys(cellToLabelMap) as DeprecationTableColumns[]; -const pageSizeOptions = [50, 100, 200]; +const pageSizeOptions = PAGINATION_CONFIG.pageSizeOptions; -const renderTableRowCells = (deprecation: EnrichedDeprecationInfo) => { +const renderTableRowCells = ( + deprecation: EnrichedDeprecationInfo, + mlUpgradeModeEnabled: boolean +) => { switch (deprecation.correctiveAction?.type) { case 'mlSnapshot': - return ; + return ( + + ); case 'indexSetting': return ; @@ -146,12 +156,19 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ deprecations = [], reload, }) => { + const { + services: { api }, + } = useAppContext(); + + const { data } = api.useLoadMlUpgradeMode(); + const mlUpgradeModeEnabled = !!data?.mlUpgradeModeEnabled; + const [sortConfig, setSortConfig] = useState({ isSortAscending: true, sortField: 'isCritical', }); - const [itemsPerPage, setItemsPerPage] = useState(pageSizeOptions[0]); + const [itemsPerPage, setItemsPerPage] = useState(PAGINATION_CONFIG.initialPageSize); const [currentPageIndex, setCurrentPageIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(EuiSearchBar.Query.MATCH_ALL); const [searchError, setSearchError] = useState<{ message: string } | undefined>(undefined); @@ -261,7 +278,7 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ - + {Object.entries(cellToLabelMap).map(([fieldName, cell]) => { return ( @@ -291,7 +308,7 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ {visibleDeprecations.map((deprecation, index) => { return ( - {renderTableRowCells(deprecation)} + {renderTableRowCells(deprecation, mlUpgradeModeEnabled)} ); })} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx index dd187f19d5e9..472ecccb4f02 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx @@ -7,10 +7,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBadge, EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../common/types'; import { DEPRECATION_TYPE_MAP } from '../constants'; import { DeprecationTableColumns } from '../types'; +import { DeprecationBadge } from '../shared'; interface Props { resolutionTableCell?: React.ReactNode; @@ -20,10 +21,16 @@ interface Props { } const i18nTexts = { - criticalBadgeLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.criticalBadgeLabel', + manualCellLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel', { - defaultMessage: 'Critical', + defaultMessage: 'Manual', + } + ), + manualCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.reindex.manualCellTooltipLabel', + { + defaultMessage: 'This issue needs to be resolved manually.', } ), }; @@ -36,11 +43,7 @@ export const EsDeprecationsTableCells: React.FunctionComponent = ({ }) => { // "Status column" if (fieldName === 'isCritical') { - if (deprecation.isCritical === true) { - return {i18nTexts.criticalBadgeLabel}; - } - - return <>{''}; + return ; } // "Issue" column @@ -66,7 +69,13 @@ export const EsDeprecationsTableCells: React.FunctionComponent = ({ return <>{resolutionTableCell}; } - return <>{''}; + return ( + + + {i18nTexts.manualCellLabel} + + + ); } // Default behavior: render value or empty string if undefined diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/index.ts new file mode 100644 index 000000000000..8924fa2d355a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { KibanaDeprecations } from './kibana_deprecations'; +export { EsDeprecations } from './es_deprecations'; +export { ComingSoonPrompt } from './coming_soon_prompt'; +export { Overview } from './overview'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss new file mode 100644 index 000000000000..c877ea4b4882 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss @@ -0,0 +1,4 @@ +// Used to add spacing between the list of manual deprecation steps +.upgResolveStep { + margin-bottom: $euiSizeL; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx new file mode 100644 index 000000000000..baf725b48e6a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx @@ -0,0 +1,242 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; + +import { + EuiButtonEmpty, + EuiButton, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { uiMetricService, UIM_KIBANA_QUICK_RESOLVE_CLICK } from '../../lib/ui_metric'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../shared'; +import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; + +import './_deprecation_details_flyout.scss'; + +export interface DeprecationDetailsFlyoutProps { + deprecation: KibanaDeprecationDetails; + closeFlyout: () => void; + resolveDeprecation: (deprecationDetails: KibanaDeprecationDetails) => Promise; + deprecationResolutionState?: DeprecationResolutionState; +} + +const i18nTexts = { + closeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.closeButtonLabel', + { + defaultMessage: 'Close', + } + ), + quickResolveButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveButtonLabel', + { + defaultMessage: 'Quick resolve', + } + ), + retryQuickResolveButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.retryQuickResolveButtonLabel', + { + defaultMessage: 'Try again', + } + ), + resolvedButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.resolvedButtonLabel', + { + defaultMessage: 'Resolved', + } + ), + quickResolveInProgressButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveInProgressButtonLabel', + { + defaultMessage: 'Resolution in progress…', + } + ), + quickResolveCalloutTitle: ( + + {i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveText', { + defaultMessage: 'Quick resolve', + })} + + ), + }} + /> + ), + quickResolveErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveErrorTitle', + { + defaultMessage: 'Error resolving issue', + } + ), + manualFixTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.manualFixTitle', + { + defaultMessage: 'How to fix', + } + ), +}; + +const getQuickResolveButtonLabel = (deprecationResolutionState?: DeprecationResolutionState) => { + if (deprecationResolutionState?.resolveDeprecationStatus === 'in_progress') { + return i18nTexts.quickResolveInProgressButtonLabel; + } + + if (deprecationResolutionState?.resolveDeprecationStatus === 'ok') { + return i18nTexts.resolvedButtonLabel; + } + + if (deprecationResolutionState?.resolveDeprecationError) { + return i18nTexts.retryQuickResolveButtonLabel; + } + + return i18nTexts.quickResolveButtonLabel; +}; + +export const DeprecationDetailsFlyout = ({ + deprecation, + closeFlyout, + resolveDeprecation, + deprecationResolutionState, +}: DeprecationDetailsFlyoutProps) => { + const { documentationUrl, message, correctiveActions, title } = deprecation; + const isCurrent = deprecationResolutionState?.id === deprecation.id; + const isResolved = isCurrent && deprecationResolutionState?.resolveDeprecationStatus === 'ok'; + + const onResolveDeprecation = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_KIBANA_QUICK_RESOLVE_CLICK); + resolveDeprecation(deprecation); + }, [deprecation, resolveDeprecation]); + + return ( + <> + + + + +

+ {title} +

+
+
+ + {deprecationResolutionState?.resolveDeprecationStatus === 'fail' && ( + <> + + {deprecationResolutionState.resolveDeprecationError} + + + + )} + + +

{message}

+ {documentationUrl && ( +

+ +

+ )} +
+ + + + {/* Hide resolution steps if already resolved */} + {!isResolved && ( +
+ {correctiveActions.api && ( + <> + + + + + )} + + {correctiveActions.manualSteps.length > 0 && ( + <> + +

{i18nTexts.manualFixTitle}

+
+ + + {correctiveActions.manualSteps.length === 1 ? ( +

+ {correctiveActions.manualSteps[0]} +

+ ) : ( +
    + {correctiveActions.manualSteps.map((step, stepIndex) => ( +
  1. + {step} +
  2. + ))} +
+ )} +
+ + )} +
+ )} +
+ + + + + + {i18nTexts.closeButtonLabel} + + + + {/* Only show the "Quick resolve" button if deprecation supports it and deprecation is not yet resolved */} + {correctiveActions.api && !isResolved && ( + + + {getQuickResolveButtonLabel(deprecationResolutionState)} + + + )} + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx deleted file mode 100644 index 5bcc49590c55..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx +++ /dev/null @@ -1,145 +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, { FunctionComponent } from 'react'; -import { - EuiAccordion, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiText, - EuiCallOut, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { DeprecationHealth } from '../shared'; -import { LEVEL_MAP } from '../constants'; -import { StepsModalContent } from './steps_modal'; - -const i18nTexts = { - getDeprecationTitle: (domainId: string) => { - return i18n.translate('xpack.upgradeAssistant.deprecationGroupItemTitle', { - defaultMessage: "'{domainId}' is using a deprecated feature", - values: { - domainId, - }, - }); - }, - docLinkText: i18n.translate('xpack.upgradeAssistant.deprecationGroupItem.docLinkText', { - defaultMessage: 'View documentation', - }), - manualFixButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel', - { - defaultMessage: 'Show steps to fix', - } - ), - resolveButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel', - { - defaultMessage: 'Quick resolve', - } - ), -}; - -export interface Props { - deprecation: DomainDeprecationDetails; - index: number; - forceExpand: boolean; - showStepsModal: (modalContent: StepsModalContent) => void; - showResolveModal: (deprecation: DomainDeprecationDetails) => void; -} - -export const KibanaDeprecationAccordion: FunctionComponent = ({ - deprecation, - forceExpand, - index, - showStepsModal, - showResolveModal, -}) => { - const { domainId, level, message, documentationUrl, correctiveActions } = deprecation; - - return ( - } - > - - - - {level === 'fetch_error' ? ( - - ) : ( - <> -

{message}

- - {(documentationUrl || correctiveActions?.manualSteps) && ( - - {correctiveActions?.api && ( - - showResolveModal(deprecation)} - > - {i18nTexts.resolveButtonLabel} - - - )} - - {correctiveActions?.manualSteps && ( - - - showStepsModal({ - domainId, - steps: correctiveActions.manualSteps!, - documentationUrl, - }) - } - > - {i18nTexts.manualFixButtonLabel} - - - )} - - {documentationUrl && ( - - - {i18nTexts.docLinkText} - - - )} - - )} - - )} -
-
-
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx deleted file mode 100644 index fb61efc373ac..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx +++ /dev/null @@ -1,150 +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, { FunctionComponent, useState, useEffect } from 'react'; -import { groupBy } from 'lodash'; -import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; - -import type { DomainDeprecationDetails } from 'kibana/public'; - -import { LevelFilterOption } from '../types'; -import { SearchBar, DeprecationListBar, DeprecationPagination } from '../shared'; -import { DEPRECATIONS_PER_PAGE } from '../constants'; -import { KibanaDeprecationAccordion } from './deprecation_item'; -import { StepsModalContent } from './steps_modal'; -import { KibanaDeprecationErrors } from './kibana_deprecation_errors'; - -interface Props { - deprecations: DomainDeprecationDetails[]; - showStepsModal: (newStepsModalContent: StepsModalContent) => void; - showResolveModal: (deprecation: DomainDeprecationDetails) => void; - reloadDeprecations: () => Promise; - isLoading: boolean; -} - -const getFilteredDeprecations = ( - deprecations: DomainDeprecationDetails[], - level: LevelFilterOption, - search: string -) => { - return deprecations - .filter((deprecation) => { - return level === 'all' || deprecation.level === level; - }) - .filter((filteredDep) => { - if (search.length > 0) { - try { - // 'i' is used for case-insensitive matching - const searchReg = new RegExp(search, 'i'); - return searchReg.test(filteredDep.message); - } catch (e) { - // ignore any regexp errors - return true; - } - } - return true; - }); -}; - -export const KibanaDeprecationList: FunctionComponent = ({ - deprecations, - showStepsModal, - showResolveModal, - reloadDeprecations, - isLoading, -}) => { - const [currentFilter, setCurrentFilter] = useState('all'); - const [search, setSearch] = useState(''); - const [expandState, setExpandState] = useState({ - forceExpand: false, - expandNumber: 0, - }); - const [currentPage, setCurrentPage] = useState(0); - - const setExpandAll = (expandAll: boolean) => { - setExpandState({ forceExpand: expandAll, expandNumber: expandState.expandNumber + 1 }); - }; - - const levelGroups = groupBy(deprecations, 'level'); - const levelToDeprecationCountMap = Object.keys(levelGroups).reduce((counts, level) => { - counts[level] = levelGroups[level].length; - return counts; - }, {} as { [level: string]: number }); - - const filteredDeprecations = getFilteredDeprecations(deprecations, currentFilter, search); - - const deprecationsWithErrors = deprecations.filter((dep) => dep.level === 'fetch_error'); - - useEffect(() => { - const pageCount = Math.ceil(filteredDeprecations.length / DEPRECATIONS_PER_PAGE); - if (currentPage >= pageCount) { - setCurrentPage(0); - } - }, [filteredDeprecations, currentPage]); - - return ( - <> - - - {deprecationsWithErrors.length > 0 && ( - <> - - - - )} - - - - - - <> - {filteredDeprecations - .slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE) - .map((deprecation, index) => [ -
- - -
, - ])} - - {/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE */} - {filteredDeprecations.length > DEPRECATIONS_PER_PAGE && ( - <> - - - - - )} - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts index 84d2b8875718..6a1375f57cd4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KibanaDeprecationsContent } from './kibana_deprecations'; +export { KibanaDeprecations } from './kibana_deprecations'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx deleted file mode 100644 index 79ada21941b5..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx +++ /dev/null @@ -1,73 +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 from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; - -interface Props { - errorType: 'pluginError' | 'requestError'; -} - -const i18nTexts = { - pluginError: { - title: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle', { - defaultMessage: 'Not all Kibana deprecations were retrieved successfully', - }), - description: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription', - { - defaultMessage: 'Check the Kibana server logs for errors.', - } - ), - }, - loadingError: { - title: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle', { - defaultMessage: 'Could not retrieve Kibana deprecations', - }), - description: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription', - { - defaultMessage: 'Check the Kibana server logs for errors.', - } - ), - }, -}; - -export const KibanaDeprecationErrors: React.FunctionComponent = ({ errorType }) => { - if (errorType === 'pluginError') { - return ( - - {i18nTexts.pluginError.title}} - body={

{i18nTexts.pluginError.description}

} - /> -
- ); - } - - return ( - - {i18nTexts.loadingError.title}} - body={

{i18nTexts.loadingError.description}

} - /> -
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index 56d6e23d9d4f..3b4cd5acafb9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -5,29 +5,32 @@ * 2.0. */ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import uuid from 'uuid'; import { withRouter, RouteComponentProps } from 'react-router-dom'; - -import { EuiButtonEmpty, EuiPageContent, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { EuiPageContent, EuiPageHeader, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import type { DomainDeprecationDetails } from 'kibana/public'; -import { SectionLoading } from '../../../shared_imports'; +import { SectionLoading, GlobalFlyout } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; -import { NoDeprecationsPrompt } from '../shared'; -import { KibanaDeprecationList } from './deprecation_list'; -import { StepsModal, StepsModalContent } from './steps_modal'; -import { KibanaDeprecationErrors } from './kibana_deprecation_errors'; -import { ResolveDeprecationModal } from './resolve_deprecation_modal'; -import { LEVEL_MAP } from '../constants'; +import { uiMetricService, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; +import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; +import { KibanaDeprecationsTable } from './kibana_deprecations_table'; +import { + DeprecationDetailsFlyout, + DeprecationDetailsFlyoutProps, +} from './deprecation_details_flyout'; + +const { useGlobalFlyout } = GlobalFlyout; const i18nTexts = { pageTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageTitle', { - defaultMessage: 'Kibana', + defaultMessage: 'Kibana deprecation issues', }), pageDescription: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageDescription', { - defaultMessage: - 'Review the issues listed here and make the necessary changes before upgrading. Critical issues must be resolved before you upgrade.', + defaultMessage: 'Resolve all critical issues before upgrading.', }), docLinkText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.docLinkText', { defaultMessage: 'Documentation', @@ -36,43 +39,109 @@ const i18nTexts = { defaultMessage: 'Kibana', }), isLoading: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.loadingText', { - defaultMessage: 'Loading deprecations…', - }), - successMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.successMessage', { - defaultMessage: 'Deprecation resolved', - }), - errorMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.errorMessage', { - defaultMessage: 'Error resolving deprecation', + defaultMessage: 'Loading deprecation issues…', }), + kibanaDeprecationErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.kibanaDeprecationErrorTitle', + { + defaultMessage: 'List of deprecation issues might be incomplete', + } + ), + getKibanaDeprecationErrorDescription: (pluginIds: string[]) => + i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.kibanaDeprecationErrorDescription', { + defaultMessage: + 'Failed to get deprecation issues for {pluginCount, plural, one {this plugin} other {these plugins}}: {pluginIds}. Check the Kibana server logs for more information.', + values: { + pluginCount: pluginIds.length, + pluginIds: pluginIds.join(', '), + }, + }), }; -const sortByLevelDesc = (a: DomainDeprecationDetails, b: DomainDeprecationDetails) => { - return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]); +export interface DeprecationResolutionState { + id: string; + resolveDeprecationStatus: 'ok' | 'fail' | 'in_progress'; + resolveDeprecationError?: string; +} + +export type KibanaDeprecationDetails = DomainDeprecationDetails & { + id: string; + filterType: DomainDeprecationDetails['deprecationType'] | 'uncategorized'; }; -export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponentProps) => { +const getDeprecationCountByLevel = (deprecations: KibanaDeprecationDetails[]) => { + const criticalDeprecations: KibanaDeprecationDetails[] = []; + const warningDeprecations: KibanaDeprecationDetails[] = []; + + deprecations.forEach((deprecation) => { + if (deprecation.level === 'critical') { + criticalDeprecations.push(deprecation); + return; + } + warningDeprecations.push(deprecation); + }); + + return { + criticalDeprecations: criticalDeprecations.length, + warningDeprecations: warningDeprecations.length, + }; +}; + +export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) => { const [kibanaDeprecations, setKibanaDeprecations] = useState< - DomainDeprecationDetails[] | undefined + KibanaDeprecationDetails[] | undefined >(undefined); + const [kibanaDeprecationErrors, setKibanaDeprecationErrors] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); - const [stepsModalContent, setStepsModalContent] = useState( + const [flyoutContent, setFlyoutContent] = useState( undefined ); - const [resolveModalContent, setResolveModalContent] = useState< - undefined | DomainDeprecationDetails + const [deprecationResolutionState, setDeprecationResolutionState] = useState< + DeprecationResolutionState | undefined >(undefined); - const [isResolvingDeprecation, setIsResolvingDeprecation] = useState(false); - const { deprecations, breadcrumbs, docLinks, api, notifications } = useAppContext(); + const { + services: { + core: { deprecations }, + breadcrumbs, + }, + } = useAppContext(); + + const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = + useGlobalFlyout(); const getAllDeprecations = useCallback(async () => { setIsLoading(true); try { - const response = await deprecations.getAllDeprecations(); - const sortedDeprecations = response.sort(sortByLevelDesc); - setKibanaDeprecations(sortedDeprecations); + const allDeprecations = await deprecations.getAllDeprecations(); + + const filteredDeprecations: KibanaDeprecationDetails[] = []; + const deprecationErrors: string[] = []; + + allDeprecations.forEach((deprecation) => { + // Keep track of any plugin deprecations that failed to fetch to show warning in UI + if (deprecation.level === 'fetch_error') { + // It's possible that a plugin registered more than one deprecation that could fail + // We only want to keep track of the unique plugin failures + const pluginErrorExists = deprecationErrors.includes(deprecation.domainId); + if (pluginErrorExists === false) { + deprecationErrors.push(deprecation.domainId); + } + return; + } + + // Only show deprecations in the table that fetched successfully + filteredDeprecations.push({ + ...deprecation, + id: uuid.v4(), // Associate an unique ID with each deprecation to track resolution state + filterType: deprecation.deprecationType ?? 'uncategorized', // deprecationType is currently optional, in order to correctly handle sort/filter, we default any undefined types to "uncategorized" + }); + }); + + setKibanaDeprecations(filteredDeprecations); + setKibanaDeprecationErrors(deprecationErrors); } catch (e) { setError(e); } @@ -80,45 +149,72 @@ export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponent setIsLoading(false); }, [deprecations]); - const toggleStepsModal = (newStepsModalContent?: StepsModalContent) => { - setStepsModalContent(newStepsModalContent); - }; + const deprecationsCountByLevel: { + warningDeprecations: number; + criticalDeprecations: number; + } = useMemo(() => getDeprecationCountByLevel(kibanaDeprecations || []), [kibanaDeprecations]); - const toggleResolveModal = (newResolveModalContent?: DomainDeprecationDetails) => { - setResolveModalContent(newResolveModalContent); + const toggleFlyout = (newFlyoutContent?: KibanaDeprecationDetails) => { + setFlyoutContent(newFlyoutContent); }; - const resolveDeprecation = async (deprecationDetails: DomainDeprecationDetails) => { - setIsResolvingDeprecation(true); + const closeFlyout = useCallback(() => { + toggleFlyout(); + removeContentFromGlobalFlyout('deprecationDetails'); + }, [removeContentFromGlobalFlyout]); - const response = await deprecations.resolveDeprecation(deprecationDetails); + const resolveDeprecation = useCallback( + async (deprecationDetails: KibanaDeprecationDetails) => { + setDeprecationResolutionState({ + id: deprecationDetails.id, + resolveDeprecationStatus: 'in_progress', + }); - setIsResolvingDeprecation(false); - toggleResolveModal(); + const response = await deprecations.resolveDeprecation(deprecationDetails); - // Handle error case - if (response.status === 'fail') { - notifications.toasts.addError(new Error(response.reason), { - title: i18nTexts.errorMessage, + setDeprecationResolutionState({ + id: deprecationDetails.id, + resolveDeprecationStatus: response.status, + resolveDeprecationError: response.status === 'fail' ? response.reason : undefined, }); - return; - } - - notifications.toasts.addSuccess(i18nTexts.successMessage); - // Refetch deprecations - getAllDeprecations(); - }; + closeFlyout(); + }, + [closeFlyout, deprecations] + ); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - kibana: true, + if (flyoutContent) { + addContentToGlobalFlyout({ + id: 'deprecationDetails', + Component: DeprecationDetailsFlyout, + props: { + deprecation: flyoutContent, + closeFlyout, + resolveDeprecation, + deprecationResolutionState: + deprecationResolutionState && flyoutContent.id === deprecationResolutionState.id + ? deprecationResolutionState + : undefined, + }, + flyoutProps: { + onClose: closeFlyout, + 'data-test-subj': 'kibanaDeprecationDetails', + 'aria-labelledby': 'kibanaDeprecationDetailsFlyoutTitle', + }, }); } + }, [ + addContentToGlobalFlyout, + closeFlyout, + deprecationResolutionState, + flyoutContent, + resolveDeprecation, + ]); - sendTelemetryData(); - }, [api]); + useEffect(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('kibanaDeprecations'); @@ -128,69 +224,65 @@ export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponent getAllDeprecations(); }, [deprecations, getAllDeprecations]); - if (kibanaDeprecations && kibanaDeprecations.length === 0) { + if (error) { + return ; + } + + if (isLoading) { return ( - history.push('/overview')} - /> + {i18nTexts.isLoading} ); } - if (isLoading) { + if (kibanaDeprecations?.length === 0) { return ( - {i18nTexts.isLoading} + history.push('/overview')} + /> ); - } else if (kibanaDeprecations?.length) { - return ( -
- - {i18nTexts.docLinkText} - , - ]} + } + + return ( +
+ + + - + - + {kibanaDeprecationErrors.length > 0 && ( + <> + +

{i18nTexts.getKibanaDeprecationErrorDescription(kibanaDeprecationErrors)}

+
- {stepsModalContent && ( - toggleStepsModal()} modalContent={stepsModalContent} /> - )} - - {resolveModalContent && ( - toggleResolveModal()} - resolveDeprecation={resolveDeprecation} - isResolvingDeprecation={isResolvingDeprecation} - deprecation={resolveModalContent} - /> - )} -
- ); - } else if (error) { - return ; - } + + + )} - return null; + +
+ ); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx new file mode 100644 index 000000000000..6a757d0cb2b0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx @@ -0,0 +1,235 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink, Search } from '@elastic/eui'; + +import { PAGINATION_CONFIG } from '../constants'; +import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; +import { ResolutionTableCell } from './resolution_table_cell'; +import { DeprecationBadge } from '../shared'; + +const i18nTexts = { + refreshButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.refreshButtonLabel', + { + defaultMessage: 'Refresh', + } + ), + statusColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.statusColumnTitle', + { + defaultMessage: 'Status', + } + ), + issueColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.issueColumnTitle', + { + defaultMessage: 'Issue', + } + ), + typeColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.typeColumnTitle', + { + defaultMessage: 'Type', + } + ), + resolutionColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.resolutionColumnTitle', + { + defaultMessage: 'Resolution', + } + ), + configDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.configDeprecationTypeCellLabel', + { + defaultMessage: 'Config', + } + ), + featureDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.featureDeprecationTypeCellLabel', + { + defaultMessage: 'Feature', + } + ), + unknownDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.unknownDeprecationTypeCellLabel', + { + defaultMessage: 'Uncategorized', + } + ), + typeFilterLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.typeFilterLabel', + { + defaultMessage: 'Type', + } + ), + criticalFilterLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.criticalFilterLabel', + { + defaultMessage: 'Critical', + } + ), + searchPlaceholderLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.searchPlaceholderLabel', + { + defaultMessage: 'Filter', + } + ), +}; + +interface Props { + deprecations?: KibanaDeprecationDetails[]; + reload: () => void; + toggleFlyout: (newFlyoutContent?: KibanaDeprecationDetails) => void; + deprecationResolutionState?: DeprecationResolutionState; +} + +export const KibanaDeprecationsTable: React.FunctionComponent = ({ + deprecations, + reload, + toggleFlyout, + deprecationResolutionState, +}) => { + const columns: Array> = [ + { + field: 'level', + name: i18nTexts.statusColumnTitle, + width: '5%', + truncateText: true, + sortable: true, + render: (level: KibanaDeprecationDetails['level']) => { + return ; + }, + }, + { + field: 'title', + width: '40%', + name: i18nTexts.issueColumnTitle, + truncateText: true, + sortable: true, + render: (title: KibanaDeprecationDetails['title'], deprecation: KibanaDeprecationDetails) => { + return ( + toggleFlyout(deprecation)} + data-test-subj="deprecationDetailsLink" + > + {title} + + ); + }, + }, + { + field: 'filterType', + name: i18nTexts.typeColumnTitle, + width: '20%', + truncateText: true, + sortable: true, + render: (filterType: KibanaDeprecationDetails['filterType']) => { + switch (filterType) { + case 'config': + return i18nTexts.configDeprecationTypeCellLabel; + case 'feature': + return i18nTexts.featureDeprecationTypeCellLabel; + case 'uncategorized': + default: + return i18nTexts.unknownDeprecationTypeCellLabel; + } + }, + }, + { + field: 'correctiveActions', + name: i18nTexts.resolutionColumnTitle, + width: '30%', + truncateText: true, + sortable: true, + render: ( + correctiveActions: KibanaDeprecationDetails['correctiveActions'], + deprecation: KibanaDeprecationDetails + ) => { + return ( + + ); + }, + }, + ]; + + const sorting = { + sort: { + field: 'level', + direction: 'asc', + }, + } as const; + + const searchConfig: Search = { + filters: [ + { + type: 'field_value_toggle', + name: i18nTexts.criticalFilterLabel, + field: 'level', + value: 'critical', + }, + { + type: 'field_value_selection', + field: 'filterType', + name: i18nTexts.typeFilterLabel, + multiSelect: false, + options: [ + { + value: 'config', + name: i18nTexts.configDeprecationTypeCellLabel, + }, + { + value: 'feature', + name: i18nTexts.featureDeprecationTypeCellLabel, + }, + { + value: 'uncategorized', + name: i18nTexts.unknownDeprecationTypeCellLabel, + }, + ], + }, + ], + box: { + incremental: true, + placeholder: i18nTexts.searchPlaceholderLabel, + }, + toolsRight: [ + + {i18nTexts.refreshButtonLabel} + , + ], + }; + + return ( + ({ + 'data-test-subj': 'row', + })} + cellProps={(deprecation, field) => ({ + 'data-test-subj': `${((field?.name as string) || 'table').toLowerCase()}Cell`, + })} + data-test-subj="kibanaDeprecationsTable" + tableLayout="auto" + /> + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx new file mode 100644 index 000000000000..daf276b7ed3f --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx @@ -0,0 +1,145 @@ +/* + * 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 { + EuiFlexItem, + EuiText, + EuiFlexGroup, + EuiIcon, + EuiLoadingSpinner, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import type { DeprecationResolutionState } from './kibana_deprecations'; + +const i18nTexts = { + manualCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.manualCellLabel', + { + defaultMessage: 'Manual', + } + ), + manualCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.manualCellTooltipLabel', + { + defaultMessage: 'This issue needs to be resolved manually.', + } + ), + automatedCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automatedCellLabel', + { + defaultMessage: 'Automated', + } + ), + automationInProgressCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automationInProgressCellLabel', + { + defaultMessage: 'Resolution in progress…', + } + ), + automationCompleteCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automationCompleteCellLabel', + { + defaultMessage: 'Resolved', + } + ), + automationFailedCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automationFailedCellLabel', + { + defaultMessage: 'Resolution failed', + } + ), + automatedCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automatedCellTooltipLabel', + { + defaultMessage: 'This issue can be resolved automatically.', + } + ), +}; + +interface Props { + deprecationId: string; + isAutomated: boolean; + deprecationResolutionState?: DeprecationResolutionState; +} + +export const ResolutionTableCell: React.FunctionComponent = ({ + deprecationId, + isAutomated, + deprecationResolutionState, +}) => { + if (isAutomated) { + if (deprecationResolutionState?.id === deprecationId) { + const { resolveDeprecationStatus } = deprecationResolutionState; + + switch (resolveDeprecationStatus) { + case 'in_progress': + return ( + + + + + + {i18nTexts.automationInProgressCellLabel} + + + ); + case 'fail': + return ( + + + + + + {i18nTexts.automationFailedCellLabel} + + + ); + case 'ok': + default: + return ( + + + + + + {i18nTexts.automationCompleteCellLabel} + + + ); + } + } + + return ( + + + + + + + {i18nTexts.automatedCellLabel} + + + + ); + } + + return ( + + + {i18nTexts.manualCellLabel} + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx deleted file mode 100644 index f94512fac563..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx +++ /dev/null @@ -1,64 +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, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiConfirmModal } from '@elastic/eui'; -import type { DomainDeprecationDetails } from 'kibana/public'; - -interface Props { - closeModal: () => void; - deprecation: DomainDeprecationDetails; - isResolvingDeprecation: boolean; - resolveDeprecation: (deprecationDetails: DomainDeprecationDetails) => Promise; -} - -const i18nTexts = { - getModalTitle: (domainId: string) => - i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle', - { - defaultMessage: "Resolve deprecation in '{domainId}'?", - values: { - domainId, - }, - } - ), - cancelButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - ), - resolveButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel', - { - defaultMessage: 'Resolve', - } - ), -}; - -export const ResolveDeprecationModal: FunctionComponent = ({ - closeModal, - deprecation, - isResolvingDeprecation, - resolveDeprecation, -}) => { - return ( - resolveDeprecation(deprecation)} - cancelButtonText={i18nTexts.cancelButtonLabel} - confirmButtonText={i18nTexts.resolveButtonLabel} - defaultFocusedButton="confirm" - isLoading={isResolvingDeprecation} - /> - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx deleted file mode 100644 index 98027d4f46aa..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx +++ /dev/null @@ -1,115 +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, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiText, - EuiSteps, - EuiButton, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiTitle, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; - -export interface StepsModalContent { - domainId: string; - steps: string[]; - documentationUrl?: string; -} - -interface Props { - closeModal: () => void; - modalContent: StepsModalContent; -} - -const i18nTexts = { - getModalTitle: (domainId: string) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle', { - defaultMessage: "Resolve deprecation in '{domainId}'", - values: { - domainId, - }, - }), - getStepTitle: (step: number) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle', { - defaultMessage: 'Step {step}', - values: { - step, - }, - }), - docLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel', - { - defaultMessage: 'View documentation', - } - ), - closeButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel', - { - defaultMessage: 'Close', - } - ), -}; - -export const StepsModal: FunctionComponent = ({ closeModal, modalContent }) => { - const { domainId, steps, documentationUrl } = modalContent; - - return ( - - - - -

{i18nTexts.getModalTitle(domainId)}

-
-
-
- - - { - return { - title: i18nTexts.getStepTitle(index + 1), - children: ( - -

{step}

-
- ), - }; - })} - /> -
- - - - {documentationUrl && ( - - - {i18nTexts.docLinkLabel} - - - )} - - - - {i18nTexts.closeButtonLabel} - - - - -
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss deleted file mode 100644 index cbcfbff3bab6..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'review_logs_step/index'; -@import 'fix_deprecation_logs_step/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx new file mode 100644 index 000000000000..46b11aee15b3 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx @@ -0,0 +1,45 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { CloudSetup } from '../../../../../../cloud/public'; +import { OnPremBackup } from './on_prem_backup'; +import { CloudBackup } from './cloud_backup'; +import type { OverviewStepProps } from '../../types'; + +const title = i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', { + defaultMessage: 'Back up your data', +}); + +interface Props extends OverviewStepProps { + cloud?: CloudSetup; +} + +export const getBackupStep = ({ cloud, isComplete, setIsComplete }: Props): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + if (cloud?.isCloudEnabled) { + return { + status, + title, + 'data-test-subj': `backupStep-${status}`, + children: ( + + ), + }; + } + + return { + title, + 'data-test-subj': 'backupStep-incomplete', + status: 'incomplete', + children: , + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx new file mode 100644 index 000000000000..4ab860a0bf6a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import moment from 'moment-timezone'; +import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { + EuiLoadingContent, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiButton, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; + +import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_CLOUD_CLICK } from '../../../lib/ui_metric'; + +interface Props { + cloudSnapshotsUrl: string; + setIsComplete: (isComplete: boolean) => void; +} + +export const CloudBackup: React.FunctionComponent = ({ + cloudSnapshotsUrl, + setIsComplete, +}) => { + const { + services: { api }, + } = useAppContext(); + + const { isInitialRequest, isLoading, error, data, resendRequest } = + api.useLoadCloudBackupStatus(); + + // Tell overview whether the step is complete or not. + useEffect(() => { + // Loading shouldn't invalidate the previous state. + if (!isLoading) { + // An error should invalidate the previous state. + setIsComplete((!error && data?.isBackedUp) ?? false); + } + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading, data]); + + if (isInitialRequest && isLoading) { + return ; + } + + if (error) { + return ( + +

+ {error.statusCode} - {error.message} +

+ + {i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.retryButton', { + defaultMessage: 'Try again', + })} + +
+ ); + } + + const lastBackupTime = moment(data!.lastBackupTime).toISOString(); + + const statusMessage = data!.isBackedUp ? ( + + + + + + + +

+ + {' '} + + + ), + }} + /> +

+
+
+
+ ) : ( + + + + + + + +

+ {i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.noSnapshotMessage', { + defaultMessage: `Your data isn't backed up.`, + })} +

+
+
+
+ ); + + return ( + <> + {statusMessage} + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_CLOUD_CLICK); + }} + data-test-subj="cloudSnapshotsLink" + target="_blank" + iconType="popout" + iconSide="right" + > + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/index.ts similarity index 84% rename from x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/index.ts index 31ad78cf572f..8daac9645fa1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { SearchBar } from './search_bar'; +export { getBackupStep } from './backup_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx new file mode 100644 index 000000000000..e512eb5a301d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx @@ -0,0 +1,59 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; + +import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_ON_PREM_CLICK } from '../../../lib/ui_metric'; + +const SnapshotRestoreAppLink: React.FunctionComponent = () => { + const { + plugins: { share }, + } = useAppContext(); + + const snapshotRestoreUrl = share.url.locators + .get('SNAPSHOT_RESTORE_LOCATOR') + ?.useUrl({ page: 'snapshots' }); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_ON_PREM_CLICK); + }} + data-test-subj="snapshotRestoreLink" + > + + + ); +}; + +export const OnPremBackup: React.FunctionComponent = () => { + return ( + <> + +

+ {i18n.translate('xpack.upgradeAssistant.overview.backupStepDescription', { + defaultMessage: 'Make sure you have a current snapshot before making any changes.', + })} +

+
+ + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss deleted file mode 100644 index 2299c08a4ac3..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'deprecation_logging_toggle/deprecation_logging_toggle'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx deleted file mode 100644 index 0cd5ad5bfdb2..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx +++ /dev/null @@ -1,120 +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, { FunctionComponent, useState, useEffect } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiText } from '@elastic/eui'; - -import { useAppContext } from '../../../app_context'; -import { useKibana, DataPublicPluginStart } from '../../../../shared_imports'; -import { - DEPRECATION_LOGS_INDEX_PATTERN, - DEPRECATION_LOGS_SOURCE_ID, -} from '../../../../../common/constants'; - -const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) => { - const { indexPatterns: indexPatternService } = dataService; - - const results = await indexPatternService.find(DEPRECATION_LOGS_INDEX_PATTERN); - // Since the find might return also results with wildcard matchers we need to find the - // index pattern that has an exact match with our title. - const deprecationIndexPattern = results.find( - (result) => result.title === DEPRECATION_LOGS_INDEX_PATTERN - ); - - if (deprecationIndexPattern) { - return deprecationIndexPattern.id; - } else { - const newIndexPattern = await indexPatternService.createAndSave({ - title: DEPRECATION_LOGS_INDEX_PATTERN, - allowNoIndex: true, - }); - return newIndexPattern.id; - } -}; - -const DiscoverAppLink: FunctionComponent = () => { - const { getUrlForApp } = useAppContext(); - const { data: dataService, discover: discoverService } = useKibana().services; - - const [discoveryUrl, setDiscoveryUrl] = useState(); - - useEffect(() => { - const getDiscoveryUrl = async () => { - const indexPatternId = await getDeprecationIndexPatternId(dataService); - const appLocation = await discoverService?.locator?.getLocation({ indexPatternId }); - - const result = getUrlForApp(appLocation?.app as string, { - path: appLocation?.path, - }); - setDiscoveryUrl(result); - }; - - getDiscoveryUrl(); - }, [dataService, discoverService, getUrlForApp]); - - return ( - - - - ); -}; - -const ObservabilityAppLink: FunctionComponent = () => { - const { http } = useAppContext(); - const logStreamUrl = http?.basePath?.prepend( - `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}` - ); - - return ( - - - - ); -}; - -export const ExternalLinks: FunctionComponent = () => { - return ( - - - - -

- -

-
- - -
-
- - - -

- -

-
- - -
-
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx deleted file mode 100644 index a2f1feae4979..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx +++ /dev/null @@ -1,90 +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, { FunctionComponent } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { EuiText, EuiSpacer, EuiPanel, EuiCallOut } from '@elastic/eui'; -import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; - -import { ExternalLinks } from './external_links'; -import { useDeprecationLogging } from './use_deprecation_logging'; -import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; - -const i18nTexts = { - identifyStepTitle: i18n.translate('xpack.upgradeAssistant.overview.identifyStepTitle', { - defaultMessage: 'Identify deprecated API use and update your applications', - }), - toggleTitle: i18n.translate('xpack.upgradeAssistant.overview.toggleTitle', { - defaultMessage: 'Log Elasticsearch deprecation warnings', - }), - analyzeTitle: i18n.translate('xpack.upgradeAssistant.overview.analyzeTitle', { - defaultMessage: 'Analyze deprecation logs', - }), - onlyLogWritingEnabledTitle: i18n.translate( - 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningTitle', - { - defaultMessage: 'Your logs are being written to the logs directory', - } - ), - onlyLogWritingEnabledBody: i18n.translate( - 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningBody', - { - defaultMessage: - 'Go to your logs directory to view the deprecation logs or enable log collecting to see them in the UI.', - } - ), -}; - -const DeprecationLogsPreview: FunctionComponent = () => { - const state = useDeprecationLogging(); - - return ( - <> - -

{i18nTexts.toggleTitle}

-
- - - - - - {state.onlyDeprecationLogWritingEnabled && ( - <> - - -

{i18nTexts.onlyLogWritingEnabledBody}

-
- - )} - - {state.isDeprecationLogIndexingEnabled && ( - <> - - -

{i18nTexts.analyzeTitle}

-
- - - - )} - - ); -}; - -export const getFixDeprecationLogsStep = (): EuiStepProps => { - return { - title: i18nTexts.identifyStepTitle, - status: 'incomplete', - children: , - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss new file mode 100644 index 000000000000..37079275b185 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss @@ -0,0 +1,24 @@ +/** + * Push success state to the bottom + * of the card, so it aligns with , + * which is inside EuiStat. + */ +.upgDeprecationIssuesPanel .euiCard__content { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/** + * Ensure the stat is a consistent height, even when it contains + * , which is shorter than the + * standard number value. We also push it to the bottom of the its + * container, to base-align it with the number value. + */ +.upgDeprecationIssuesPanel__stat { + height: 60px; // Derived from font measurements, not sizing vars + justify-content: space-between; + flex-grow: 1; + flex-direction: column; + display: flex; +} \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx new file mode 100644 index 000000000000..8c42e71c0ef2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx @@ -0,0 +1,135 @@ +/* + * 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, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiCard, EuiStat, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { reactRouterNavigate } from '../../../../../shared_imports'; +import { DeprecationSource } from '../../../../../../common/types'; +import { getDeprecationsUpperLimit } from '../../../../lib/utils'; +import { LoadingIssuesError } from './loading_issues_error'; +import { NoDeprecationIssues } from './no_deprecation_issues'; + +import './_deprecation_issues_panel.scss'; + +const i18nTexts = { + warningDeprecationsTitle: i18n.translate( + 'xpack.upgradeAssistant.deprecationStats.warningDeprecationsTitle', + { + defaultMessage: 'Warning', + } + ), + criticalDeprecationsTitle: i18n.translate( + 'xpack.upgradeAssistant.deprecationStats.criticalDeprecationsTitle', + { + defaultMessage: 'Critical', + } + ), +}; + +interface Props { + 'data-test-subj': string; + deprecationSource: DeprecationSource; + linkUrl: string; + criticalDeprecationsCount: number; + warningDeprecationsCount: number; + isLoading: boolean; + errorMessage?: JSX.Element | string | null; + setIsFixed: (isFixed: boolean) => void; +} + +export const DeprecationIssuesPanel = (props: Props) => { + const { + deprecationSource, + linkUrl, + criticalDeprecationsCount, + warningDeprecationsCount, + isLoading, + errorMessage, + setIsFixed, + } = props; + const history = useHistory(); + + const hasError = !!errorMessage; + const hasCriticalIssues = criticalDeprecationsCount > 0; + const hasWarningIssues = warningDeprecationsCount > 0; + const hasNoIssues = !isLoading && !hasError && !hasWarningIssues && !hasCriticalIssues; + + useEffect(() => { + if (!isLoading && !errorMessage) { + setIsFixed(criticalDeprecationsCount === 0); + } + }, [setIsFixed, criticalDeprecationsCount, isLoading, errorMessage]); + + return ( + + + + {hasError ? ( + {errorMessage} + ) : hasNoIssues ? ( + + ) : ( + + + + ) + } + titleElement="span" + description={i18nTexts.criticalDeprecationsTitle} + titleColor="danger" + isLoading={isLoading} + /> + + + + + ) + } + titleElement="span" + description={i18nTexts.warningDeprecationsTitle} + isLoading={isLoading} + /> + + + )} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx new file mode 100644 index 000000000000..b4258ababc92 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +import { useAppContext } from '../../../../app_context'; +import { getEsDeprecationError } from '../../../../lib/get_es_deprecation_error'; +import { DeprecationIssuesPanel } from './deprecation_issues_panel'; + +interface Props { + setIsFixed: (isFixed: boolean) => void; +} + +export const EsDeprecationIssuesPanel: FunctionComponent = ({ setIsFixed }) => { + const { + services: { api }, + } = useAppContext(); + + const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations(); + + const criticalDeprecationsCount = + esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical)?.length ?? 0; + + const warningDeprecationsCount = + esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) + ?.length ?? 0; + + const errorMessage = error && getEsDeprecationError(error).message; + + return ( + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts new file mode 100644 index 000000000000..a2a321900271 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts @@ -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 { EsDeprecationIssuesPanel } from './es_deprecation_issues_panel'; +export { KibanaDeprecationIssuesPanel } from './kibana_deprecation_issues_panel'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx new file mode 100644 index 000000000000..b0aa7b592e3a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx @@ -0,0 +1,73 @@ +/* + * 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, { FunctionComponent, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { DomainDeprecationDetails } from 'kibana/public'; + +import { useAppContext } from '../../../../app_context'; +import { DeprecationIssuesPanel } from './deprecation_issues_panel'; + +interface Props { + setIsFixed: (isFixed: boolean) => void; +} + +export const KibanaDeprecationIssuesPanel: FunctionComponent = ({ setIsFixed }) => { + const { + services: { + core: { deprecations }, + }, + } = useAppContext(); + + const [kibanaDeprecations, setKibanaDeprecations] = useState< + DomainDeprecationDetails[] | undefined + >(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + async function getAllDeprecations() { + setIsLoading(true); + + try { + const response = await deprecations.getAllDeprecations(); + setKibanaDeprecations(response); + } catch (e) { + setError(e); + } + + setIsLoading(false); + } + + getAllDeprecations(); + }, [deprecations]); + + const criticalDeprecationsCount = + kibanaDeprecations?.filter((deprecation) => deprecation.level === 'critical')?.length ?? 0; + + const warningDeprecationsCount = + kibanaDeprecations?.filter((deprecation) => deprecation.level === 'warning')?.length ?? 0; + + const errorMessage = + error && + i18n.translate('xpack.upgradeAssistant.deprecationStats.loadingErrorMessage', { + defaultMessage: 'Could not retrieve Kibana deprecation issues.', + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.tsx new file mode 100644 index 000000000000..cdd406dc8622 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.tsx @@ -0,0 +1,21 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; + +export const LoadingIssuesError: FunctionComponent = ({ children }) => ( + + + + + + + {children} + + +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.tsx new file mode 100644 index 000000000000..168a682ab6d3 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.tsx @@ -0,0 +1,45 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const i18nTexts = { + noPartialDeprecationIssuesText: i18n.translate( + 'xpack.upgradeAssistant.noPartialDeprecationsMessage', + { + defaultMessage: 'None', + } + ), + noDeprecationIssuesText: i18n.translate('xpack.upgradeAssistant.noDeprecationsMessage', { + defaultMessage: 'No issues', + }), +}; + +interface Props { + isPartial?: boolean; + 'data-test-subj'?: string; +} + +export const NoDeprecationIssues: FunctionComponent = (props) => { + const { isPartial = false } = props; + + return ( + + + + + + + + {isPartial ? i18nTexts.noPartialDeprecationIssuesText : i18nTexts.noDeprecationIssuesText} + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx new file mode 100644 index 000000000000..61d25404b2ae --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx @@ -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 React, { FunctionComponent, useState, useEffect } from 'react'; + +import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { OverviewStepProps } from '../../types'; +import { EsDeprecationIssuesPanel, KibanaDeprecationIssuesPanel } from './components'; + +const i18nTexts = { + reviewStepTitle: i18n.translate('xpack.upgradeAssistant.overview.fixIssuesStepTitle', { + defaultMessage: 'Review deprecated settings and resolve issues', + }), +}; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +const FixIssuesStep: FunctionComponent = ({ setIsComplete }) => { + // We consider ES and Kibana issues to be fixed when there are 0 critical issues. + const [isEsFixed, setIsEsFixed] = useState(false); + const [isKibanaFixed, setIsKibanaFixed] = useState(false); + + useEffect(() => { + setIsComplete(isEsFixed && isKibanaFixed); + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEsFixed, isKibanaFixed]); + + return ( + + + + + + + + + + ); +}; + +export const getFixIssuesStep = ({ + isComplete, + setIsComplete, +}: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + title: i18nTexts.reviewStepTitle, + status, + 'data-test-subj': `fixIssuesStep-${status}`, + children: ( + <> + +

+ +

+
+ + + + + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/index.ts similarity index 82% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/index.ts index a2684505eb9c..dde6996edfc7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { NoDeprecations } from './no_deprecations'; +export { getFixIssuesStep } from './fix_issues_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx similarity index 89% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx index 42b9f073a52f..cddf5101a4b4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx @@ -20,9 +20,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ResponseError } from '../../../../lib/api'; +import { ResponseError } from '../../../../../../common/types'; import { DeprecationLoggingPreviewProps } from '../../../types'; +import './_deprecation_logging_toggle.scss'; + const i18nTexts = { fetchErrorMessage: i18n.translate( 'xpack.upgradeAssistant.overview.deprecationLogs.fetchErrorMessage', @@ -46,10 +48,10 @@ const i18nTexts = { defaultMessage: 'Error', }), buttonLabel: i18n.translate('xpack.upgradeAssistant.overview.deprecationLogs.buttonLabel', { - defaultMessage: 'Enable deprecation logging and indexing', + defaultMessage: 'Enable deprecation log collection', }), loadingLogsLabel: i18n.translate('xpack.upgradeAssistant.overview.loadingLogsLabel', { - defaultMessage: 'Loading log collection state…', + defaultMessage: 'Loading deprecation log collection state…', }), }; @@ -77,7 +79,18 @@ const ErrorDetailsLink = ({ error }: { error: ResponseError }) => { ); }; -export const DeprecationLoggingToggle: FunctionComponent = ({ +type Props = Pick< + DeprecationLoggingPreviewProps, + | 'isDeprecationLogIndexingEnabled' + | 'isLoading' + | 'isUpdating' + | 'fetchError' + | 'updateError' + | 'resendRequest' + | 'toggleLogging' +>; + +export const DeprecationLoggingToggle: FunctionComponent = ({ isDeprecationLogIndexingEnabled, isLoading, isUpdating, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/index.ts similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/index.ts diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx new file mode 100644 index 000000000000..6ce1fec32d66 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx @@ -0,0 +1,149 @@ +/* + * 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, { FunctionComponent, useEffect, useState } from 'react'; +import moment from 'moment-timezone'; +import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiButton, EuiLoadingContent } from '@elastic/eui'; + +import { useAppContext } from '../../../../app_context'; +import { uiMetricService, UIM_RESET_LOGS_COUNTER_CLICK } from '../../../../lib/ui_metric'; + +const i18nTexts = { + calloutTitle: (warningsCount: number, previousCheck: string) => ( + + {' '} + + + ), + }} + /> + ), + calloutBody: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.calloutBody', { + defaultMessage: `After making changes, reset the counter and continue monitoring to verify you're no longer using deprecated features.`, + }), + loadingError: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.loadingError', { + defaultMessage: 'An error occurred while retrieving the count of deprecation logs', + }), + retryButton: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.retryButton', { + defaultMessage: 'Try again', + }), + resetCounterButton: i18n.translate( + 'xpack.upgradeAssistant.overview.verifyChanges.resetCounterButton', + { + defaultMessage: 'Reset counter', + } + ), + errorToastTitle: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.errorToastTitle', { + defaultMessage: 'Could not delete deprecation logs cache', + }), +}; + +interface Props { + checkpoint: string; + setCheckpoint: (value: string) => void; + setHasNoDeprecationLogs: (hasNoLogs: boolean) => void; +} + +export const DeprecationsCountCheckpoint: FunctionComponent = ({ + checkpoint, + setCheckpoint, + setHasNoDeprecationLogs, +}) => { + const [isDeletingCache, setIsDeletingCache] = useState(false); + const { + services: { + api, + core: { notifications }, + }, + } = useAppContext(); + const { data, error, isLoading, resendRequest, isInitialRequest } = + api.getDeprecationLogsCount(checkpoint); + + const logsCount = data?.count || 0; + const hasLogs = logsCount > 0; + const calloutTint = hasLogs ? 'warning' : 'success'; + const calloutIcon = hasLogs ? 'alert' : 'check'; + const calloutTestId = hasLogs ? 'hasWarningsCallout' : 'noWarningsCallout'; + + const onResetClick = async () => { + setIsDeletingCache(true); + const { error: deleteLogsCacheError } = await api.deleteDeprecationLogsCache(); + setIsDeletingCache(false); + + if (deleteLogsCacheError) { + notifications.toasts.addDanger({ + title: i18nTexts.errorToastTitle, + text: deleteLogsCacheError.message.toString(), + }); + return; + } + + const now = moment().toISOString(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_RESET_LOGS_COUNTER_CLICK); + setCheckpoint(now); + }; + + useEffect(() => { + // Loading shouldn't invalidate the previous state. + if (!isLoading) { + // An error should invalidate the previous state. + setHasNoDeprecationLogs(!error && !hasLogs); + } + // Depending upon setHasNoDeprecationLogs would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading, hasLogs]); + + if (isInitialRequest && isLoading) { + return ; + } + + if (error) { + return ( + +

+ {error.statusCode} - {error.message} +

+ + {i18nTexts.retryButton} + +
+ ); + } + + return ( + +

{i18nTexts.calloutBody}

+ + {i18nTexts.resetCounterButton} + +
+ ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/index.ts similarity index 76% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/index.ts index d4794623d8a9..e32655f90b84 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { getFixDeprecationLogsStep } from './fix_deprecation_logs_step'; +export { DeprecationsCountCheckpoint } from './deprecations_count_checkpoint'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts new file mode 100644 index 000000000000..64a8920324d8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { getDeprecationIndexPatternId } from './external_links'; + +import { DEPRECATION_LOGS_INDEX_PATTERN } from '../../../../../common/constants'; +import { dataPluginMock, Start } from '../../../../../../../../src/plugins/data/public/mocks'; + +describe('External Links', () => { + let dataService: Start; + + beforeEach(() => { + dataService = dataPluginMock.createStartContract(); + }); + + describe('getDeprecationIndexPatternId', () => { + it('creates new index pattern if doesnt exist', async () => { + dataService.dataViews.find = jest.fn().mockResolvedValue([]); + dataService.dataViews.createAndSave = jest.fn().mockResolvedValue({ id: '123-456' }); + + const indexPatternId = await getDeprecationIndexPatternId(dataService); + + expect(indexPatternId).toBe('123-456'); + // prettier-ignore + expect(dataService.dataViews.createAndSave).toHaveBeenCalledWith({ + title: DEPRECATION_LOGS_INDEX_PATTERN, + allowNoIndex: true, + }, false, true); + }); + + it('uses existing index pattern if it already exists', async () => { + dataService.dataViews.find = jest.fn().mockResolvedValue([ + { + id: '123-456', + title: DEPRECATION_LOGS_INDEX_PATTERN, + }, + ]); + + const indexPatternId = await getDeprecationIndexPatternId(dataService); + + expect(indexPatternId).toBe('123-456'); + expect(dataService.dataViews.find).toHaveBeenCalledWith(DEPRECATION_LOGS_INDEX_PATTERN); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx new file mode 100644 index 000000000000..dec43145ef96 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx @@ -0,0 +1,175 @@ +/* + * 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 { encode } from 'rison-node'; +import React, { FunctionComponent, useState, useEffect } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiText } from '@elastic/eui'; + +import { DataPublicPluginStart } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { + uiMetricService, + UIM_OBSERVABILITY_CLICK, + UIM_DISCOVER_CLICK, +} from '../../../lib/ui_metric'; + +import { + DEPRECATION_LOGS_INDEX_PATTERN, + DEPRECATION_LOGS_SOURCE_ID, +} from '../../../../../common/constants'; + +interface Props { + checkpoint: string; +} + +export const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) => { + const results = await dataService.dataViews.find(DEPRECATION_LOGS_INDEX_PATTERN); + // Since the find might return also results with wildcard matchers we need to find the + // index pattern that has an exact match with our title. + const deprecationIndexPattern = results.find( + (result) => result.title === DEPRECATION_LOGS_INDEX_PATTERN + ); + + if (deprecationIndexPattern) { + return deprecationIndexPattern.id; + } else { + // When creating the index pattern, we need to be careful when creating an indexPattern + // for an index that doesnt exist. Since the deprecation logs data stream is only created + // when a deprecation log is indexed it could be possible that it might not exist at the + // time we need to render the DiscoveryAppLink. + // So in order to avoid those errors we need to make sure that the indexPattern is created + // with allowNoIndex and that we skip fetching fields to from the source index. + const override = false; + const skipFetchFields = true; + // prettier-ignore + const newIndexPattern = await dataService.dataViews.createAndSave({ + title: DEPRECATION_LOGS_INDEX_PATTERN, + allowNoIndex: true, + }, override, skipFetchFields); + + return newIndexPattern.id; + } +}; + +const DiscoverAppLink: FunctionComponent = ({ checkpoint }) => { + const { + services: { data: dataService }, + plugins: { share }, + } = useAppContext(); + + const [discoveryUrl, setDiscoveryUrl] = useState(); + + useEffect(() => { + const getDiscoveryUrl = async () => { + const indexPatternId = await getDeprecationIndexPatternId(dataService); + const locator = share.url.locators.get('DISCOVER_APP_LOCATOR'); + + if (!locator) { + return; + } + + const url = await locator.getUrl({ + indexPatternId, + query: { + language: 'kuery', + query: `@timestamp > "${checkpoint}"`, + }, + }); + + setDiscoveryUrl(url); + }; + + getDiscoveryUrl(); + }, [dataService, checkpoint, share.url.locators]); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DISCOVER_CLICK); + }} + data-test-subj="viewDiscoverLogs" + > + + + ); +}; + +const ObservabilityAppLink: FunctionComponent = ({ checkpoint }) => { + const { + services: { + core: { http }, + }, + } = useAppContext(); + const logStreamUrl = http?.basePath?.prepend( + `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}&logPosition=(end:now,start:${encode( + checkpoint + )})` + ); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_OBSERVABILITY_CLICK); + }} + data-test-subj="viewObserveLogs" + > + + + ); +}; + +export const ExternalLinks: FunctionComponent = ({ checkpoint }) => { + const { infra: hasInfraPlugin } = useAppContext().plugins; + + return ( + + {hasInfraPlugin && ( + + + +

+ +

+
+ + +
+
+ )} + + + +

+ +

+
+ + +
+
+
+ ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx new file mode 100644 index 000000000000..a3e81f6edcd3 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx @@ -0,0 +1,235 @@ +/* + * 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, { FunctionComponent, useState, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiSpacer, EuiLink, EuiCallOut, EuiCode } from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import { useAppContext } from '../../../app_context'; +import { ExternalLinks } from './external_links'; +import { DeprecationsCountCheckpoint } from './deprecations_count_checkpoint'; +import { useDeprecationLogging } from './use_deprecation_logging'; +import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; +import { loadLogsCheckpoint, saveLogsCheckpoint } from '../../../lib/logs_checkpoint'; +import type { OverviewStepProps } from '../../types'; +import { DEPRECATION_LOGS_INDEX } from '../../../../../common/constants'; +import { WithPrivileges, MissingPrivileges } from '../../../../shared_imports'; + +const i18nTexts = { + identifyStepTitle: i18n.translate('xpack.upgradeAssistant.overview.identifyStepTitle', { + defaultMessage: 'Identify deprecated API use and update your applications', + }), + analyzeTitle: i18n.translate('xpack.upgradeAssistant.overview.analyzeTitle', { + defaultMessage: 'Analyze deprecation logs', + }), + deprecationsCountCheckpointTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationsCountCheckpointTitle', + { + defaultMessage: 'Resolve deprecation issues and verify your changes', + } + ), + apiCompatibilityNoteTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.apiCompatibilityNoteTitle', + { + defaultMessage: 'Apply API compatibility headers (optional)', + } + ), + apiCompatibilityNoteBody: (docLink: string) => ( + + + + ), + }} + /> + ), + onlyLogWritingEnabledTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningTitle', + { + defaultMessage: 'Your logs are being written to the logs directory', + } + ), + onlyLogWritingEnabledBody: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningBody', + { + defaultMessage: + 'Go to your logs directory to view the deprecation logs or enable deprecation log collection to see them in Kibana.', + } + ), + deniedPrivilegeTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deniedPrivilegeTitle', + { + defaultMessage: 'You require index privileges to analyze the deprecation logs', + } + ), + deniedPrivilegeDescription: (privilegesMissing: MissingPrivileges) => ( + // NOTE: hardcoding the missing privilege because the WithPrivileges HOC + // doesnt provide a way to retrieve which specific privileges an index + // is missing. + {privilegesMissing?.index?.join(', ')} + ), + privilegesCount: privilegesMissing?.index?.length, + }} + /> + ), +}; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; +} + +const FixLogsStep: FunctionComponent = ({ + setIsComplete, + hasPrivileges, + privilegesMissing, +}) => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + + const { + isDeprecationLogIndexingEnabled, + onlyDeprecationLogWritingEnabled, + isLoading, + isUpdating, + fetchError, + updateError, + resendRequest, + toggleLogging, + } = useDeprecationLogging(); + + const [checkpoint, setCheckpoint] = useState(loadLogsCheckpoint()); + + useEffect(() => { + saveLogsCheckpoint(checkpoint); + }, [checkpoint]); + + useEffect(() => { + if (!isDeprecationLogIndexingEnabled) { + setIsComplete(false); + } + + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDeprecationLogIndexingEnabled]); + + return ( + <> + + + {onlyDeprecationLogWritingEnabled && ( + <> + + +

{i18nTexts.onlyLogWritingEnabledBody}

+
+ + )} + + {!hasPrivileges && isDeprecationLogIndexingEnabled && ( + <> + + +

{i18nTexts.deniedPrivilegeDescription(privilegesMissing)}

+
+ + )} + + {hasPrivileges && isDeprecationLogIndexingEnabled && ( + <> + + +

{i18nTexts.analyzeTitle}

+
+ + + + + +

{i18nTexts.deprecationsCountCheckpointTitle}

+
+ + + + + +

{i18nTexts.apiCompatibilityNoteTitle}

+
+ + +

+ {i18nTexts.apiCompatibilityNoteBody( + docLinks.links.elasticsearch.apiCompatibilityHeader + )} +

+
+ + )} + + ); +}; + +export const getFixLogsStep = ({ isComplete, setIsComplete }: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + status, + title: i18nTexts.identifyStepTitle, + 'data-test-subj': `fixLogsStep-${status}`, + children: ( + + {({ hasPrivileges, privilegesMissing, isLoading }) => ( + + )} + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/index.ts similarity index 83% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/index.ts index daf2644c2477..8a9a9faa6d09 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ESDeprecationStats } from './es_stats'; +export { getFixLogsStep } from './fix_logs_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/use_deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/use_deprecation_logging.ts similarity index 94% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/use_deprecation_logging.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/use_deprecation_logging.ts index 1aa34f2ec97c..e25fd91ae2c5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/use_deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/use_deprecation_logging.ts @@ -9,8 +9,8 @@ import { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; +import { ResponseError } from '../../../../../common/types'; import { useAppContext } from '../../../app_context'; -import { ResponseError } from '../../../lib/api'; import { DeprecationLoggingPreviewProps } from '../../types'; const i18nTexts = { @@ -29,7 +29,12 @@ const i18nTexts = { }; export const useDeprecationLogging = (): DeprecationLoggingPreviewProps => { - const { api, notifications } = useAppContext(); + const { + services: { + api, + core: { notifications }, + }, + } = useAppContext(); const { data, error: fetchError, isLoading, resendRequest } = api.useLoadDeprecationLogging(); const [isDeprecationLogIndexingEnabled, setIsDeprecationLogIndexingEnabled] = useState(false); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx new file mode 100644 index 000000000000..632994d4948a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx @@ -0,0 +1,193 @@ +/* + * 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 { startCase } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiLoadingSpinner, + EuiTitle, + EuiText, + EuiIcon, + EuiSpacer, + EuiInMemoryTable, +} from '@elastic/eui'; + +import { + SystemIndicesMigrationStatus, + SystemIndicesMigrationFeature, + MIGRATION_STATUS, +} from '../../../../../common/types'; + +export interface SystemIndicesFlyoutProps { + closeFlyout: () => void; + data: SystemIndicesMigrationStatus; +} + +const i18nTexts = { + closeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.flyoutCloseButtonLabel', + { + defaultMessage: 'Close', + } + ), + flyoutTitle: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.flyoutTitle', { + defaultMessage: 'Migrate system indices', + }), + flyoutDescription: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.flyoutDescription', + { + defaultMessage: + 'Migrate the indices that store information for the following features before you upgrade.', + } + ), + migrationCompleteLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.migrationCompleteLabel', + { + defaultMessage: 'Migration complete', + } + ), + needsMigrationLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.needsMigrationLabel', + { + defaultMessage: 'Migration required', + } + ), + migratingLabel: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.migratingLabel', { + defaultMessage: 'Migration in progress', + }), + errorLabel: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.errorLabel', { + defaultMessage: 'Migration failed', + }), + featureNameTableColumn: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.featureNameTableColumn', + { + defaultMessage: 'Feature', + } + ), + statusTableColumn: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.statusTableColumn', + { + defaultMessage: 'Status', + } + ), +}; + +const renderMigrationStatus = (status: MIGRATION_STATUS) => { + if (status === 'NO_MIGRATION_NEEDED') { + return ( + + + + + + +

{i18nTexts.migrationCompleteLabel}

+
+
+
+ ); + } + + if (status === 'MIGRATION_NEEDED') { + return ( + +

{i18nTexts.needsMigrationLabel}

+
+ ); + } + + if (status === 'IN_PROGRESS') { + return ( + + + + + + +

{i18nTexts.migratingLabel}

+
+
+
+ ); + } + + if (status === 'ERROR') { + return ( + + + + + + +

{i18nTexts.errorLabel}

+
+
+
+ ); + } + + return ''; +}; + +const columns = [ + { + field: 'feature_name', + name: i18nTexts.featureNameTableColumn, + sortable: true, + truncateText: true, + render: (name: string) => startCase(name), + }, + { + field: 'migration_status', + name: i18nTexts.statusTableColumn, + sortable: true, + render: renderMigrationStatus, + }, +]; + +export const SystemIndicesFlyout = ({ closeFlyout, data }: SystemIndicesFlyoutProps) => { + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + +

{i18nTexts.flyoutDescription}

+
+ + + data-test-subj="featuresTable" + itemId="feature_name" + items={data.features} + columns={columns} + pagination={true} + sorting={true} + /> +
+ + + + + {i18nTexts.closeButtonLabel} + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts similarity index 77% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts index 231b8ba2d777..0be86929f2a4 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { getReviewLogsStep } from './review_logs_step'; +export { getMigrateSystemIndicesStep } from './migrate_system_indices'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx new file mode 100644 index 000000000000..d14958148b2f --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx @@ -0,0 +1,239 @@ +/* + * 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, { FunctionComponent, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiText, + EuiButton, + EuiSpacer, + EuiIcon, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiCode, +} from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { SystemIndicesMigrationFeature } from '../../../../../common/types'; +import type { OverviewStepProps } from '../../types'; +import { useMigrateSystemIndices } from './use_migrate_system_indices'; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +const getFailureCause = (features: SystemIndicesMigrationFeature[]) => { + const featureWithError = features.find((feature) => feature.migration_status === 'ERROR'); + + if (featureWithError) { + const indexWithError = featureWithError.indices.find((index) => index.failure_cause); + return { + feature: featureWithError?.feature_name, + failureCause: indexWithError?.failure_cause?.error.type, + }; + } + + return {}; +}; + +const i18nTexts = { + title: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.title', { + defaultMessage: 'Migrate system indices', + }), + bodyDescription: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.body', { + defaultMessage: 'Migrate the indices that store system information before you upgrade.', + }), + startButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.startButtonLabel', + { + defaultMessage: 'Migrate indices', + } + ), + inProgressButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.inProgressButtonLabel', + { + defaultMessage: 'Migration in progress', + } + ), + noMigrationNeeded: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.noMigrationNeeded', + { + defaultMessage: 'Migration complete', + } + ), + viewSystemIndicesStatus: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.viewSystemIndicesStatus', + { + defaultMessage: 'View migration details', + } + ), + retryButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.retryButtonLabel', + { + defaultMessage: 'Retry migration', + } + ), + loadingError: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.loadingError', { + defaultMessage: 'Could not retrieve the system indices status', + }), + migrationFailedTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.migrationFailedTitle', + { + defaultMessage: 'System indices migration failed', + } + ), + migrationFailedBody: (features: SystemIndicesMigrationFeature[]) => { + const { feature, failureCause } = getFailureCause(features); + + return ( + {failureCause}, + }} + /> + ); + }, +}; + +const MigrateSystemIndicesStep: FunctionComponent = ({ setIsComplete }) => { + const { beginSystemIndicesMigration, startMigrationStatus, migrationStatus, setShowFlyout } = + useMigrateSystemIndices(); + + useEffect(() => { + setIsComplete(migrationStatus.data?.migration_status === 'NO_MIGRATION_NEEDED'); + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [migrationStatus.data?.migration_status]); + + if (migrationStatus.error) { + return ( + +

+ {migrationStatus.error.statusCode} - {migrationStatus.error.message} +

+ + {i18nTexts.retryButtonLabel} + +
+ ); + } + + if (migrationStatus.data?.migration_status === 'NO_MIGRATION_NEEDED') { + return ( + + + + + + +

{i18nTexts.noMigrationNeeded}

+
+
+
+ ); + } + + const isButtonDisabled = migrationStatus.isInitialRequest && migrationStatus.isLoading; + const isMigrating = migrationStatus.data?.migration_status === 'IN_PROGRESS'; + + return ( + <> + {startMigrationStatus.statusType === 'error' && ( + <> + + + + )} + + {migrationStatus.data?.migration_status === 'ERROR' && ( + <> + +

{i18nTexts.migrationFailedBody(migrationStatus.data?.features)}

+
+ + + )} + + + + + {isMigrating ? i18nTexts.inProgressButtonLabel : i18nTexts.startButtonLabel} + + + + setShowFlyout(true)} + isDisabled={isButtonDisabled} + data-test-subj="viewSystemIndicesStateButton" + > + {i18nTexts.viewSystemIndicesStatus} + + + + + ); +}; + +export const getMigrateSystemIndicesStep = ({ + isComplete, + setIsComplete, +}: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + title: i18nTexts.title, + status, + 'data-test-subj': `migrateSystemIndicesStep-${status}`, + children: ( + <> + +

{i18nTexts.bodyDescription}

+
+ + + + + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts new file mode 100644 index 000000000000..d38e73562816 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts @@ -0,0 +1,93 @@ +/* + * 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, useEffect } from 'react'; +import useInterval from 'react-use/lib/useInterval'; + +import { SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS } from '../../../../../common/constants'; +import type { ResponseError } from '../../../../../common/types'; +import { GlobalFlyout } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { SystemIndicesFlyout, SystemIndicesFlyoutProps } from './flyout'; + +const FLYOUT_ID = 'migrateSystemIndicesFlyout'; +const { useGlobalFlyout } = GlobalFlyout; + +export type StatusType = 'idle' | 'error' | 'started'; +interface MigrationStatus { + statusType: StatusType; + error?: ResponseError; +} + +export const useMigrateSystemIndices = () => { + const { + services: { api }, + } = useAppContext(); + + const [showFlyout, setShowFlyout] = useState(false); + + const [startMigrationStatus, setStartMigrationStatus] = useState({ + statusType: 'idle', + }); + + const { data, error, isLoading, resendRequest, isInitialRequest } = + api.useLoadSystemIndicesMigrationStatus(); + const isInProgress = data?.migration_status === 'IN_PROGRESS'; + + // We only want to poll for the status while the migration process is in progress. + useInterval(resendRequest, isInProgress ? SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS : null); + + const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = + useGlobalFlyout(); + + const closeFlyout = useCallback(() => { + setShowFlyout(false); + removeContentFromGlobalFlyout(FLYOUT_ID); + }, [removeContentFromGlobalFlyout]); + + useEffect(() => { + if (showFlyout) { + addContentToGlobalFlyout({ + id: FLYOUT_ID, + Component: SystemIndicesFlyout, + props: { + data: data!, + closeFlyout, + }, + flyoutProps: { + onClose: closeFlyout, + }, + }); + } + }, [addContentToGlobalFlyout, data, showFlyout, closeFlyout]); + + const beginSystemIndicesMigration = useCallback(async () => { + const { error: startMigrationError } = await api.migrateSystemIndices(); + + setStartMigrationStatus({ + statusType: startMigrationError ? 'error' : 'started', + error: startMigrationError ?? undefined, + }); + + if (!startMigrationError) { + resendRequest(); + } + }, [api, resendRequest]); + + return { + setShowFlyout, + startMigrationStatus, + beginSystemIndicesMigration, + migrationStatus: { + data, + error, + isLoading, + resendRequest, + isInitialRequest, + }, + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index f900416873b8..1e7961f8ea78 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent, useEffect } from 'react'; +import React, { FunctionComponent, useEffect, useState } from 'react'; import { EuiSteps, @@ -18,33 +18,53 @@ import { EuiPageContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAppContext } from '../../app_context'; -import { getReviewLogsStep } from './review_logs_step'; -import { getFixDeprecationLogsStep } from './fix_deprecation_logs_step'; +import { uiMetricService, UIM_OVERVIEW_PAGE_LOAD } from '../../lib/ui_metric'; +import { getBackupStep } from './backup_step'; +import { getFixIssuesStep } from './fix_issues_step'; +import { getFixLogsStep } from './fix_logs_step'; import { getUpgradeStep } from './upgrade_step'; +import { getMigrateSystemIndicesStep } from './migrate_system_indices'; + +type OverviewStep = 'backup' | 'migrate_system_indices' | 'fix_issues' | 'fix_logs'; export const Overview: FunctionComponent = () => { - const { kibanaVersionInfo, breadcrumbs, docLinks, api } = useAppContext(); - const { nextMajor } = kibanaVersionInfo; + const { + services: { + breadcrumbs, + core: { docLinks }, + }, + plugins: { cloud }, + } = useAppContext(); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - overview: true, - }); - } - - sendTelemetryData(); - }, [api]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_OVERVIEW_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('overview'); }, [breadcrumbs]); + const [completedStepsMap, setCompletedStepsMap] = useState({ + backup: false, + migrate_system_indices: false, + fix_issues: false, + fix_logs: false, + }); + + const isStepComplete = (step: OverviewStep) => completedStepsMap[step]; + const setCompletedStep = (step: OverviewStep, isCompleted: boolean) => { + setCompletedStepsMap({ + ...completedStepsMap, + [step]: isCompleted, + }); + }; + return ( - + { defaultMessage: 'Upgrade Assistant', })} description={i18n.translate('xpack.upgradeAssistant.overview.pageDescription', { - defaultMessage: 'Get ready for the next version of the Elastic Stack!', + defaultMessage: 'Get ready for the next version of Elastic!', })} rightSideItems={[ { @@ -83,9 +102,24 @@ export const Overview: FunctionComponent = () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss deleted file mode 100644 index 7eea518d5698..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'stats_panel'; -@import 'no_deprecations/no_deprecations'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss deleted file mode 100644 index b32f3eb9ddbd..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Used by both es_stats and kibana_stats panel for having the EuiPopover Icon -// for errors shown next to the title without having to resort to wrapping everything -// with EuiFlexGroups. -.upgWarningIcon { - margin-left: $euiSizeS; -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx deleted file mode 100644 index ef0b3f438da0..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx +++ /dev/null @@ -1,146 +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, { FunctionComponent } from 'react'; -import { useHistory } from 'react-router-dom'; - -import { - EuiStat, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiCard, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { getDeprecationsUpperLimit } from '../../../../lib/utils'; -import { useAppContext } from '../../../../app_context'; -import { EsStatsErrors } from './es_stats_error'; -import { NoDeprecations } from '../no_deprecations'; - -const i18nTexts = { - statsTitle: i18n.translate('xpack.upgradeAssistant.esDeprecationStats.statsTitle', { - defaultMessage: 'Elasticsearch', - }), - warningDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTitle', - { - defaultMessage: 'Warning', - } - ), - criticalDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsTitle', - { - defaultMessage: 'Critical', - } - ), - loadingText: i18n.translate('xpack.upgradeAssistant.esDeprecationStats.loadingText', { - defaultMessage: 'Loading Elasticsearch deprecation stats…', - }), - getCriticalDeprecationsMessage: (criticalDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel', { - defaultMessage: 'This cluster has {criticalDeprecations} critical deprecations', - values: { - criticalDeprecations, - }, - }), - getWarningDeprecationMessage: (warningDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTooltip', { - defaultMessage: - 'This cluster has {warningDeprecations} non-critical {warningDeprecations, plural, one {deprecation} other {deprecations}}', - values: { - warningDeprecations, - }, - }), -}; - -export const ESDeprecationStats: FunctionComponent = () => { - const history = useHistory(); - const { api } = useAppContext(); - - const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations(); - - const warningDeprecations = - esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) || []; - const criticalDeprecations = - esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical) || []; - - const hasWarnings = warningDeprecations.length > 0; - const hasCritical = criticalDeprecations.length > 0; - const hasNoDeprecations = !isLoading && !error && !hasWarnings && !hasCritical; - const shouldRenderStat = (forSection: boolean) => error || isLoading || forSection; - - return ( - - {i18nTexts.statsTitle} - {error && } - - } - {...(!hasNoDeprecations && reactRouterNavigate(history, '/es_deprecations'))} - > - - - {hasNoDeprecations && ( - - - - )} - - {shouldRenderStat(hasCritical) && ( - - - {error === null && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getCriticalDeprecationsMessage(criticalDeprecations.length)} -

-
- )} -
-
- )} - - {shouldRenderStat(hasWarnings) && ( - - - {!error && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getWarningDeprecationMessage(warningDeprecations.length)} -

-
- )} -
-
- )} -
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx deleted file mode 100644 index c717a8a2e12e..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx +++ /dev/null @@ -1,79 +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 from 'react'; - -import { EuiIconTip } from '@elastic/eui'; -import { ResponseError } from '../../../../lib/api'; -import { getEsDeprecationError } from '../../../../lib/get_es_deprecation_error'; - -interface Props { - error: ResponseError; -} - -export const EsStatsErrors: React.FunctionComponent = ({ error }) => { - let iconContent: React.ReactNode; - - const { code: errorType, message } = getEsDeprecationError(error); - - switch (errorType) { - case 'unauthorized_error': - iconContent = ( - - ); - break; - case 'partially_upgraded_error': - iconContent = ( - - ); - break; - case 'upgraded_error': - iconContent = ( - - ); - break; - case 'request_error': - default: - iconContent = ( - - ); - } - - return {iconContent}; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx deleted file mode 100644 index d7b820aa4a48..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx +++ /dev/null @@ -1,188 +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, { FunctionComponent, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; - -import { - EuiCard, - EuiStat, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { getDeprecationsUpperLimit } from '../../../../lib/utils'; -import { useAppContext } from '../../../../app_context'; -import { NoDeprecations } from '../no_deprecations'; - -const i18nTexts = { - statsTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle', { - defaultMessage: 'Kibana', - }), - warningDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationStats.warningDeprecationsTitle', - { - defaultMessage: 'Warning', - } - ), - criticalDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle', - { - defaultMessage: 'Critical', - } - ), - loadingError: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage', - { - defaultMessage: 'An error occurred while retrieving Kibana deprecations.', - } - ), - loadingText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.loadingText', { - defaultMessage: 'Loading Kibana deprecation stats…', - }), - getCriticalDeprecationsMessage: (criticalDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsLabel', { - defaultMessage: - 'Kibana has {criticalDeprecations} critical {criticalDeprecations, plural, one {deprecation} other {deprecations}}', - values: { - criticalDeprecations, - }, - }), - getWarningDeprecationsMessage: (warningDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.getWarningDeprecationsMessage', { - defaultMessage: - 'Kibana has {warningDeprecations} warning {warningDeprecations, plural, one {deprecation} other {deprecations}}', - values: { - warningDeprecations, - }, - }), -}; - -export const KibanaDeprecationStats: FunctionComponent = () => { - const history = useHistory(); - const { deprecations } = useAppContext(); - - const [kibanaDeprecations, setKibanaDeprecations] = useState< - DomainDeprecationDetails[] | undefined - >(undefined); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - async function getAllDeprecations() { - setIsLoading(true); - - try { - const response = await deprecations.getAllDeprecations(); - setKibanaDeprecations(response); - } catch (e) { - setError(e); - } - - setIsLoading(false); - } - - getAllDeprecations(); - }, [deprecations]); - - const warningDeprecationsCount = - kibanaDeprecations?.filter((deprecation) => deprecation.level === 'warning')?.length ?? 0; - const criticalDeprecationsCount = - kibanaDeprecations?.filter((deprecation) => deprecation.level === 'critical')?.length ?? 0; - - const hasCritical = criticalDeprecationsCount > 0; - const hasWarnings = warningDeprecationsCount > 0; - const hasNoDeprecations = !isLoading && !error && !hasWarnings && !hasCritical; - const shouldRenderStat = (forSection: boolean) => error || isLoading || forSection; - - return ( - - {i18nTexts.statsTitle} - {error && ( - - )} - - } - {...(!hasNoDeprecations && reactRouterNavigate(history, '/kibana_deprecations'))} - > - - - {hasNoDeprecations && ( - - - - )} - - {shouldRenderStat(hasCritical) && ( - - - {error === undefined && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getCriticalDeprecationsMessage(criticalDeprecationsCount)} -

-
- )} -
-
- )} - - {shouldRenderStat(hasWarnings) && ( - - - {!error && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getWarningDeprecationsMessage(warningDeprecationsCount)} -

-
- )} -
-
- )} -
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss deleted file mode 100644 index 0697efbd6ee3..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss +++ /dev/null @@ -1,3 +0,0 @@ -.upgRenderSuccessMessage { - margin-top: $euiSizeL; -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx deleted file mode 100644 index 06fea677aa0a..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx +++ /dev/null @@ -1,35 +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, { FunctionComponent } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const i18nTexts = { - noDeprecationsText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecationStats.noDeprecationsText', - { - defaultMessage: 'No warnings. Good to go!', - } - ), -}; - -export const NoDeprecations: FunctionComponent = () => { - return ( - - - - - - - {i18nTexts.noDeprecationsText} - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx deleted file mode 100644 index 4ebde8b5f847..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx +++ /dev/null @@ -1,53 +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 from 'react'; - -import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { ESDeprecationStats } from './es_stats'; -import { KibanaDeprecationStats } from './kibana_stats'; - -const i18nTexts = { - reviewStepTitle: i18n.translate('xpack.upgradeAssistant.overview.reviewStepTitle', { - defaultMessage: 'Review deprecated settings and resolve issues', - }), -}; - -export const getReviewLogsStep = ({ nextMajor }: { nextMajor: number }): EuiStepProps => { - return { - title: i18nTexts.reviewStepTitle, - status: 'incomplete', - children: ( - <> - -

- -

-
- - - - - - - - - - - - - - ), - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx index d66a408cfce7..b3a3179ed907 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx @@ -17,24 +17,21 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import type { DocLinksStart } from 'src/core/public'; -import { useKibana } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; const i18nTexts = { - upgradeStepTitle: (nextMajor: number) => - i18n.translate('xpack.upgradeAssistant.overview.upgradeStepTitle', { - defaultMessage: 'Install {nextMajor}.0', - values: { nextMajor }, - }), + upgradeStepTitle: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepTitle', { + defaultMessage: 'Upgrade to Elastic 8.x', + }), upgradeStepDescription: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepDescription', { defaultMessage: - "Once you've resolved all critical issues and verified that your applications are ready, you can upgrade the Elastic Stack.", + 'Once you’ve resolved all critical issues and verified that your applications are ready, you can upgrade to Elastic 8.x. Be sure to back up your data again before upgrading.', }), upgradeStepDescriptionForCloud: i18n.translate( 'xpack.upgradeAssistant.overview.upgradeStepDescriptionForCloud', { defaultMessage: - "Once you've resolved all critical issues and verified that your applications are ready, you can upgrade the Elastic Stack. Upgrade your deployment on Elastic Cloud.", + "Once you've resolved all critical issues and verified that your applications are ready, you can upgrade to Elastic 8.x. Be sure to back up your data again before upgrading. Upgrade your deployment on Elastic Cloud.", } ), upgradeStepLink: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepLink', { @@ -48,20 +45,23 @@ const i18nTexts = { }), }; -const UpgradeStep = ({ docLinks }: { docLinks: DocLinksStart }) => { - const { cloud } = useKibana().services; - +const UpgradeStep = () => { + const { + plugins: { cloud }, + services: { + core: { docLinks }, + }, + } = useAppContext(); const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled); - const cloudDeploymentUrl: string = `${cloud?.baseUrl ?? ''}/deployments/${cloud?.cloudId ?? ''}`; - let callToAction; if (isCloudEnabled) { + const upgradeOnCloudUrl = cloud!.deploymentUrl + '?show_upgrade=true'; callToAction = ( { { } else { callToAction = ( { ); }; -interface Props { - docLinks: DocLinksStart; - nextMajor: number; -} - -export const getUpgradeStep = ({ docLinks, nextMajor }: Props): EuiStepProps => { +export const getUpgradeStep = (): EuiStepProps => { return { - title: i18nTexts.upgradeStepTitle(nextMajor), + title: i18nTexts.upgradeStepTitle, status: 'incomplete', - children: , + 'data-test-subj': 'upgradeStep', + children: , }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.tsx new file mode 100644 index 000000000000..c0b8f0eb2430 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.tsx @@ -0,0 +1,51 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; + +const i18nTexts = { + criticalBadgeLabel: i18n.translate('xpack.upgradeAssistant.deprecationBadge.criticalBadgeLabel', { + defaultMessage: 'Critical', + }), + resolvedBadgeLabel: i18n.translate('xpack.upgradeAssistant.deprecationBadge.resolvedBadgeLabel', { + defaultMessage: 'Resolved', + }), + warningBadgeLabel: i18n.translate('xpack.upgradeAssistant.deprecationBadge.warningBadgeLabel', { + defaultMessage: 'Warning', + }), +}; + +interface Props { + isCritical: boolean; + isResolved?: boolean; +} + +export const DeprecationBadge: FunctionComponent = ({ isCritical, isResolved }) => { + if (isResolved) { + return ( + + {i18nTexts.resolvedBadgeLabel} + + ); + } + + if (isCritical) { + return ( + + {i18nTexts.criticalBadgeLabel} + + ); + } + + return ( + + {i18nTexts.warningBadgeLabel} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx new file mode 100644 index 000000000000..32d214f0d80f --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx @@ -0,0 +1,71 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { LevelInfoTip } from './level_info_tip'; + +const i18nTexts = { + getCriticalStatusLabel: (count: number) => + i18n.translate('xpack.upgradeAssistant.deprecationCount.criticalStatusLabel', { + defaultMessage: 'Critical: {count}', + values: { + count, + }, + }), + getWarningStatusLabel: (count: number) => + i18n.translate('xpack.upgradeAssistant.deprecationCount.warningStatusLabel', { + defaultMessage: 'Warning: {count}', + values: { + count, + }, + }), +}; + +interface Props { + totalCriticalDeprecations: number; + totalWarningDeprecations: number; +} + +export const DeprecationCount: FunctionComponent = ({ + totalCriticalDeprecations, + totalWarningDeprecations, +}) => { + return ( + + + + + + {i18nTexts.getCriticalStatusLabel(totalCriticalDeprecations)} + + + + + + + + + + + + + + {i18nTexts.getWarningStatusLabel(totalWarningDeprecations)} + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx new file mode 100644 index 000000000000..da8c83597f7e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx @@ -0,0 +1,24 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + documentationUrl?: string; +} + +export const DeprecationFlyoutLearnMoreLink = ({ documentationUrl }: Props) => { + return ( + + {i18n.translate('xpack.upgradeAssistant.deprecationFlyout.learnMoreLinkLabel', { + defaultMessage: 'Learn more', + })} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx deleted file mode 100644 index 709ef7224870..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx +++ /dev/null @@ -1,41 +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, { FunctionComponent } from 'react'; - -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const DeprecationCountSummary: FunctionComponent<{ - allDeprecationsCount: number; - filteredDeprecationsCount: number; -}> = ({ filteredDeprecationsCount, allDeprecationsCount }) => ( - - {allDeprecationsCount > 0 ? ( - - ) : ( - - )} - {filteredDeprecationsCount !== allDeprecationsCount && ( - <> - {'. '} - - - )} - -); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx deleted file mode 100644 index 6cb5ae3675c4..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx +++ /dev/null @@ -1,69 +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, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { DeprecationCountSummary } from './count_summary'; - -const i18nTexts = { - expandAllButton: i18n.translate( - 'xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel', - { - defaultMessage: 'Expand all', - } - ), - collapseAllButton: i18n.translate( - 'xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel', - { - defaultMessage: 'Collapse all', - } - ), -}; - -export const DeprecationListBar: FunctionComponent<{ - allDeprecationsCount: number; - filteredDeprecationsCount: number; - setExpandAll: (shouldExpandAll: boolean) => void; -}> = ({ allDeprecationsCount, filteredDeprecationsCount, setExpandAll }) => { - return ( - - - - - - - - - setExpandAll(true)} - data-test-subj="expandAll" - > - {i18nTexts.expandAllButton} - - - - setExpandAll(false)} - data-test-subj="collapseAll" - > - {i18nTexts.collapseAllButton} - - - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts deleted file mode 100644 index cbc04fd86bfb..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { DeprecationListBar } from './deprecation_list_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx deleted file mode 100644 index ae2c0ba1c487..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx +++ /dev/null @@ -1,24 +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, { FunctionComponent } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui'; - -export const DeprecationPagination: FunctionComponent<{ - pageCount: number; - activePage: number; - setPage: (page: number) => void; -}> = ({ pageCount, activePage, setPage }) => { - return ( - - - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx new file mode 100644 index 000000000000..01cf950abbc3 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx @@ -0,0 +1,42 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DeprecationSource } from '../../../../common/types'; + +interface Props { + deprecationSource: DeprecationSource; + message?: string; +} + +export const DeprecationsPageLoadingError: FunctionComponent = ({ + deprecationSource, + message, +}) => ( + + + {i18n.translate('xpack.upgradeAssistant.deprecationsPageLoadingError.title', { + defaultMessage: 'Could not retrieve {deprecationSource} deprecation issues', + values: { deprecationSource }, + })} + + } + body={message} + /> + +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx deleted file mode 100644 index 9bf35668ac88..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx +++ /dev/null @@ -1,88 +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 { countBy } from 'lodash'; -import React, { FunctionComponent } from 'react'; - -import { EuiBadge, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { DeprecationInfo } from '../../../../common/types'; -import { COLOR_MAP, REVERSE_LEVEL_MAP } from '../constants'; - -const LocalizedLevels: { [level: string]: string } = { - warning: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.warningLabel', { - defaultMessage: 'Warning', - }), - critical: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel', { - defaultMessage: 'Critical', - }), -}; - -export const LocalizedActions: { [level: string]: string } = { - warning: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip', { - defaultMessage: 'Resolving this issue before upgrading is advised, but not required.', - }), - critical: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip', { - defaultMessage: 'Resolve this issue before upgrading.', - }), -}; - -interface DeprecationHealthProps { - deprecationLevels: number[]; - single?: boolean; -} - -const SingleHealth: FunctionComponent<{ level: DeprecationInfo['level']; label: string }> = ({ - level, - label, -}) => ( - - - {label} - -   - -); - -/** - * Displays a summary health for a list of deprecations that shows the number and level of severity - * deprecations in the list. - */ -export const DeprecationHealth: FunctionComponent = ({ - deprecationLevels, - single = false, -}) => { - if (deprecationLevels.length === 0) { - return ; - } - - if (single) { - const highest = Math.max(...deprecationLevels); - const highestLevel = REVERSE_LEVEL_MAP[highest]; - - return ; - } - - const countByLevel = countBy(deprecationLevels); - - return ( - - {Object.keys(countByLevel) - .map((k) => parseInt(k, 10)) - .sort() - .map((level) => [level, REVERSE_LEVEL_MAP[level]]) - .map(([numLevel, stringLevel]) => ( - - ))} - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts index c79d8247a93f..34496e1e8eb5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts @@ -6,7 +6,8 @@ */ export { NoDeprecationsPrompt } from './no_deprecations'; -export { DeprecationHealth } from './health'; -export { SearchBar } from './search_bar'; -export { DeprecationPagination } from './deprecation_pagination'; -export { DeprecationListBar } from './deprecation_list_bar'; +export { DeprecationCount } from './deprecation_count'; +export { DeprecationBadge } from './deprecation_badge'; +export { DeprecationsPageLoadingError } from './deprecations_page_loading_error'; +export { DeprecationFlyoutLearnMoreLink } from './deprecation_flyout_learn_more_link'; +export { LevelInfoTip } from './level_info_tip'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx new file mode 100644 index 000000000000..d3600a7290b4 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx @@ -0,0 +1,27 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIconTip } from '@elastic/eui'; + +const i18nTexts = { + critical: i18n.translate('xpack.upgradeAssistant.levelInfoTip.criticalLabel', { + defaultMessage: 'Critical issues must be resolved before you upgrade', + }), + warning: i18n.translate('xpack.upgradeAssistant.levelInfoTip.warningLabel', { + defaultMessage: 'Warning issues can be ignored at your discretion', + }), +}; + +interface Props { + level: 'critical' | 'warning'; +} + +export const LevelInfoTip: FunctionComponent = ({ level }) => { + return ; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap deleted file mode 100644 index 5a8619e1e687..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GroupByFilter renders 1`] = ` - - - - By issue - - - By index - - - -`; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap deleted file mode 100644 index 551e212f23dd..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DeprecationLevelFilter renders 1`] = ` - - - - Critical - - - -`; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx deleted file mode 100644 index fa863e4935c0..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx +++ /dev/null @@ -1,31 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { GroupByOption } from '../../types'; -import { GroupByFilter } from './group_by_filter'; - -const defaultProps = { - availableGroupByOptions: [GroupByOption.message, GroupByOption.index], - currentGroupBy: GroupByOption.message, - onGroupByChange: jest.fn(), -}; - -describe('GroupByFilter', () => { - test('renders', () => { - expect(shallow()).toMatchSnapshot(); - }); - - test('clicking button calls onGroupByChange', () => { - const wrapper = mount(); - wrapper.find('button.euiFilterButton-hasActiveFilters').simulate('click'); - expect(defaultProps.onGroupByChange).toHaveBeenCalledTimes(1); - expect(defaultProps.onGroupByChange.mock.calls[0][0]).toEqual(GroupByOption.message); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx deleted file mode 100644 index c37ae47793b9..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx +++ /dev/null @@ -1,54 +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 from 'react'; - -import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { GroupByOption } from '../../types'; - -const LocalizedOptions: { [option: string]: string } = { - message: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel', { - defaultMessage: 'By issue', - }), - index: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel', { - defaultMessage: 'By index', - }), -}; - -interface GroupByFilterProps { - availableGroupByOptions: GroupByOption[]; - currentGroupBy: GroupByOption; - onGroupByChange: (groupBy: GroupByOption) => void; -} - -export const GroupByFilter: React.FunctionComponent = ({ - availableGroupByOptions, - currentGroupBy, - onGroupByChange, -}) => { - if (availableGroupByOptions.length <= 1) { - return null; - } - - return ( - - - {availableGroupByOptions.map((option) => ( - - {LocalizedOptions[option]} - - ))} - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx deleted file mode 100644 index c778e56e8df1..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx +++ /dev/null @@ -1,34 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; -import { LevelFilterOption } from '../../types'; - -import { DeprecationLevelFilter } from './level_filter'; - -const defaultProps = { - levelsCount: { - warning: 4, - critical: 1, - }, - currentFilter: 'all' as LevelFilterOption, - onFilterChange: jest.fn(), -}; - -describe('DeprecationLevelFilter', () => { - test('renders', () => { - expect(shallow()).toMatchSnapshot(); - }); - - test('clicking button calls onFilterChange', () => { - const wrapper = mount(); - wrapper.find('button[data-test-subj="criticalLevelFilter"]').simulate('click'); - expect(defaultProps.onFilterChange).toHaveBeenCalledTimes(1); - expect(defaultProps.onFilterChange.mock.calls[0][0]).toEqual('critical'); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx deleted file mode 100644 index 59bfaa595b0a..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { LevelFilterOption } from '../../types'; - -const LocalizedOptions: { [option: string]: string } = { - critical: i18n.translate( - 'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel', - { defaultMessage: 'Critical' } - ), -}; -interface DeprecationLevelProps { - levelsCount: { - [key: string]: number; - }; - currentFilter: LevelFilterOption; - onFilterChange(level: LevelFilterOption): void; -} - -export const DeprecationLevelFilter: React.FunctionComponent = ({ - levelsCount, - currentFilter, - onFilterChange, -}) => { - return ( - - - { - onFilterChange(currentFilter !== 'critical' ? 'critical' : 'all'); - }} - hasActiveFilters={currentFilter === 'critical'} - numFilters={levelsCount.critical || undefined} - data-test-subj="criticalLevelFilter" - > - {LocalizedOptions.critical} - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx deleted file mode 100644 index 7c805398a6b4..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx +++ /dev/null @@ -1,141 +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, { FunctionComponent, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButton, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiCallOut, - EuiSpacer, -} from '@elastic/eui'; - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { DeprecationInfo } from '../../../../../common/types'; -import { validateRegExpString } from '../../../lib/utils'; -import { GroupByOption, LevelFilterOption } from '../../types'; -import { DeprecationLevelFilter } from './level_filter'; -import { GroupByFilter } from './group_by_filter'; - -interface SearchBarProps { - allDeprecations?: DeprecationInfo[] | DomainDeprecationDetails; - isLoading: boolean; - loadData: () => void; - currentFilter: LevelFilterOption; - onFilterChange: (filter: LevelFilterOption) => void; - onSearchChange: (filter: string) => void; - totalDeprecationsCount: number; - levelToDeprecationCountMap: { - [key: string]: number; - }; - groupByFilterProps?: { - availableGroupByOptions: GroupByOption[]; - currentGroupBy: GroupByOption; - onGroupByChange: (groupBy: GroupByOption) => void; - }; -} - -const i18nTexts = { - searchAriaLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel', - { defaultMessage: 'Filter' } - ), - searchPlaceholderLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel', - { - defaultMessage: 'Filter', - } - ), - reloadButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel', - { - defaultMessage: 'Reload', - } - ), - getInvalidSearchMessage: (searchTermError: string) => - i18n.translate('xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel', { - defaultMessage: 'Filter invalid: {searchTermError}', - values: { searchTermError }, - }), -}; - -export const SearchBar: FunctionComponent = ({ - totalDeprecationsCount, - levelToDeprecationCountMap, - isLoading, - loadData, - currentFilter, - onFilterChange, - onSearchChange, - groupByFilterProps, -}) => { - const [searchTermError, setSearchTermError] = useState(null); - const filterInvalid = Boolean(searchTermError); - return ( - <> - - - - - { - const string = e.target.value; - const errorMessage = validateRegExpString(string); - if (errorMessage) { - // Emit an empty search term to listeners if search term is invalid. - onSearchChange(''); - setSearchTermError(errorMessage); - } else { - onSearchChange(e.target.value); - if (searchTermError) { - setSearchTermError(null); - } - } - }} - /> - - - {/* These two components provide their own EuiFlexItem wrappers */} - - {groupByFilterProps && } - - - - - {i18nTexts.reloadButtonLabel} - - - - - {filterInvalid && ( - <> - - - - - )} - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts index b46bb583244f..637c48cc6140 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ResponseError } from '../lib/api'; +import { ResponseError } from '../../../common/types'; export enum LoadingState { Loading, @@ -13,12 +13,11 @@ export enum LoadingState { Error, } -export type LevelFilterOption = 'all' | 'critical'; - -export enum GroupByOption { - message = 'message', - index = 'index', - node = 'node', +export enum CancelLoadingState { + Requested, + Loading, + Success, + Error, } export type DeprecationTableColumns = @@ -39,3 +38,8 @@ export interface DeprecationLoggingPreviewProps { resendRequest: () => void; toggleLogging: () => void; } + +export interface OverviewStepProps { + isComplete: boolean; + setIsComplete: (isComplete: boolean) => void; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index 78070c571749..8b967d994af9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -6,8 +6,20 @@ */ import { HttpSetup } from 'src/core/public'; -import { ESUpgradeStatus } from '../../../common/types'; -import { API_BASE_PATH } from '../../../common/constants'; + +import { + ESUpgradeStatus, + CloudBackupStatus, + ClusterUpgradeState, + ResponseError, + SystemIndicesMigrationStatus, +} from '../../../common/types'; +import { + API_BASE_PATH, + CLUSTER_UPGRADE_STATUS_POLL_INTERVAL_MS, + DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, + CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS, +} from '../../../common/constants'; import { UseRequestConfig, SendRequestConfig, @@ -16,52 +28,103 @@ import { useRequest as _useRequest, } from '../../shared_imports'; -export interface ResponseError { - statusCode: number; - message: string | Error; - attributes?: Record; -} +type ClusterUpgradeStateListener = (clusterUpgradeState: ClusterUpgradeState) => void; export class ApiService { private client: HttpSetup | undefined; + private clusterUpgradeStateListeners: ClusterUpgradeStateListener[] = []; + + private handleClusterUpgradeError(error: ResponseError | null) { + const isClusterUpgradeError = Boolean(error && error.statusCode === 426); + if (isClusterUpgradeError) { + const clusterUpgradeState = error!.attributes!.allNodesUpgraded + ? 'isUpgradeComplete' + : 'isUpgrading'; + this.clusterUpgradeStateListeners.forEach((listener) => listener(clusterUpgradeState)); + } + } - private useRequest(config: UseRequestConfig) { + private useRequest(config: UseRequestConfig) { if (!this.client) { - throw new Error('API service has not be initialized.'); + throw new Error('API service has not been initialized.'); } - return _useRequest(this.client, config); + const response = _useRequest(this.client, config); + // NOTE: This will cause an infinite render loop in any component that both + // consumes the hook calling this useRequest function and also handles + // cluster upgrade errors. Note that sendRequest doesn't have this problem. + // + // This is due to React's fundamental expectation that hooks be idempotent, + // so it can render a component as many times as necessary and thereby call + // the hook on each render without worrying about that triggering subsequent + // renders. + // + // In this case we call handleClusterUpgradeError every time useRequest is + // called, which is on every render. If handling the cluster upgrade error + // causes a state change in the consuming component, that will trigger a + // render, which will call useRequest again, calling handleClusterUpgradeError, + // causing a state change in the consuming component, and so on. + this.handleClusterUpgradeError(response.error); + return response; } - private sendRequest( + private async sendRequest( config: SendRequestConfig - ): Promise> { + ): Promise> { if (!this.client) { - throw new Error('API service has not be initialized.'); + throw new Error('API service has not been initialized.'); } - return _sendRequest(this.client, config); + const response = await _sendRequest(this.client, config); + this.handleClusterUpgradeError(response.error); + return response; } public setup(httpClient: HttpSetup): void { this.client = httpClient; } - public useLoadEsDeprecations() { - return this.useRequest({ - path: `${API_BASE_PATH}/es_deprecations`, + public onClusterUpgradeStateChange(listener: ClusterUpgradeStateListener) { + this.clusterUpgradeStateListeners.push(listener); + } + + public useLoadClusterUpgradeStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/cluster_upgrade_status`, + method: 'get', + pollIntervalMs: CLUSTER_UPGRADE_STATUS_POLL_INTERVAL_MS, + }); + } + + public useLoadCloudBackupStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/cloud_backup_status`, method: 'get', + pollIntervalMs: CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS, }); } - public async sendPageTelemetryData(telemetryData: { [tabName: string]: boolean }) { + public useLoadSystemIndicesMigrationStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/system_indices_migration`, + method: 'get', + }); + } + + public async migrateSystemIndices() { const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_open`, - method: 'put', - body: JSON.stringify(telemetryData), + path: `${API_BASE_PATH}/system_indices_migration`, + method: 'post', }); return result; } + public useLoadEsDeprecations() { + return this.useRequest({ + path: `${API_BASE_PATH}/es_deprecations`, + method: 'get', + }); + } + public useLoadDeprecationLogging() { return this.useRequest<{ isDeprecationLogIndexingEnabled: boolean; @@ -73,44 +136,54 @@ export class ApiService { } public async updateDeprecationLogging(loggingData: { isEnabled: boolean }) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/deprecation_logging`, method: 'put', body: JSON.stringify(loggingData), }); + } - return result; + public getDeprecationLogsCount(from: string) { + return this.useRequest<{ + count: number; + }>({ + path: `${API_BASE_PATH}/deprecation_logging/count`, + method: 'get', + query: { from }, + pollIntervalMs: DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, + }); + } + + public deleteDeprecationLogsCache() { + return this.sendRequest({ + path: `${API_BASE_PATH}/deprecation_logging/cache`, + method: 'delete', + }); } public async updateIndexSettings(indexName: string, settings: string[]) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/${indexName}/index_settings`, method: 'post', body: { settings: JSON.stringify(settings), }, }); - - return result; } public async upgradeMlSnapshot(body: { jobId: string; snapshotId: string }) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/ml_snapshots`, method: 'post', body, }); - - return result; } public async deleteMlSnapshot({ jobId, snapshotId }: { jobId: string; snapshotId: string }) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/ml_snapshots/${jobId}/${snapshotId}`, method: 'delete', }); - - return result; } public async getMlSnapshotUpgradeStatus({ @@ -126,14 +199,13 @@ export class ApiService { }); } - public async sendReindexTelemetryData(telemetryData: { [key: string]: boolean }) { - const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_reindex`, - method: 'put', - body: JSON.stringify(telemetryData), + public useLoadMlUpgradeMode() { + return this.useRequest<{ + mlUpgradeModeEnabled: boolean; + }>({ + path: `${API_BASE_PATH}/ml_upgrade_mode`, + method: 'get', }); - - return result; } public async getReindexStatus(indexName: string) { diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts index f36dc2096ddc..3e30ffd06db1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts @@ -16,12 +16,12 @@ const i18nTexts = { defaultMessage: 'Upgrade Assistant', }), esDeprecations: i18n.translate('xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel', { - defaultMessage: 'Elasticsearch deprecation warnings', + defaultMessage: 'Elasticsearch deprecation issues', }), kibanaDeprecations: i18n.translate( 'xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel', { - defaultMessage: 'Kibana deprecations', + defaultMessage: 'Kibana deprecation issues', } ), }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts index 85cfd2a3fd16..9581ce872a28 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts @@ -6,13 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { ResponseError } from './api'; +import { ResponseError } from '../../../common/types'; const i18nTexts = { permissionsError: i18n.translate( 'xpack.upgradeAssistant.esDeprecationErrors.permissionsErrorMessage', { - defaultMessage: 'You are not authorized to view Elasticsearch deprecations.', + defaultMessage: 'You are not authorized to view Elasticsearch deprecation issues.', } ), partiallyUpgradedWarning: i18n.translate( @@ -29,7 +29,7 @@ const i18nTexts = { } ), loadingError: i18n.translate('xpack.upgradeAssistant.esDeprecationErrors.loadingErrorMessage', { - defaultMessage: 'Could not retrieve Elasticsearch deprecations.', + defaultMessage: 'Could not retrieve Elasticsearch deprecation issues.', }), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts new file mode 100644 index 000000000000..59c3adaed95d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts @@ -0,0 +1,30 @@ +/* + * 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 moment from 'moment-timezone'; + +import { Storage } from '../../shared_imports'; + +const SETTING_ID = 'kibana.upgradeAssistant.lastCheckpoint'; +const localStorage = new Storage(window.localStorage); + +export const loadLogsCheckpoint = () => { + const storedValue = moment(localStorage.get(SETTING_ID)); + + if (storedValue.isValid()) { + return storedValue.toISOString(); + } + + const now = moment().toISOString(); + localStorage.set(SETTING_ID, now); + + return now; +}; + +export const saveLogsCheckpoint = (value: string) => { + localStorage.set(SETTING_ID, value); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts new file mode 100644 index 000000000000..394f046a8baf --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts @@ -0,0 +1,49 @@ +/* + * 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 { UiCounterMetricType } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export const UIM_APP_NAME = 'upgrade_assistant'; +export const UIM_ES_DEPRECATIONS_PAGE_LOAD = 'es_deprecations_page_load'; +export const UIM_KIBANA_DEPRECATIONS_PAGE_LOAD = 'kibana_deprecations_page_load'; +export const UIM_OVERVIEW_PAGE_LOAD = 'overview_page_load'; +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_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click'; +export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click'; +export const UIM_RESET_LOGS_COUNTER_CLICK = 'reset_logs_counter_click'; +export const UIM_OBSERVABILITY_CLICK = 'observability_click'; +export const UIM_DISCOVER_CLICK = 'discover_click'; +export const UIM_ML_SNAPSHOT_UPGRADE_CLICK = 'ml_snapshot_upgrade_click'; +export const UIM_ML_SNAPSHOT_DELETE_CLICK = 'ml_snapshot_delete_click'; +export const UIM_INDEX_SETTINGS_DELETE_CLICK = 'index_settings_delete_click'; +export const UIM_KIBANA_QUICK_RESOLVE_CLICK = 'kibana_quick_resolve_click'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(metricType: UiCounterMetricType, eventName: string | string[]) { + if (!this.usageCollection) { + // Usage collection might be disabled in Kibana config. + return; + } + return this.usageCollection.reportUiCounter(UIM_APP_NAME, metricType, eventName); + } + + public trackUiMetric(metricType: UiCounterMetricType, eventName: string | string[]) { + return this.track(metricType, eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts index 83fc9cabbbec..37392c832ecf 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts @@ -6,7 +6,8 @@ */ import { DEPRECATION_WARNING_UPPER_LIMIT } from '../../../common/constants'; -import { validateRegExpString, getDeprecationsUpperLimit } from './utils'; +import { getDeprecationsUpperLimit, getReindexProgressLabel, validateRegExpString } from './utils'; +import { ReindexStep } from '../../../common/types'; describe('validRegExpString', () => { it('correctly returns false for invalid strings', () => { @@ -35,3 +36,33 @@ describe('getDeprecationsUpperLimit', () => { ); }); }); + +describe('getReindexProgressLabel', () => { + it('returns 0% when the reindex task has just been created', () => { + expect(getReindexProgressLabel(null, ReindexStep.created)).toBe('0%'); + }); + + it('returns 5% when the index has been made read-only', () => { + expect(getReindexProgressLabel(null, ReindexStep.readonly)).toBe('5%'); + }); + + it('returns 10% when the reindexing documents has started, but the progress is null', () => { + expect(getReindexProgressLabel(null, ReindexStep.reindexStarted)).toBe('10%'); + }); + + it('returns 10% when the reindexing documents has started, but the progress is 0', () => { + expect(getReindexProgressLabel(0, ReindexStep.reindexStarted)).toBe('10%'); + }); + + it('returns 53% when the reindexing documents progress is 0.5', () => { + expect(getReindexProgressLabel(0.5, ReindexStep.reindexStarted)).toBe('53%'); + }); + + it('returns 95% when the reindexing documents progress is 1', () => { + expect(getReindexProgressLabel(1, ReindexStep.reindexStarted)).toBe('95%'); + }); + + it('returns 100% when alias has been switched', () => { + expect(getReindexProgressLabel(null, ReindexStep.aliasCreated)).toBe('100%'); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts index b90038e1166a..bdbc0949e368 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts @@ -9,6 +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 } from '../../../common/types'; export const validateRegExpString = (s: string) => pipe( @@ -34,3 +35,50 @@ export const getDeprecationsUpperLimit = (count: number) => { return count.toString(); }; + +/* + * Reindexing task consists of 4 steps: making the index read-only, creating a new index, + * reindexing documents into the new index and switching alias from the old to the new index. + * Steps 1, 2 and 4 each contribute 5% to the overall progress. + * Step 3 (reindexing documents) can take a long time for large indices and its progress is calculated + * between 10% and 95% of the overall progress depending on its completeness percentage. + */ +export const getReindexProgressLabel = ( + reindexTaskPercComplete: number | null, + lastCompletedStep: ReindexStep | undefined +): string => { + let percentsComplete = 0; + switch (lastCompletedStep) { + case ReindexStep.created: + // the reindex task has just started, 0% progress + percentsComplete = 0; + break; + case ReindexStep.readonly: { + // step 1 completed, 5% progress + percentsComplete = 5; + break; + } + case ReindexStep.newIndexCreated: { + // step 2 completed, 10% progress + percentsComplete = 10; + break; + } + case ReindexStep.reindexStarted: { + // step 3 started, 10-95% progress depending on progress of reindexing documents in ES + percentsComplete = + reindexTaskPercComplete !== null ? 10 + Math.round(reindexTaskPercComplete * 85) : 10; + break; + } + case ReindexStep.reindexCompleted: { + // step 3 completed, only step 4 remaining, 95% progress + percentsComplete = 95; + break; + } + case ReindexStep.aliasCreated: { + // step 4 completed, 100% progress + percentsComplete = 100; + break; + } + } + return `${percentsComplete}%`; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts deleted file mode 100644 index 7d6d071fcf95..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts +++ /dev/null @@ -1,48 +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 { CoreSetup } from 'src/core/public'; -import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; -import { renderApp } from './render_app'; -import { KibanaVersionContext } from './app_context'; -import { apiService } from './lib/api'; -import { breadcrumbService } from './lib/breadcrumbs'; -import { AppServicesContext } from '../types'; - -export async function mountManagementSection( - coreSetup: CoreSetup, - params: ManagementAppMountParams, - kibanaVersionInfo: KibanaVersionContext, - readonly: boolean, - services: AppServicesContext -) { - const [{ i18n, docLinks, notifications, application, deprecations }] = - await coreSetup.getStartServices(); - - const { element, history, setBreadcrumbs } = params; - const { http } = coreSetup; - - apiService.setup(http); - breadcrumbService.setup(setBreadcrumbs); - - return renderApp({ - element, - http, - i18n, - docLinks, - kibanaVersionInfo, - notifications, - isReadOnlyMode: readonly, - history, - api: apiService, - breadcrumbs: breadcrumbService, - getUrlForApp: application.getUrlForApp, - deprecations, - application, - services, - }); -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx new file mode 100644 index 000000000000..6ab764ddcba6 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import type { ManagementAppMountParams } from 'src/plugins/management/public'; +import { RootComponent } from './app'; +import { AppDependencies } from '../types'; + +import { apiService } from './lib/api'; +import { breadcrumbService } from './lib/breadcrumbs'; + +export function mountManagementSection( + params: ManagementAppMountParams, + dependencies: AppDependencies +) { + const { element, setBreadcrumbs } = params; + + apiService.setup(dependencies.services.core.http); + breadcrumbService.setup(setBreadcrumbs); + + render(, element); + + return () => { + unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx b/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx deleted file mode 100644 index 248e6961a74e..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx +++ /dev/null @@ -1,22 +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 from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { AppDependencies, RootComponent } from './app'; - -interface BootDependencies extends AppDependencies { - element: HTMLElement; -} - -export const renderApp = (deps: BootDependencies) => { - const { element, ...appDependencies } = deps; - render(, element); - return () => { - unmountComponentAtNode(element); - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/index.scss b/x-pack/plugins/upgrade_assistant/public/index.scss deleted file mode 100644 index 9bd47b647337..000000000000 --- a/x-pack/plugins/upgrade_assistant/public/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './application/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/index.ts b/x-pack/plugins/upgrade_assistant/public/index.ts index a4091bcb3e1a..e338b9c044f6 100644 --- a/x-pack/plugins/upgrade_assistant/public/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import './index.scss'; import { PluginInitializerContext } from 'src/core/public'; import { UpgradeAssistantUIPlugin } from './plugin'; diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 32e825fbdc20..2b0ad7241b3a 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -9,19 +9,20 @@ import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; -import { - SetupDependencies, - StartDependencies, - AppServicesContext, - ClientConfigType, -} from './types'; +import { apiService } from './application/lib/api'; +import { breadcrumbService } from './application/lib/breadcrumbs'; +import { uiMetricService } from './application/lib/ui_metric'; +import { SetupDependencies, StartDependencies, AppDependencies, ClientConfigType } from './types'; export class UpgradeAssistantUIPlugin implements Plugin { constructor(private ctx: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, { management, cloud }: SetupDependencies) { + setup( + coreSetup: CoreSetup, + { management, cloud, share, usageCollection }: SetupDependencies + ) { const { readonly, ui: { enabled: isUpgradeAssistantUiEnabled }, @@ -38,17 +39,19 @@ export class UpgradeAssistantUIPlugin }; const pluginName = i18n.translate('xpack.upgradeAssistant.appTitle', { - defaultMessage: '{version} Upgrade Assistant', - values: { version: `${kibanaVersionInfo.nextMajor}.0` }, + defaultMessage: 'Upgrade Assistant', }); + if (usageCollection) { + uiMetricService.setup(usageCollection); + } + appRegistrar.registerApp({ id: 'upgrade_assistant', title: pluginName, order: 1, async mount(params) { - const [coreStart, { discover, data }] = await coreSetup.getStartServices(); - const services: AppServicesContext = { discover, data, cloud }; + const [coreStart, { data, ...plugins }] = await coreSetup.getStartServices(); const { chrome: { docTitle }, @@ -56,14 +59,28 @@ export class UpgradeAssistantUIPlugin docTitle.change(pluginName); - const { mountManagementSection } = await import('./application/mount_management_section'); - const unmountAppCallback = await mountManagementSection( - coreSetup, - params, + const appDependencies: AppDependencies = { kibanaVersionInfo, - readonly, - services - ); + isReadOnlyMode: readonly, + plugins: { + cloud, + share, + // Infra plugin doesnt export anything as a public interface. So the only + // way we have at this stage for checking if the plugin is available or not + // is by checking if the startServices has the `infra` key. + infra: plugins.hasOwnProperty('infra') ? {} : undefined, + }, + services: { + core: coreStart, + data, + history: params.history, + api: apiService, + breadcrumbs: breadcrumbService, + }, + }; + + const { mountManagementSection } = await import('./application/mount_management_section'); + const unmountAppCallback = mountManagementSection(params, appDependencies); return () => { docTitle.reset(); diff --git a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts index 06816daac428..c6c00f34bfad 100644 --- a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts @@ -5,23 +5,31 @@ * 2.0. */ -import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; -import { AppServicesContext } from './types'; - export type { SendRequestConfig, SendRequestResponse, UseRequestConfig, + Privileges, + MissingPrivileges, + Authorization, } from '../../../../src/plugins/es_ui_shared/public/'; export { sendRequest, useRequest, SectionLoading, GlobalFlyout, + WithPrivileges, + AuthorizationProvider, + AuthorizationContext, } from '../../../../src/plugins/es_ui_shared/public/'; -export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +export { Storage } from '../../../../src/plugins/kibana_utils/public'; + +export { + KibanaContextProvider, + reactRouterNavigate, +} from '../../../../src/plugins/kibana_react/public'; export type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -export const useKibana = () => _useKibana(); +export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; diff --git a/x-pack/plugins/upgrade_assistant/public/types.ts b/x-pack/plugins/upgrade_assistant/public/types.ts index cbeaf22bb095..ace009d9c74a 100644 --- a/x-pack/plugins/upgrade_assistant/public/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/types.ts @@ -5,25 +5,32 @@ * 2.0. */ -import { DiscoverStart } from 'src/plugins/discover/public'; +import { ScopedHistory } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { SharePluginSetup } from 'src/plugins/share/public'; +import { CoreStart } from 'src/core/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; +import { BreadcrumbService } from './application/lib/breadcrumbs'; +import { ApiService } from './application/lib/api'; -export interface AppServicesContext { - cloud?: CloudSetup; - discover: DiscoverStart; - data: DataPublicPluginStart; +export interface KibanaVersionContext { + currentMajor: number; + prevMajor: number; + nextMajor: number; } export interface SetupDependencies { management: ManagementSetup; + share: SharePluginSetup; cloud?: CloudSetup; + usageCollection?: UsageCollectionSetup; } + export interface StartDependencies { licensing: LicensingPluginStart; - discover: DiscoverStart; data: DataPublicPluginStart; } @@ -33,3 +40,20 @@ export interface ClientConfigType { enabled: boolean; }; } + +export interface AppDependencies { + isReadOnlyMode: boolean; + kibanaVersionInfo: KibanaVersionContext; + plugins: { + cloud?: CloudSetup; + share: SharePluginSetup; + infra: object | undefined; + }; + services: { + core: CoreStart; + data: DataPublicPluginStart; + breadcrumbs: BreadcrumbService; + history: ScopedHistory; + api: ApiService; + }; +} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json index 617bb02ff9df..2337e0e2dc03 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json +++ b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json @@ -102,6 +102,15 @@ "resolve_during_rolling_upgrade": false } ], + ".ml-config": [ + { + "level": "critical", + "message": "Index created before 7.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", + "details": "This index was created using version: 6.8.16", + "resolve_during_rolling_upgrade": false + } + ], ".watcher-history-6-2018.11.07": [ { "level": "warning", diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts index 0e01d8d6a345..b3b93582e226 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts @@ -64,7 +64,7 @@ describe('setDeprecationLogging', () => { }); describe('isDeprecationLoggingEnabled', () => { - ['default', 'persistent', 'transient'].forEach((tier) => { + ['defaults', 'persistent', 'transient'].forEach((tier) => { ['ALL', 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ALL'].forEach((level) => { it(`returns true when ${tier} is set to ${level}`, () => { expect(isDeprecationLoggingEnabled({ [tier]: { logger: { deprecation: level } } })).toBe( @@ -74,7 +74,7 @@ describe('isDeprecationLoggingEnabled', () => { }); }); - ['default', 'persistent', 'transient'].forEach((tier) => { + ['defaults', 'persistent', 'transient'].forEach((tier) => { ['ERROR', 'FATAL'].forEach((level) => { it(`returns false when ${tier} is set to ${level}`, () => { expect(isDeprecationLoggingEnabled({ [tier]: { logger: { deprecation: level } } })).toBe( @@ -87,7 +87,7 @@ describe('isDeprecationLoggingEnabled', () => { it('allows transient to override persistent and default', () => { expect( isDeprecationLoggingEnabled({ - default: { logger: { deprecation: 'FATAL' } }, + defaults: { logger: { deprecation: 'FATAL' } }, persistent: { logger: { deprecation: 'FATAL' } }, transient: { logger: { deprecation: 'WARN' } }, }) @@ -97,7 +97,7 @@ describe('isDeprecationLoggingEnabled', () => { it('allows persistent to override default', () => { expect( isDeprecationLoggingEnabled({ - default: { logger: { deprecation: 'FATAL' } }, + defaults: { logger: { deprecation: 'FATAL' } }, persistent: { logger: { deprecation: 'WARN' } }, }) ).toBe(true); @@ -108,7 +108,7 @@ describe('isDeprecationLogIndexingEnabled', () => { it('allows transient to override persistent and default', () => { expect( isDeprecationLogIndexingEnabled({ - default: { cluster: { deprecation_indexing: { enabled: 'false' } } }, + defaults: { cluster: { deprecation_indexing: { enabled: 'false' } } }, persistent: { cluster: { deprecation_indexing: { enabled: 'false' } } }, transient: { cluster: { deprecation_indexing: { enabled: 'true' } } }, }) @@ -118,7 +118,7 @@ describe('isDeprecationLogIndexingEnabled', () => { it('allows persistent to override default', () => { expect( isDeprecationLogIndexingEnabled({ - default: { cluster: { deprecation_indexing: { enabled: 'false' } } }, + defaults: { cluster: { deprecation_indexing: { enabled: 'false' } } }, persistent: { cluster: { deprecation_indexing: { enabled: 'true' } } }, }) ).toBe(true); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts index 214aabb98992..2793c2c6ac81 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts @@ -51,7 +51,7 @@ export async function setDeprecationLogging( } export function isDeprecationLogIndexingEnabled(settings: any) { - const clusterDeprecationLoggingEnabled = ['default', 'persistent', 'transient'].reduce( + const clusterDeprecationLoggingEnabled = ['defaults', 'persistent', 'transient'].reduce( (currentLogLevel, settingsTier) => get(settings, [settingsTier, 'cluster', 'deprecation_indexing', 'enabled'], currentLogLevel), 'false' @@ -61,7 +61,7 @@ export function isDeprecationLogIndexingEnabled(settings: any) { } export function isDeprecationLoggingEnabled(settings: any) { - const deprecationLogLevel = ['default', 'persistent', 'transient'].reduce( + const deprecationLogLevel = ['defaults', 'persistent', 'transient'].reduce( (currentLogLevel, settingsTier) => get(settings, [settingsTier, 'logger', 'deprecation'], currentLogLevel), 'WARN' diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts index 99c101e04e36..06c0352ebcdc 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts @@ -40,6 +40,25 @@ describe('getESUpgradeStatus', () => { asApiResponse(deprecationsResponse) ); + esClient.asCurrentUser.transport.request.mockResolvedValue( + asApiResponse({ + features: [ + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.1', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.ml-config', + version: '7.1.1', + }, + ], + }, + ], + migration_status: 'MIGRATION_NEEDED', + }) + ); + // @ts-expect-error not full interface of response esClient.asCurrentUser.indices.resolveIndex.mockResolvedValue(asApiResponse(resolvedIndices)); @@ -86,4 +105,30 @@ describe('getESUpgradeStatus', () => { 0 ); }); + + it('filters out system indices returned by upgrade system indices API', async () => { + esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + asApiResponse({ + cluster_settings: [], + node_settings: [], + ml_settings: [], + index_settings: { + '.ml-config': [ + { + level: 'critical', + message: 'Index created before 7.0', + url: 'https://', + details: '...', + resolve_during_rolling_upgrade: false, + }, + ], + }, + }) + ); + + const upgradeStatus = await getESUpgradeStatus(esClient); + + expect(upgradeStatus.deprecations).toHaveLength(0); + expect(upgradeStatus.totalCriticalDeprecations).toBe(0); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts index aa08ecef78d3..2e2c80b790cd 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts @@ -11,6 +11,10 @@ import { indexSettingDeprecations } from '../../common/constants'; import { EnrichedDeprecationInfo, ESUpgradeStatus } from '../../common/types'; import { esIndicesStateCheck } from './es_indices_state_check'; +import { + getESSystemIndicesMigrationStatus, + convertFeaturesToIndicesArray, +} from '../lib/es_system_indices_migration'; export async function getESUpgradeStatus( dataClient: IScopedClusterClient @@ -19,10 +23,19 @@ export async function getESUpgradeStatus( const getCombinedDeprecations = async () => { const indices = await getCombinedIndexInfos(deprecations, dataClient); + const systemIndices = await getESSystemIndicesMigrationStatus(dataClient.asCurrentUser); + const systemIndicesList = convertFeaturesToIndicesArray(systemIndices.features); return Object.keys(deprecations).reduce((combinedDeprecations, deprecationType) => { if (deprecationType === 'index_settings') { - combinedDeprecations = combinedDeprecations.concat(indices); + // We need to exclude all index related deprecations for system indices since + // they are resolved separately through the system indices upgrade section in + // the Overview page. + const withoutSystemIndices = indices.filter( + (index) => !systemIndicesList.includes(index.index!) + ); + + combinedDeprecations = combinedDeprecations.concat(withoutSystemIndices); } else { const deprecationsByType = deprecations[ deprecationType as keyof estypes.MigrationDeprecationsResponse diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts new file mode 100644 index 000000000000..560d42712b5d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { convertFeaturesToIndicesArray } from './es_system_indices_migration'; +import { SystemIndicesMigrationStatus } from '../../common/types'; + +const esUpgradeSystemIndicesStatusMock: SystemIndicesMigrationStatus = { + features: [ + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.1', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.ml-config', + version: '7.1.1', + }, + { + index: '.ml-notifications', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'security', + minimum_index_version: '7.1.1', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.ml-config', + version: '7.1.1', + }, + ], + }, + ], + migration_status: 'MIGRATION_NEEDED', +}; + +describe('convertFeaturesToIndicesArray', () => { + it('converts list with features to flat array of uniq indices', async () => { + const result = convertFeaturesToIndicesArray(esUpgradeSystemIndicesStatusMock.features); + expect(result).toEqual(['.ml-config', '.ml-notifications']); + }); + + it('returns empty array if no features are passed to it', async () => { + expect(convertFeaturesToIndicesArray([])).toEqual([]); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts new file mode 100644 index 000000000000..aa239de7dd00 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts @@ -0,0 +1,51 @@ +/* + * 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 { flow, flatMap, map, flatten, uniq } from 'lodash/fp'; +import { ElasticsearchClient } from 'src/core/server'; +import { + SystemIndicesMigrationStatus, + SystemIndicesMigrationFeature, + SystemIndicesMigrationStarted, +} from '../../common/types'; + +export const convertFeaturesToIndicesArray = ( + features: SystemIndicesMigrationFeature[] +): string[] => { + return flow( + // Map each feature into Indices[] + map('indices'), + // Flatten each into an string[] of indices + map(flatMap('index')), + // Flatten the array + flatten, + // And finally dedupe the indices + uniq + )(features); +}; + +export const getESSystemIndicesMigrationStatus = async ( + client: ElasticsearchClient +): Promise => { + const { body } = await client.transport.request({ + method: 'GET', + path: '/_migration/system_features', + }); + + return body as SystemIndicesMigrationStatus; +}; + +export const startESSystemIndicesMigration = async ( + client: ElasticsearchClient +): Promise => { + const { body } = await client.transport.request({ + method: 'POST', + path: '/_migration/system_features', + }); + + return body as SystemIndicesMigrationStarted; +}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts index 8bf9143d93db..8532e2e4eece 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts @@ -5,33 +5,171 @@ * 2.0. */ +import { KibanaRequest } from 'src/core/server'; +import { loggingSystemMock, httpServerMock } from 'src/core/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; import { ReindexSavedObject } from '../../../common/types'; -import { Credential, credentialStoreFactory } from './credential_store'; +import { credentialStoreFactory } from './credential_store'; + +const basicAuthHeader = 'Basic abc'; + +const logMock = loggingSystemMock.create().get(); +const requestMock = KibanaRequest.from( + httpServerMock.createRawRequest({ + headers: { + authorization: basicAuthHeader, + }, + }) +); +const securityStartMock = securityMock.createStart(); + +const reindexOpMock = { + id: 'asdf', + attributes: { indexName: 'test', lastCompletedStep: 1, locked: null }, +} as ReindexSavedObject; describe('credentialStore', () => { - it('retrieves the same credentials for the same state', () => { - const creds = { key: '1' } as Credential; - const reindexOp = { - id: 'asdf', - attributes: { indexName: 'test', lastCompletedStep: 1, locked: null }, - } as ReindexSavedObject; - - const credStore = credentialStoreFactory(); - credStore.set(reindexOp, creds); - expect(credStore.get(reindexOp)).toEqual(creds); + it('retrieves the same credentials for the same state', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + expect(credStore.get(reindexOpMock)).toEqual({ + authorization: basicAuthHeader, + }); + }); + + it('does not retrieve credentials if the state changed', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + reindexOpMock.attributes.lastCompletedStep = 0; + + expect(credStore.get(reindexOpMock)).toBeUndefined(); + }); + + it('retrieves credentials after update', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + const updatedReindexOp = { + ...reindexOpMock, + attributes: { + ...reindexOpMock.attributes, + status: 0, + }, + }; + + await credStore.update({ + credential: { + authorization: basicAuthHeader, + }, + reindexOp: updatedReindexOp, + security: securityStartMock, + }); + + expect(credStore.get(updatedReindexOp)).toEqual({ + authorization: basicAuthHeader, + }); }); - it('does retrieve credentials if the state is changed', () => { - const creds = { key: '1' } as Credential; - const reindexOp = { - id: 'asdf', - attributes: { indexName: 'test', lastCompletedStep: 1, locked: null }, - } as ReindexSavedObject; + describe('API keys enabled', () => { + const apiKeyResultMock = { + id: 'api_key_id', + name: 'api_key_name', + api_key: '123', + }; + + const invalidateApiKeyResultMock = { + invalidated_api_keys: [apiKeyResultMock.api_key], + previously_invalidated_api_keys: [], + error_count: 0, + }; + + const base64ApiKey = Buffer.from(`${apiKeyResultMock.id}:${apiKeyResultMock.api_key}`).toString( + 'base64' + ); + + beforeEach(() => { + securityStartMock.authc.apiKeys.areAPIKeysEnabled.mockReturnValue(Promise.resolve(true)); + securityStartMock.authc.apiKeys.grantAsInternalUser.mockReturnValue( + Promise.resolve(apiKeyResultMock) + ); + securityStartMock.authc.apiKeys.invalidateAsInternalUser.mockReturnValue( + Promise.resolve(invalidateApiKeyResultMock) + ); + }); + + it('sets API key in authorization header', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + expect(credStore.get(reindexOpMock)).toEqual({ + authorization: `ApiKey ${base64ApiKey}`, + }); + }); + + it('invalidates API keys when a reindex operation is complete', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + await credStore.update({ + credential: { + authorization: `ApiKey ${base64ApiKey}`, + }, + reindexOp: { + ...reindexOpMock, + attributes: { + ...reindexOpMock.attributes, + status: 1, + }, + }, + security: securityStartMock, + }); + + expect(securityStartMock.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalled(); + }); + + it('falls back to user credentials when error granting API key', async () => { + const credStore = credentialStoreFactory(logMock); + + securityStartMock.authc.apiKeys.grantAsInternalUser.mockRejectedValue( + new Error('Error granting API key') + ); - const credStore = credentialStoreFactory(); - credStore.set(reindexOp, creds); + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); - reindexOp.attributes.lastCompletedStep = 0; - expect(credStore.get(reindexOp)).not.toBeDefined(); + expect(credStore.get(reindexOpMock)).toEqual({ + authorization: basicAuthHeader, + }); + }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts index 2c4f86824518..66885a23cf96 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts @@ -8,10 +8,73 @@ import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; -import { ReindexSavedObject } from '../../../common/types'; +import { KibanaRequest, Logger } from 'src/core/server'; + +import { SecurityPluginStart } from '../../../../security/server'; +import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; export type Credential = Record; +// Generates a stable hash for the reindex operation's current state. +const getHash = (reindexOp: ReindexSavedObject) => + createHash('sha256') + .update(stringify({ id: reindexOp.id, ...reindexOp.attributes })) + .digest('base64'); + +// Returns a base64-encoded API key string or undefined +const getApiKey = async ({ + request, + security, + reindexOpId, + apiKeysMap, +}: { + request: KibanaRequest; + security: SecurityPluginStart; + reindexOpId: string; + apiKeysMap: Map; +}): Promise => { + try { + const apiKeyResult = await security.authc.apiKeys.grantAsInternalUser(request, { + name: `ua_reindex_${reindexOpId}`, + role_descriptors: {}, + metadata: { + description: + 'Created by the Upgrade Assistant for a reindex operation; this can be safely deleted after Kibana is upgraded.', + }, + }); + + if (apiKeyResult) { + const { api_key: apiKey, id } = apiKeyResult; + // Store each API key per reindex operation so that we can later invalidate it when the reindex operation is complete + apiKeysMap.set(reindexOpId, id); + // Returns the base64 encoding of `id:api_key` + // This can be used when sending a request with an "Authorization: ApiKey xxx" header + return Buffer.from(`${id}:${apiKey}`).toString('base64'); + } + } catch (error) { + // There are a few edge cases were granting an API key could fail, + // in which case we fall back to using the requestor's credentials in memory + return undefined; + } +}; + +const invalidateApiKey = async ({ + apiKeyId, + security, + log, +}: { + apiKeyId: string; + security?: SecurityPluginStart; + log: Logger; +}) => { + try { + await security?.authc.apiKeys.invalidateAsInternalUser({ ids: [apiKeyId] }); + } catch (error) { + // Swallow error if there's a problem invalidating API key + log.debug(`Error invalidating API key for id ${apiKeyId}: ${error.message}`); + } +}; + /** * An in-memory cache for user credentials to be used for reindexing operations. When looking up * credentials, the reindex operation must be in the same state it was in when the credentials @@ -20,25 +83,82 @@ export type Credential = Record; */ export interface CredentialStore { get(reindexOp: ReindexSavedObject): Credential | undefined; - set(reindexOp: ReindexSavedObject, credential: Credential): void; + set(params: { + reindexOp: ReindexSavedObject; + request: KibanaRequest; + security?: SecurityPluginStart; + }): Promise; + update(params: { + reindexOp: ReindexSavedObject; + security?: SecurityPluginStart; + credential: Credential; + }): Promise; clear(): void; } -export const credentialStoreFactory = (): CredentialStore => { +export const credentialStoreFactory = (logger: Logger): CredentialStore => { const credMap = new Map(); - - // Generates a stable hash for the reindex operation's current state. - const getHash = (reindexOp: ReindexSavedObject) => - createHash('sha256') - .update(stringify({ id: reindexOp.id, ...reindexOp.attributes })) - .digest('base64'); + const apiKeysMap = new Map(); + const log = logger.get('credential_store'); return { get(reindexOp: ReindexSavedObject) { return credMap.get(getHash(reindexOp)); }, - set(reindexOp: ReindexSavedObject, credential: Credential) { + async set({ + reindexOp, + request, + security, + }: { + reindexOp: ReindexSavedObject; + request: KibanaRequest; + security?: SecurityPluginStart; + }) { + const areApiKeysEnabled = (await security?.authc.apiKeys.areAPIKeysEnabled()) ?? false; + + if (areApiKeysEnabled) { + const apiKey = await getApiKey({ + request, + security: security!, + reindexOpId: reindexOp.id, + apiKeysMap, + }); + + if (apiKey) { + credMap.set(getHash(reindexOp), { + ...request.headers, + authorization: `ApiKey ${apiKey}`, + }); + return; + } + } + + // Set the requestor's credentials in memory if apiKeys are not enabled + credMap.set(getHash(reindexOp), request.headers); + }, + + async update({ + reindexOp, + security, + credential, + }: { + reindexOp: ReindexSavedObject; + security?: SecurityPluginStart; + credential: Credential; + }) { + // If the reindex operation is completed... + if (reindexOp.attributes.status === ReindexStatus.completed) { + // ...and an API key is being used, invalidate it + const apiKeyId = apiKeysMap.get(reindexOp.id); + if (apiKeyId) { + await invalidateApiKey({ apiKeyId, security, log }); + apiKeysMap.delete(reindexOp.id); + return; + } + } + + // Otherwise, re-associate the credentials credMap.set(getHash(reindexOp), credential); }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 7595e1da7b57..3f58a04949da 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -13,7 +13,6 @@ import { ScopedClusterClientMock } from 'src/core/server/elasticsearch/client/mo import moment from 'moment'; import { - IndexGroup, REINDEX_OP_TYPE, ReindexSavedObject, ReindexStatus, @@ -283,46 +282,4 @@ describe('ReindexActions', () => { await expect(actions.getFlatSettings('myIndex')).resolves.toBeNull(); }); }); - - describe('runWhileConsumerLocked', () => { - Object.entries(IndexGroup).forEach(([typeKey, consumerType]) => { - describe(`IndexConsumerType.${typeKey}`, () => { - it('creates the lock doc if it does not exist and executes callback', async () => { - expect.assertions(3); - client.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError()); // mock no ML doc exists yet - client.create.mockImplementationOnce((type: any, attributes: any, { id }: any) => - Promise.resolve({ - type, - id, - attributes, - }) - ); - - let flip = false; - await actions.runWhileIndexGroupLocked(consumerType, async (mlDoc) => { - expect(mlDoc.id).toEqual(consumerType); - expect(mlDoc.attributes.runningReindexCount).toEqual(0); - flip = true; - return mlDoc; - }); - expect(flip).toEqual(true); - }); - - it('fails after 10 attempts to lock', async () => { - client.get.mockResolvedValue({ - type: REINDEX_OP_TYPE, - id: consumerType, - attributes: { mlReindexCount: 0 }, - }); - - client.update.mockRejectedValue(new Error('NO LOCKING!')); - - await expect( - actions.runWhileIndexGroupLocked(consumerType, async (m) => m) - ).rejects.toThrow('Could not acquire lock for ML jobs'); - expect(client.update).toHaveBeenCalledTimes(10); - }, 20000); - }); - }); - }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index fe8844b28e37..09ba4b744e68 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -13,7 +13,6 @@ import { ElasticsearchClient, } from 'src/core/server'; import { - IndexGroup, REINDEX_OP_TYPE, ReindexOperation, ReindexOptions, @@ -33,11 +32,6 @@ export const LOCK_WINDOW = moment.duration(90, 'seconds'); * This is NOT intended to be used by any other code. */ export interface ReindexActions { - /** - * Namespace for ML-specific actions. - */ - // ml: MlActions; - /** * Creates a new reindexOp, does not perform any pre-flight checks. * @param indexName @@ -86,34 +80,10 @@ export interface ReindexActions { * Retrieve index settings (in flat, dot-notation style) and mappings. * @param indexName */ - getFlatSettings(indexName: string): Promise; - - // ----- Functions below are for enforcing locks around groups of indices like ML or Watcher - - /** - * Atomically increments the number of reindex operations running for an index group. - */ - incrementIndexGroupReindexes(group: IndexGroup): Promise; - - /** - * Atomically decrements the number of reindex operations running for an index group. - */ - decrementIndexGroupReindexes(group: IndexGroup): Promise; - - /** - * Runs a callback function while locking an index group. - * @param func A function to run with the locked index group lock document. Must return a promise that resolves - * to the updated ReindexSavedObject. - */ - runWhileIndexGroupLocked( - group: IndexGroup, - func: (lockDoc: ReindexSavedObject) => Promise - ): Promise; - - /** - * Exposed only for testing, DO NOT USE. - */ - _fetchAndLockIndexGroupDoc(group: IndexGroup): Promise; + getFlatSettings( + indexName: string, + withTypeName?: boolean + ): Promise; } export const reindexActionsFactory = ( @@ -266,76 +236,5 @@ export const reindexActionsFactory = ( return flatSettings.body[indexName]; }, - - async _fetchAndLockIndexGroupDoc(indexGroup) { - const fetchDoc = async () => { - try { - // The IndexGroup enum value (a string) serves as the ID of the lock doc - return await client.get(REINDEX_OP_TYPE, indexGroup); - } catch (e) { - if (client.errors.isNotFoundError(e)) { - return await client.create( - REINDEX_OP_TYPE, - { - indexName: null, - newIndexName: null, - locked: null, - status: null, - lastCompletedStep: null, - reindexTaskId: null, - reindexTaskPercComplete: null, - errorMessage: null, - runningReindexCount: 0, - } as any, - { id: indexGroup } - ); - } else { - throw e; - } - } - }; - - const lockDoc = async (attempt = 1): Promise => { - try { - // Refetch the document each time to avoid version conflicts. - return await acquireLock(await fetchDoc()); - } catch (e) { - if (attempt >= 10) { - throw new Error(`Could not acquire lock for ML jobs`); - } - - await new Promise((resolve) => setTimeout(resolve, 1000)); - return lockDoc(attempt + 1); - } - }; - - return lockDoc(); - }, - - async incrementIndexGroupReindexes(indexGroup) { - this.runWhileIndexGroupLocked(indexGroup, (lockDoc) => - this.updateReindexOp(lockDoc, { - runningReindexCount: lockDoc.attributes.runningReindexCount! + 1, - }) - ); - }, - - async decrementIndexGroupReindexes(indexGroup) { - this.runWhileIndexGroupLocked(indexGroup, (lockDoc) => - this.updateReindexOp(lockDoc, { - runningReindexCount: lockDoc.attributes.runningReindexCount! - 1, - }) - ); - }, - - async runWhileIndexGroupLocked(indexGroup, func) { - let lockDoc = await this._fetchAndLockIndexGroupDoc(indexGroup); - - try { - lockDoc = await func(lockDoc); - } finally { - await releaseLock(lockDoc); - } - }, }; }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index bd31196dbb78..b68faf7f75b9 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -14,7 +14,6 @@ import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/moc import { ScopedClusterClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { - IndexGroup, ReindexOperation, ReindexSavedObject, ReindexStatus, @@ -28,12 +27,7 @@ import { getMockVersionInfo } from '../__fixtures__/version'; import { esIndicesStateCheck } from '../es_indices_state_check'; import { versionService } from '../version'; -import { - isMlIndex, - isWatcherIndex, - ReindexService, - reindexServiceFactory, -} from './reindex_service'; +import { ReindexService, reindexServiceFactory } from './reindex_service'; const asApiResponse = (body: T): TransportResult => ({ @@ -69,9 +63,6 @@ describe('reindexService', () => { findAllByStatus: jest.fn(unimplemented('findAllInProgressOperations')), getFlatSettings: jest.fn(unimplemented('getFlatSettings')), cleanupChanges: jest.fn(), - incrementIndexGroupReindexes: jest.fn(unimplemented('incrementIndexGroupReindexes')), - decrementIndexGroupReindexes: jest.fn(unimplemented('decrementIndexGroupReindexes')), - runWhileIndexGroupLocked: jest.fn(async (group: string, f: any) => f({ attributes: {} })), }; clusterClient = elasticsearchServiceMock.createScopedClusterClient(); log = loggingSystemMock.create().get(); @@ -129,31 +120,6 @@ describe('reindexService', () => { }); }); - it('includes manage_ml for ML indices', async () => { - clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ has_all_requested: true }) - ); - - await service.hasRequiredPrivileges('.ml-anomalies'); - expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ - body: { - cluster: ['manage', 'manage_ml'], - index: [ - { - names: ['.ml-anomalies', `.reindexed-v${currentMajor}-ml-anomalies`], - allow_restricted_indices: true, - privileges: ['all'], - }, - { - names: ['.tasks'], - privileges: ['read', 'delete'], - }, - ], - }, - }); - }); - it('includes checking for permissions on the baseName which could be an alias', async () => { clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( // @ts-expect-error not full interface @@ -183,33 +149,6 @@ describe('reindexService', () => { }, }); }); - - it('includes manage_watcher for watcher indices', async () => { - clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ - has_all_requested: true, - }) - ); - - await service.hasRequiredPrivileges('.watches'); - expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ - body: { - cluster: ['manage', 'manage_watcher'], - index: [ - { - names: ['.watches', `.reindexed-v${currentMajor}-watches`], - allow_restricted_indices: true, - privileges: ['all'], - }, - { - names: ['.tasks'], - privileges: ['read', 'delete'], - }, - ], - }, - }); - }); }); describe('detectReindexWarnings', () => { @@ -496,40 +435,6 @@ describe('reindexService', () => { }); }); - describe('isMlIndex', () => { - it('is false for non-ml indices', () => { - expect(isMlIndex('.literally-anything')).toBe(false); - }); - - it('is true for ML indices', () => { - expect(isMlIndex('.ml-state')).toBe(true); - expect(isMlIndex('.ml-anomalies')).toBe(true); - expect(isMlIndex('.ml-config')).toBe(true); - }); - - it('is true for ML re-indexed indices', () => { - expect(isMlIndex(`.reindexed-v${prevMajor}-ml-state`)).toBe(true); - expect(isMlIndex(`.reindexed-v${prevMajor}-ml-anomalies`)).toBe(true); - expect(isMlIndex(`.reindexed-v${prevMajor}-ml-config`)).toBe(true); - }); - }); - - describe('isWatcherIndex', () => { - it('is false for non-watcher indices', () => { - expect(isWatcherIndex('.literally-anything')).toBe(false); - }); - - it('is true for watcher indices', () => { - expect(isWatcherIndex('.watches')).toBe(true); - expect(isWatcherIndex('.triggered-watches')).toBe(true); - }); - - it('is true for watcher re-indexed indices', () => { - expect(isWatcherIndex(`.reindexed-v${prevMajor}-watches`)).toBe(true); - expect(isWatcherIndex(`.reindexed-v${prevMajor}-triggered-watches`)).toBe(true); - }); - }); - describe('state machine, lastCompletedStep ===', () => { const defaultAttributes = { indexName: 'myIndex', @@ -541,287 +446,6 @@ describe('reindexService', () => { mappings: { _doc: { properties: { timestampl: { type: 'date' } } } }, }; - describe('created', () => { - const reindexOp = { - id: '1', - attributes: { ...defaultAttributes, lastCompletedStep: ReindexStep.created }, - } as ReindexSavedObject; - - describe('ml behavior', () => { - const mlReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.ml-anomalies' }, - } as ReindexSavedObject; - - it('does nothing if index is not an ML index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).not.toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).not.toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.nodes.info).not.toHaveBeenCalled(); - }); - - it('supports an already migrated ML index', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const mlReindexedOp = { - id: '2', - attributes: { - ...reindexOp.attributes, - indexName: `.reindexed-v${prevMajor}-ml-anomalies`, - }, - } as ReindexSavedObject; - const updatedOp = await service.processNextStep(mlReindexedOp); - - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('increments ML reindexes and calls ML stop endpoint', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if ML reindexes cannot be incremented', async () => { - actions.incrementIndexGroupReindexes.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if ML doc cannot be locked', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if ML endpoint fails', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.7.0' } } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not stop ML jobs') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if not all nodes have been upgraded to 6.7.0', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.6.0' } } }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Some nodes are not on minimum version') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - // Should not have called ML endpoint at all - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - }); - - describe('watcher behavior', () => { - const watcherReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.watches' }, - } as ReindexSavedObject; - - it('does nothing if index is not a watcher index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).not.toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).not.toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalled(); - }); - - it('increments ML reindexes and calls watcher stop endpoint', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (type: string, f: any) => - f() - ); - clusterClient.asCurrentUser.watcher.stop.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.watcher); - expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.watcher.stop).toHaveBeenCalled(); - }); - - it('fails if watcher reindexes cannot be incremented', async () => { - actions.incrementIndexGroupReindexes.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if watcher doc cannot be locked', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalled(); - }); - - it('fails if watcher endpoint fails', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (type: string, f: any) => - f() - ); - clusterClient.asCurrentUser.watcher.stop.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not stop Watcher') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.stop).toHaveBeenCalled(); - }); - }); - }); - - describe('indexConsumersStopped', () => { - const reindexOp = { - id: '1', - attributes: { - ...defaultAttributes, - lastCompletedStep: ReindexStep.indexGroupServicesStopped, - }, - } as ReindexSavedObject; - - it('blocks writes and updates lastCompletedStep', async () => { - clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); - expect(clusterClient.asCurrentUser.indices.putSettings).toHaveBeenCalledWith({ - index: 'myIndex', - body: { blocks: { write: true } }, - }); - }); - - it('fails if setting updates are not acknowledged', async () => { - clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - }); - - it('fails if setting updates fail', async () => { - clusterClient.asCurrentUser.indices.putSettings.mockRejectedValueOnce(new Error('blah!')); - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - }); - }); - describe('readonly', () => { const reindexOp = { id: '1', @@ -1129,216 +753,19 @@ describe('reindexService', () => { }); describe('aliasCreated', () => { - const reindexOp = { - id: '1', - attributes: { ...defaultAttributes, lastCompletedStep: ReindexStep.aliasCreated }, - } as ReindexSavedObject; - - describe('ml behavior', () => { - const mlReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.ml-anomalies' }, - } as ReindexSavedObject; - - it('does nothing if index is not an ML index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalled(); - }); - - it('decrements ML reindexes and calls ML start endpoint if no remaining ML jobs', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(actions.decrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.ml); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('does not call ML start endpoint if there are remaining ML jobs', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 2 } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('fails if ML reindexes cannot be decremented', async () => { - // Mock unable to lock ml doc - actions.decrementIndexGroupReindexes.mockRejectedValue(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('fails if ML doc cannot be locked', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - // Mock unable to lock ml doc - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('fails if ML endpoint fails', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not resume ML jobs') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: false, - }); - }); - }); - - describe('watcher behavior', () => { - const watcherReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.watches' }, - } as ReindexSavedObject; - - it('does nothing if index is not a watcher index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalled(); - }); - - it('decrements watcher reindexes and calls wathcer start endpoint if no remaining watcher reindexes', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(actions.decrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.watcher); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.watcher.start).toHaveBeenCalled(); - }); - - it('does not call watcher start endpoint if there are remaining watcher reindexes', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 2 } }) - ); - clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); - }); - - it('fails if watcher reindexes cannot be decremented', async () => { - // Mock unable to lock watcher doc - actions.decrementIndexGroupReindexes.mockRejectedValue(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); - }); - - it('fails if watcher doc cannot be locked', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - // Mock unable to lock watcher doc - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); - }); - - it('fails if watcher endpoint fails', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - - clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not start Watcher') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.start).toHaveBeenCalled(); - }); - }); - }); - - describe('indexGroupServicesStarted', () => { const reindexOp = { id: '1', attributes: { ...defaultAttributes, - lastCompletedStep: ReindexStep.indexGroupServicesStarted, + lastCompletedStep: ReindexStep.aliasCreated, }, } as ReindexSavedObject; - it('sets to completed', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.completed); + it('sets reindex status as complete', async () => { + await service.processNextStep(reindexOp); + expect(actions.updateReindexOp).toHaveBeenCalledWith(reindexOp, { + status: ReindexStatus.completed, + }); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 77b5495bd456..f9db1692ab1b 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -11,7 +11,6 @@ import { first } from 'rxjs/operators'; import { LicensingPluginSetup } from '../../../../licensing/server'; import { - IndexGroup, ReindexSavedObject, ReindexStatus, ReindexStep, @@ -31,10 +30,6 @@ import { ReindexActions } from './reindex_actions'; import { error } from './error'; -const VERSION_REGEX = new RegExp(/^([1-9]+)\.([0-9]+)\.([0-9]+)/); -const ML_INDICES = ['.ml-state', '.ml-anomalies', '.ml-config']; -const WATCHER_INDICES = ['.watches', '.triggered-watches']; - export interface ReindexService { /** * Checks whether or not the user has proper privileges required to reindex this index. @@ -49,12 +44,6 @@ export interface ReindexService { */ detectReindexWarnings(indexName: string): Promise; - /** - * Returns an IndexGroup if the index belongs to one, otherwise undefined. - * @param indexName - */ - getIndexGroup(indexName: string): IndexGroup | undefined; - /** * Creates a new reindex operation for a given index. * @param indexName @@ -135,83 +124,6 @@ export const reindexServiceFactory = ( licensing: LicensingPluginSetup ): ReindexService => { // ------ Utility functions - - /** - * If the index is a ML index that will cause jobs to fail when set to readonly, - * turn on 'upgrade mode' to pause all ML jobs. - * @param reindexOp - */ - const stopMlJobs = async () => { - await actions.incrementIndexGroupReindexes(IndexGroup.ml); - await actions.runWhileIndexGroupLocked(IndexGroup.ml, async (mlDoc) => { - await validateNodesMinimumVersion(6, 7); - - const { body } = await esClient.ml.setUpgradeMode({ - enabled: true, - }); - - if (!body.acknowledged) { - throw new Error(`Could not stop ML jobs`); - } - - return mlDoc; - }); - }; - - /** - * Resumes ML jobs if there are no more remaining reindex operations. - */ - const resumeMlJobs = async () => { - await actions.decrementIndexGroupReindexes(IndexGroup.ml); - await actions.runWhileIndexGroupLocked(IndexGroup.ml, async (mlDoc) => { - if (mlDoc.attributes.runningReindexCount === 0) { - const { body } = await esClient.ml.setUpgradeMode({ - enabled: false, - }); - - if (!body.acknowledged) { - throw new Error(`Could not resume ML jobs`); - } - } - - return mlDoc; - }); - }; - - /** - * Stops Watcher in Elasticsearch. - */ - const stopWatcher = async () => { - await actions.incrementIndexGroupReindexes(IndexGroup.watcher); - await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async (watcherDoc) => { - const { body } = await esClient.watcher.stop(); - - if (!body.acknowledged) { - throw new Error('Could not stop Watcher'); - } - - return watcherDoc; - }); - }; - - /** - * Starts Watcher in Elasticsearch. - */ - const startWatcher = async () => { - await actions.decrementIndexGroupReindexes(IndexGroup.watcher); - await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async (watcherDoc) => { - if (watcherDoc.attributes.runningReindexCount === 0) { - const { body } = await esClient.watcher.start(); - - if (!body.acknowledged) { - throw new Error('Could not start Watcher'); - } - } - - return watcherDoc; - }); - }; - const cleanupChanges = async (reindexOp: ReindexSavedObject) => { // Cancel reindex task if it was started but not completed if (reindexOp.attributes.lastCompletedStep === ReindexStep.reindexStarted) { @@ -239,48 +151,11 @@ export const reindexServiceFactory = ( }); } - // Resume consumers if we ever got past this point. - if (reindexOp.attributes.lastCompletedStep >= ReindexStep.indexGroupServicesStopped) { - await resumeIndexGroupServices(reindexOp); - } - return reindexOp; }; // ------ Functions used to process the state machine - const validateNodesMinimumVersion = async (minMajor: number, minMinor: number) => { - const { body: nodesResponse } = await esClient.nodes.info(); - - const outDatedNodes = Object.values(nodesResponse.nodes).filter((node: any) => { - const matches = node.version.match(VERSION_REGEX); - const major = parseInt(matches[1], 10); - const minor = parseInt(matches[2], 10); - - // All ES nodes must be >= 6.7.0 to pause ML jobs - return !(major > minMajor || (major === minMajor && minor >= minMinor)); - }); - - if (outDatedNodes.length > 0) { - const nodeList = JSON.stringify(outDatedNodes.map((n: any) => n.name)); - throw new Error( - `Some nodes are not on minimum version (${minMajor}.${minMinor}.0) required: ${nodeList}` - ); - } - }; - - const stopIndexGroupServices = async (reindexOp: ReindexSavedObject) => { - if (isMlIndex(reindexOp.attributes.indexName)) { - await stopMlJobs(); - } else if (isWatcherIndex(reindexOp.attributes.indexName)) { - await stopWatcher(); - } - - return actions.updateReindexOp(reindexOp, { - lastCompletedStep: ReindexStep.indexGroupServicesStopped, - }); - }; - /** * Sets the original index as readonly so new data can be indexed until the reindex * is completed. @@ -476,23 +351,6 @@ export const reindexServiceFactory = ( }); }; - const resumeIndexGroupServices = async (reindexOp: ReindexSavedObject) => { - if (isMlIndex(reindexOp.attributes.indexName)) { - await resumeMlJobs(); - } else if (isWatcherIndex(reindexOp.attributes.indexName)) { - await startWatcher(); - } - - // Only change the status if we're still in-progress (this function is also called when the reindex fails or is cancelled) - if (reindexOp.attributes.status === ReindexStatus.inProgress) { - return actions.updateReindexOp(reindexOp, { - lastCompletedStep: ReindexStep.indexGroupServicesStarted, - }); - } else { - return reindexOp; - } - }; - // ------ The service itself return { @@ -537,14 +395,6 @@ export const reindexServiceFactory = ( ], } as any; - if (isMlIndex(indexName)) { - body.cluster = [...body.cluster, 'manage_ml']; - } - - if (isWatcherIndex(indexName)) { - body.cluster = [...body.cluster, 'manage_watcher']; - } - const { body: resp } = await esClient.security.hasPrivileges({ body, }); @@ -561,14 +411,6 @@ export const reindexServiceFactory = ( } }, - getIndexGroup(indexName: string) { - if (isMlIndex(indexName)) { - return IndexGroup.ml; - } else if (isWatcherIndex(indexName)) { - return IndexGroup.watcher; - } - }, - async createReindexOperation(indexName: string, opts?: { enqueue: boolean }) { const { body: indexExists } = await esClient.indices.exists({ index: indexName }); if (!indexExists) { @@ -636,9 +478,6 @@ export const reindexServiceFactory = ( try { switch (lockedReindexOp.attributes.lastCompletedStep) { case ReindexStep.created: - lockedReindexOp = await stopIndexGroupServices(lockedReindexOp); - break; - case ReindexStep.indexGroupServicesStopped: lockedReindexOp = await setReadonly(lockedReindexOp); break; case ReindexStep.readonly: @@ -654,12 +493,10 @@ export const reindexServiceFactory = ( lockedReindexOp = await switchAlias(lockedReindexOp); break; case ReindexStep.aliasCreated: - lockedReindexOp = await resumeIndexGroupServices(lockedReindexOp); - break; - case ReindexStep.indexGroupServicesStarted: lockedReindexOp = await actions.updateReindexOp(lockedReindexOp, { status: ReindexStatus.completed, }); + break; default: break; } @@ -767,13 +604,3 @@ export const reindexServiceFactory = ( }, }; }; - -export const isMlIndex = (indexName: string) => { - const sourceName = sourceNameForIndex(indexName); - return ML_INDICES.indexOf(sourceName) >= 0; -}; - -export const isWatcherIndex = (indexName: string) => { - const sourceName = sourceNameForIndex(indexName); - return WATCHER_INDICES.indexOf(sourceName) >= 0; -}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts index c598da93388c..3491c92ef595 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts @@ -7,6 +7,7 @@ import { IClusterClient, Logger, SavedObjectsClientContract, FakeRequest } from 'src/core/server'; import moment from 'moment'; +import { SecurityPluginStart } from '../../../../security/server'; import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { Credential, CredentialStore } from './credential_store'; import { reindexActionsFactory } from './reindex_actions'; @@ -46,15 +47,19 @@ export class ReindexWorker { private inProgressOps: ReindexSavedObject[] = []; private readonly reindexService: ReindexService; private readonly log: Logger; + private readonly security: SecurityPluginStart; constructor( private client: SavedObjectsClientContract, private credentialStore: CredentialStore, private clusterClient: IClusterClient, log: Logger, - private licensing: LicensingPluginSetup + private licensing: LicensingPluginSetup, + security: SecurityPluginStart ) { this.log = log.get('reindex_worker'); + this.security = security; + if (ReindexWorker.workerSingleton) { throw new Error(`More than one ReindexWorker cannot be created.`); } @@ -171,7 +176,11 @@ export class ReindexWorker { firstOpInQueue.attributes.indexName ); // Re-associate the credentials - this.credentialStore.set(firstOpInQueue, credential); + this.credentialStore.update({ + reindexOp: firstOpInQueue, + security: this.security, + credential, + }); } } @@ -223,7 +232,7 @@ export class ReindexWorker { reindexOp = await swallowExceptions(service.processNextStep, this.log)(reindexOp); // Update credential store with most recent state. - this.credentialStore.set(reindexOp, credential); + this.credentialStore.update({ reindexOp, security: this.security, credential }); }; } diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts deleted file mode 100644 index caff78390b9d..000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ /dev/null @@ -1,48 +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 { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; - -import { upsertUIOpenOption } from './es_ui_open_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { - describe('Upsert UIOpen Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - - await upsertUIOpenOption({ - overview: true, - elasticsearch: true, - kibana: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.overview'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.elasticsearch'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.kibana'] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts deleted file mode 100644 index 3d463fe4b03e..000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ /dev/null @@ -1,57 +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 { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIOpen, - UIOpenOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIOpenDependencies { - uiOpenOptionCounter: UIOpenOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIOpenOptionCounter({ - savedObjects, - uiOpenOptionCounter, -}: IncrementUIOpenDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_open.${uiOpenOptionCounter}`, - ]); -} - -type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIOpenOption({ - overview, - elasticsearch, - savedObjects, - kibana, -}: UpsertUIOpenOptionDependencies): Promise { - if (overview) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' }); - } - - if (elasticsearch) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'elasticsearch' }); - } - - if (kibana) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'kibana' }); - } - - return { - overview, - elasticsearch, - kibana, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts deleted file mode 100644 index 6a05e8a697bb..000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; -import { upsertUIReindexOption } from './es_ui_reindex_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { - describe('Upsert UIReindex Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - await upsertUIReindexOption({ - close: true, - open: true, - start: true, - stop: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.close`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.open`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.start`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.stop`] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts deleted file mode 100644 index caee1a58a400..000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ /dev/null @@ -1,63 +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 { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIReindex, - UIReindexOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIReindexOptionDependencies { - uiReindexOptionCounter: UIReindexOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIReindexOptionCounter({ - savedObjects, - uiReindexOptionCounter, -}: IncrementUIReindexOptionDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_reindex.${uiReindexOptionCounter}`, - ]); -} - -type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIReindexOption({ - start, - close, - open, - stop, - savedObjects, -}: UpsertUIReindexOptionDepencies): Promise { - if (close) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'close' }); - } - - if (open) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'open' }); - } - - if (start) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'start' }); - } - - if (stop) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'stop' }); - } - - return { - close, - open, - start, - stop, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 50c5b358aa5c..34d329557f11 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -47,26 +47,6 @@ describe('Upgrade Assistant Usage Collector', () => { }; dependencies = { usageCollection, - savedObjects: { - createInternalRepository: jest.fn().mockImplementation(() => { - return { - get: () => { - return { - attributes: { - 'ui_open.overview': 10, - 'ui_open.elasticsearch': 20, - 'ui_open.kibana': 15, - 'ui_reindex.close': 1, - 'ui_reindex.open': 4, - 'ui_reindex.start': 2, - 'ui_reindex.stop': 1, - 'ui_reindex.not_defined': 1, - }, - }; - }, - }; - }), - }, elasticsearch: { client: clusterClient, }, @@ -91,17 +71,6 @@ describe('Upgrade Assistant Usage Collector', () => { callClusterStub ); expect(upgradeAssistantStats).toEqual({ - ui_open: { - overview: 10, - elasticsearch: 20, - kibana: 15, - }, - ui_reindex: { - close: 1, - open: 4, - start: 2, - stop: 1, - }, features: { deprecation_logging: { enabled: true, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 56932f5e54b0..c535cd14f104 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -5,43 +5,14 @@ * 2.0. */ -import { get } from 'lodash'; -import { - ElasticsearchClient, - ElasticsearchServiceStart, - ISavedObjectsRepository, - SavedObjectsServiceStart, -} from 'src/core/server'; +import { ElasticsearchClient, ElasticsearchServiceStart } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, - UpgradeAssistantTelemetry, - UpgradeAssistantTelemetrySavedObject, - UpgradeAssistantTelemetrySavedObjectAttributes, -} from '../../../common/types'; +import { UpgradeAssistantTelemetry } from '../../../common/types'; import { isDeprecationLogIndexingEnabled, isDeprecationLoggingEnabled, } from '../es_deprecation_logging_apis'; -async function getSavedObjectAttributesFromRepo( - savedObjectsRepository: ISavedObjectsRepository, - docType: string, - docID: string -) { - try { - return ( - await savedObjectsRepository.get( - docType, - docID - ) - ).attributes; - } catch (e) { - return null; - } -} - async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): Promise { try { const { body: loggerDeprecationCallResult } = await esClient.cluster.getSettings({ @@ -57,58 +28,14 @@ async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): } } -export async function fetchUpgradeAssistantMetrics( - { client: esClient }: ElasticsearchServiceStart, - savedObjects: SavedObjectsServiceStart -): Promise { - const savedObjectsRepository = savedObjects.createInternalRepository(); - const upgradeAssistantSOAttributes = await getSavedObjectAttributesFromRepo( - savedObjectsRepository, - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID - ); +export async function fetchUpgradeAssistantMetrics({ + client: esClient, +}: ElasticsearchServiceStart): Promise { const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue( esClient.asInternalUser ); - const getTelemetrySavedObject = ( - upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null - ): UpgradeAssistantTelemetrySavedObject => { - const defaultTelemetrySavedObject = { - ui_open: { - overview: 0, - elasticsearch: 0, - kibana: 0, - }, - ui_reindex: { - close: 0, - open: 0, - start: 0, - stop: 0, - }, - }; - - if (!upgradeAssistantTelemetrySavedObjectAttrs) { - return defaultTelemetrySavedObject; - } - - return { - ui_open: { - overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), - elasticsearch: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.elasticsearch', 0), - kibana: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.kibana', 0), - }, - ui_reindex: { - close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), - open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), - start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), - stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), - }, - } as UpgradeAssistantTelemetrySavedObject; - }; - return { - ...getTelemetrySavedObject(upgradeAssistantSOAttributes), features: { deprecation_logging: { enabled: deprecationLoggingStatusValue, @@ -119,14 +46,12 @@ export async function fetchUpgradeAssistantMetrics( interface Dependencies { elasticsearch: ElasticsearchServiceStart; - savedObjects: SavedObjectsServiceStart; usageCollection: UsageCollectionSetup; } export function registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects, }: Dependencies) { const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ @@ -143,34 +68,8 @@ export function registerUpgradeAssistantUsageCollector({ }, }, }, - ui_open: { - elasticsearch: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Elasticsearch deprecations.', - }, - }, - overview: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the Overview page.', - }, - }, - kibana: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Kibana deprecations', - }, - }, - }, - ui_reindex: { - close: { type: 'long' }, - open: { type: 'long' }, - start: { type: 'long' }, - stop: { type: 'long' }, - }, }, - fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch), }); usageCollection.registerCollector(upgradeAssistantUsageCollector); diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 870bd6b98566..717f03758f82 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -6,7 +6,6 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - import { Plugin, CoreSetup, @@ -16,10 +15,13 @@ import { SavedObjectsClient, SavedObjectsServiceStart, } from '../../../../src/core/server'; +import { SecurityPluginStart } from '../../security/server'; import { InfraPluginSetup } from '../../infra/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants'; import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; import { ReindexWorker } from './lib/reindexing'; @@ -32,7 +34,7 @@ import { reindexOperationSavedObjectType, mlSavedObjectType, } from './saved_object_types'; -import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX_PATTERN } from '../common/constants'; +import { handleEsError } from './shared_imports'; import { RouteDependencies } from './types'; @@ -41,6 +43,11 @@ interface PluginsSetup { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; infra: InfraPluginSetup; + security?: SecurityPluginSetup; +} + +interface PluginsStart { + security: SecurityPluginStart; } export class UpgradeAssistantServerPlugin implements Plugin { @@ -53,11 +60,12 @@ export class UpgradeAssistantServerPlugin implements Plugin { // Properties set at start private savedObjectsServiceStart?: SavedObjectsServiceStart; + private securityPluginStart?: SecurityPluginStart; private worker?: ReindexWorker; constructor({ logger, env }: PluginInitializerContext) { this.logger = logger.get(); - this.credentialStore = credentialStoreFactory(); + this.credentialStore = credentialStoreFactory(this.logger); this.kibanaVersion = env.packageInfo.version; } @@ -70,7 +78,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, savedObjects }: CoreSetup, - { usageCollection, features, licensing, infra }: PluginsSetup + { usageCollection, features, licensing, infra, security }: PluginsSetup ) { this.licensing = licensing; @@ -93,12 +101,12 @@ export class UpgradeAssistantServerPlugin implements Plugin { // We need to initialize the deprecation logs plugin so that we can // navigate from this app to the observability app using a source_id. - infra.defineInternalSourceConfiguration(DEPRECATION_LOGS_SOURCE_ID, { + infra?.defineInternalSourceConfiguration(DEPRECATION_LOGS_SOURCE_ID, { name: 'deprecationLogs', description: 'deprecation logs', logIndices: { type: 'index_name', - indexName: DEPRECATION_LOGS_INDEX_PATTERN, + indexName: DEPRECATION_LOGS_INDEX, }, logColumns: [ { timestampColumn: { id: 'timestampField' } }, @@ -119,6 +127,13 @@ export class UpgradeAssistantServerPlugin implements Plugin { } return this.savedObjectsServiceStart; }, + getSecurityPlugin: () => this.securityPluginStart, + lib: { + handleEsError, + }, + config: { + isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), + }, }; // Initialize version service with current kibana version @@ -127,18 +142,18 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerRoutes(dependencies, this.getWorker.bind(this)); if (usageCollection) { - getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { + getStartServices().then(([{ elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects: savedObjectsService, }); }); } } - start({ savedObjects, elasticsearch }: CoreStart) { + start({ savedObjects, elasticsearch }: CoreStart, { security }: PluginsStart) { this.savedObjectsServiceStart = savedObjects; + this.securityPluginStart = security; // The ReindexWorker uses a map of request headers that contain the authentication credentials // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not @@ -155,6 +170,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { savedObjects: new SavedObjectsClient( this.savedObjectsServiceStart.createInternalRepository() ), + security: this.securityPluginStart, }); this.worker.start(); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts index d3a36835d12b..c77f3a6661eb 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts @@ -8,6 +8,7 @@ export const createRequestMock = (opts?: { headers?: any; params?: Record; + query?: Record; body?: Record; }) => { return Object.assign({ headers: {} }, opts || {}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/app.ts b/x-pack/plugins/upgrade_assistant/server/routes/app.ts new file mode 100644 index 000000000000..682dc83410f8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/app.ts @@ -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 { API_BASE_PATH, DEPRECATION_LOGS_INDEX } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { Privileges } from '../shared_imports'; +import { RouteDependencies } from '../types'; + +const extractMissingPrivileges = ( + privilegesObject: { [key: string]: Record } = {} +): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (Object.values(privilegesObject[privilegeName]).some((e) => !e)) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export function registerAppRoutes({ + router, + lib: { handleEsError }, + config: { isSecurityEnabled }, +}: RouteDependencies) { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + index: [], + }, + }; + + if (!isSecurityEnabled()) { + return response.ok({ body: privilegesResult }); + } + + try { + const { + body: { has_all_requested: hasAllPrivileges, index }, + } = await client.asCurrentUser.security.hasPrivileges({ + body: { + index: [ + { + names: [DEPRECATION_LOGS_INDEX], + privileges: ['read'], + }, + ], + }, + }); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.index = extractMissingPrivileges(index); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + return response.ok({ body: privilegesResult }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts new file mode 100644 index 000000000000..5d3ab7c854e7 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts @@ -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 { API_BASE_PATH, CLOUD_SNAPSHOT_REPOSITORY } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerCloudBackupStatusRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { + // GET most recent Cloud snapshot + router.get( + { path: `${API_BASE_PATH}/cloud_backup_status`, validate: false }, + versionCheckHandlerWrapper(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; + + try { + const { + body: { snapshots }, + } = await clusterClient.asCurrentUser.snapshot.get({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: '_all', + ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. + // @ts-expect-error @elastic/elasticsearch "desc" is a new param + order: 'desc', + sort: 'start_time', + size: 1, + }); + + let isBackedUp = false; + let lastBackupTime; + + if (snapshots && snapshots[0]) { + isBackedUp = true; + lastBackupTime = snapshots![0].start_time; + } + + return response.ok({ + body: { + isBackedUp, + lastBackupTime, + }, + }); + } catch (error) { + return handleEsError({ error, response }); + } + }) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.ts new file mode 100644 index 000000000000..4ae1205d2dae --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerClusterUpgradeStatusRoutes({ router }: RouteDependencies) { + router.get( + { path: `${API_BASE_PATH}/cluster_upgrade_status`, validate: false }, + // We're just depending on the version check to return a 426. + // Otherwise we just return a 200. + versionCheckHandlerWrapper(async (context, request, response) => { + return response.ok(); + }) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index 1d51666dec3e..89d4e4cb398c 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -8,6 +8,7 @@ import { kibanaResponseFactory } from 'src/core/server'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; +import { handleEsError } from '../shared_imports'; jest.mock('../lib/es_version_precheck', () => ({ versionCheckHandlerWrapper: (a: any) => a, @@ -28,6 +29,7 @@ describe('deprecation logging API', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerDeprecationLoggingRoutes(routeDependencies); }); @@ -43,7 +45,7 @@ describe('deprecation logging API', () => { .getSettings as jest.Mock ).mockResolvedValue({ body: { - default: { + defaults: { cluster: { deprecation_indexing: { enabled: 'true' } }, }, }, @@ -65,7 +67,7 @@ describe('deprecation logging API', () => { ( routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .getSettings as jest.Mock - ).mockRejectedValue(new Error(`scary error!`)); + ).mockRejectedValue(new Error('scary error!')); await expect( routeDependencies.router.getHandler({ method: 'get', @@ -82,7 +84,7 @@ describe('deprecation logging API', () => { .putSettings as jest.Mock ).mockResolvedValue({ body: { - default: { + defaults: { logger: { deprecation: 'WARN' }, cluster: { deprecation_indexing: { enabled: 'true' } }, }, @@ -104,7 +106,7 @@ describe('deprecation logging API', () => { ( routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .putSettings as jest.Mock - ).mockRejectedValue(new Error(`scary error!`)); + ).mockRejectedValue(new Error('scary error!')); await expect( routeDependencies.router.getHandler({ method: 'put', @@ -113,4 +115,103 @@ describe('deprecation logging API', () => { ).rejects.toThrow('scary error!'); }); }); + + describe('GET /api/upgrade_assistant/deprecation_logging/count', () => { + const MOCK_FROM_DATE = '2021-08-23T07:32:34.782Z'; + + it('returns count of deprecations', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.indices.exists as jest.Mock + ).mockResolvedValue({ + body: true, + }); + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.count as jest.Mock + ).mockResolvedValue({ + body: { count: 10 }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging/count', + })( + routeHandlerContextMock, + createRequestMock({ query: { from: MOCK_FROM_DATE } }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ count: 10 }); + }); + + it('returns zero matches when deprecation logs index is not created', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.indices.exists as jest.Mock + ).mockResolvedValue({ + body: false, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging/count', + })( + routeHandlerContextMock, + createRequestMock({ query: { from: MOCK_FROM_DATE } }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ count: 0 }); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.indices.exists as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging/count', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('DELETE /api/upgrade_assistant/deprecation_logging/cache', () => { + it('returns ok if if the cache was deleted', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: 'ok', + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'delete', + pathPattern: '/api/upgrade_assistant/deprecation_logging/cache', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'DELETE', + path: '/_logging/deprecation_cache', + }); + expect(resp.payload).toEqual('ok'); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'delete', + pathPattern: '/api/upgrade_assistant/deprecation_logging/cache', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts index fb2a5b559e5a..5d7f0f67b0ca 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -14,8 +14,12 @@ import { } from '../lib/es_deprecation_logging_apis'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { RouteDependencies } from '../types'; +import { DEPRECATION_LOGS_INDEX } from '../../common/constants'; -export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) { +export function registerDeprecationLoggingRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/deprecation_logging`, @@ -31,8 +35,12 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - const result = await getDeprecationLoggingStatus(client); - return response.ok({ body: result }); + try { + const result = await getDeprecationLoggingStatus(client); + return response.ok({ body: result }); + } catch (error) { + return handleEsError({ error, response }); + } } ) ); @@ -56,10 +64,92 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - const { isEnabled } = request.body as { isEnabled: boolean }; - return response.ok({ - body: await setDeprecationLogging(client, isEnabled), - }); + try { + const { isEnabled } = request.body as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(client, isEnabled), + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + router.get( + { + path: `${API_BASE_PATH}/deprecation_logging/count`, + validate: { + query: schema.object({ + from: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const { body: indexExists } = await client.asCurrentUser.indices.exists({ + index: DEPRECATION_LOGS_INDEX, + }); + + if (!indexExists) { + return response.ok({ body: { count: 0 } }); + } + + const { body } = await client.asCurrentUser.count({ + index: DEPRECATION_LOGS_INDEX, + body: { + query: { + range: { + '@timestamp': { + gte: request.query.from, + }, + }, + }, + }, + }); + + return response.ok({ body: { count: body.count } }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + router.delete( + { + path: `${API_BASE_PATH}/deprecation_logging/cache`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + await client.asCurrentUser.transport.request({ + method: 'DELETE', + path: '/_logging/deprecation_cache', + }); + + return response.ok({ body: 'ok' }); + } catch (error) { + return handleEsError({ error, response }); + } } ) ); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts index bea74f116e0e..4047ce827acb 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts @@ -6,6 +6,8 @@ */ import { kibanaResponseFactory } from 'src/core/server'; + +import { handleEsError } from '../shared_imports'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; @@ -33,6 +35,7 @@ describe('ES deprecations API', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerESDeprecationRoutes(routeDependencies); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts index eb0ade26de76..98089e34bdca 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts @@ -11,9 +11,13 @@ import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { RouteDependencies } from '../types'; import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; import { reindexServiceFactory } from '../lib/reindexing'; -import { handleEsError } from '../shared_imports'; -export function registerESDeprecationRoutes({ router, licensing, log }: RouteDependencies) { +export function registerESDeprecationRoutes({ + router, + lib: { handleEsError }, + licensing, + log, +}: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/es_deprecations`, @@ -50,8 +54,8 @@ export function registerESDeprecationRoutes({ router, licensing, log }: RouteDep return response.ok({ body: status, }); - } catch (e) { - return handleEsError({ error: e, response }); + } catch (error) { + return handleEsError({ error, response }); } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts index 2f8cdd2aba80..995e3a46cef0 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts @@ -6,6 +6,8 @@ */ import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { handleEsError } from '../shared_imports'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; import { registerMlSnapshotRoutes } from './ml_snapshots'; @@ -26,6 +28,7 @@ describe('ML snapshots APIs', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerMlSnapshotRoutes(routeDependencies); }); @@ -172,6 +175,28 @@ describe('ML snapshots APIs', () => { }); }); + describe('GET /api/upgrade_assistant/ml_upgrade_mode', () => { + it('Retrieves ml upgrade mode', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml.info as jest.Mock + ).mockResolvedValue({ + body: { + upgrade_mode: true, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/ml_upgrade_mode', + })(routeHandlerContextMock, createRequestMock({}), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + mlUpgradeModeEnabled: true, + }); + }); + }); + describe('GET /api/upgrade_assistant/ml_snapshots/:jobId/:snapshotId', () => { it('returns "idle" status if saved object does not exist', async () => { ( diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts index 65e707339d67..fa6af0f5e422 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts @@ -11,7 +11,6 @@ import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server' import { API_BASE_PATH } from '../../common/constants'; import { MlOperation, ML_UPGRADE_OP_TYPE } from '../../common/types'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { handleEsError } from '../shared_imports'; import { RouteDependencies } from '../types'; const findMlOperation = async ( @@ -99,7 +98,7 @@ const verifySnapshotUpgrade = async ( } }; -export function registerMlSnapshotRoutes({ router }: RouteDependencies) { +export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: RouteDependencies) { // Upgrade ML model snapshot router.post( { @@ -147,8 +146,8 @@ export function registerMlSnapshotRoutes({ router }: RouteDependencies) { status: body.completed === true ? 'complete' : 'in_progress', }, }); - } catch (e) { - return handleEsError({ error: e, response }); + } catch (error) { + return handleEsError({ error, response }); } } ) @@ -301,6 +300,37 @@ export function registerMlSnapshotRoutes({ router }: RouteDependencies) { ) ); + // Get the ml upgrade mode + router.get( + { + path: `${API_BASE_PATH}/ml_upgrade_mode`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + try { + const { body: mlInfo } = await esClient.asCurrentUser.ml.info(); + + return response.ok({ + body: { + mlUpgradeModeEnabled: mlInfo.upgrade_mode, + }, + }); + } catch (e) { + return handleEsError({ error: e, response }); + } + } + ) + ); + // Delete ML model snapshot router.delete( { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index 332db1080569..b6c885037668 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -7,20 +7,27 @@ import { RouteDependencies } from '../types'; +import { registerAppRoutes } from './app'; +import { registerCloudBackupStatusRoutes } from './cloud_backup_status'; +import { registerClusterUpgradeStatusRoutes } from './cluster_upgrade_status'; +import { registerSystemIndicesMigrationRoutes } from './system_indices_migration'; import { registerESDeprecationRoutes } from './es_deprecations'; import { registerDeprecationLoggingRoutes } from './deprecation_logging'; -import { registerReindexIndicesRoutes } from './reindex_indices'; -import { registerTelemetryRoutes } from './telemetry'; +import { registerReindexIndicesRoutes, registerBatchReindexIndicesRoutes } from './reindex_indices'; import { registerUpdateSettingsRoute } from './update_index_settings'; import { registerMlSnapshotRoutes } from './ml_snapshots'; import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { + registerAppRoutes(dependencies); + registerCloudBackupStatusRoutes(dependencies); + registerClusterUpgradeStatusRoutes(dependencies); + registerSystemIndicesMigrationRoutes(dependencies); registerESDeprecationRoutes(dependencies); registerDeprecationLoggingRoutes(dependencies); registerReindexIndicesRoutes(dependencies, getWorker); - registerTelemetryRoutes(dependencies); + registerBatchReindexIndicesRoutes(dependencies, getWorker); registerUpdateSettingsRoute(dependencies); registerMlSnapshotRoutes(dependencies); // Route for cloud to retrieve the upgrade status for ES and Kibana diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts new file mode 100644 index 000000000000..961b63b30f4e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts @@ -0,0 +1,192 @@ +/* + * 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 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { licensingMock } from '../../../../licensing/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from '../__mocks__/routes.mock'; +import { createRequestMock } from '../__mocks__/request.mock'; +import { handleEsError } from '../../shared_imports'; + +const mockReindexService = { + hasRequiredPrivileges: jest.fn(), + detectReindexWarnings: jest.fn(), + getIndexGroup: jest.fn(), + createReindexOperation: jest.fn(), + findAllInProgressOperations: jest.fn(), + findReindexOperation: jest.fn(), + processNextStep: jest.fn(), + resumeReindexOperation: jest.fn(), + cancelReindexing: jest.fn(), +}; +jest.mock('../../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +jest.mock('../../lib/reindexing', () => { + return { + reindexServiceFactory: () => mockReindexService, + }; +}); + +import { credentialStoreFactory } from '../../lib/reindexing/credential_store'; +import { registerBatchReindexIndicesRoutes } from './batch_reindex_indices'; + +const logMock = loggingSystemMock.create().get(); + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. Business logic is tested + * more thoroughly in the es_migration_apis test. + */ +describe('reindex API', () => { + let routeDependencies: any; + let mockRouter: MockRouter; + + const credentialStore = credentialStoreFactory(logMock); + const worker = { + includes: jest.fn(), + forceRefresh: jest.fn(), + } as any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + credentialStore, + router: mockRouter, + licensing: licensingMock.createSetup(), + lib: { handleEsError }, + getSecurityPlugin: () => securityMock.createStart(), + }; + registerBatchReindexIndicesRoutes(routeDependencies, () => worker); + + mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); + mockReindexService.detectReindexWarnings.mockReset(); + mockReindexService.getIndexGroup.mockReset(); + mockReindexService.createReindexOperation.mockReset(); + mockReindexService.findAllInProgressOperations.mockReset(); + mockReindexService.findReindexOperation.mockReset(); + mockReindexService.processNextStep.mockReset(); + mockReindexService.resumeReindexOperation.mockReset(); + mockReindexService.cancelReindexing.mockReset(); + worker.includes.mockReset(); + worker.forceRefresh.mockReset(); + + // Reset the credentialMap + credentialStore.clear(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /api/upgrade_assistant/reindex/batch', () => { + const queueSettingsArg = { + enqueue: true, + }; + it('creates a collection of index operations', async () => { + mockReindexService.createReindexOperation + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex1' }, + }) + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex2' }, + }) + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex3' }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/batch', + })( + routeHandlerContextMock, + createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), + kibanaResponseFactory + ); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 1, + 'theIndex1', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 2, + 'theIndex2', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 3, + 'theIndex3', + queueSettingsArg + ); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ + errors: [], + enqueued: [ + { indexName: 'theIndex1' }, + { indexName: 'theIndex2' }, + { indexName: 'theIndex3' }, + ], + }); + }); + + it('gracefully handles partial successes', async () => { + mockReindexService.createReindexOperation + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex1' }, + }) + .mockRejectedValueOnce(new Error('oops!')); + + mockReindexService.hasRequiredPrivileges + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/batch', + })( + routeHandlerContextMock, + createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), + kibanaResponseFactory + ); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenCalledTimes(2); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 1, + 'theIndex1', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 2, + 'theIndex3', + queueSettingsArg + ); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ + errors: [ + { + indexName: 'theIndex2', + message: 'You do not have adequate privileges to reindex "theIndex2".', + }, + { indexName: 'theIndex3', message: 'oops!' }, + ], + enqueued: [{ indexName: 'theIndex1' }], + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts new file mode 100644 index 000000000000..62be9a1807aa --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts @@ -0,0 +1,133 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { errors } from '@elastic/elasticsearch'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { ReindexStatus } from '../../../common/types'; +import { versionCheckHandlerWrapper } from '../../lib/es_version_precheck'; +import { ReindexWorker } from '../../lib/reindexing'; +import { reindexActionsFactory } from '../../lib/reindexing/reindex_actions'; +import { sortAndOrderReindexOperations } from '../../lib/reindexing/op_utils'; +import { RouteDependencies } from '../../types'; +import { mapAnyErrorToKibanaHttpResponse } from './map_any_error_to_kibana_http_response'; +import { reindexHandler } from './reindex_handler'; +import { GetBatchQueueResponse, PostBatchResponse } from './types'; + +export function registerBatchReindexIndicesRoutes( + { + credentialStore, + router, + licensing, + log, + getSecurityPlugin, + lib: { handleEsError }, + }: RouteDependencies, + getWorker: () => ReindexWorker +) { + const BASE_PATH = `${API_BASE_PATH}/reindex`; + + // Get the current batch queue + router.get( + { + path: `${BASE_PATH}/batch/queue`, + validate: {}, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client: esClient }, + savedObjects, + }, + }, + request, + response + ) => { + const { client } = savedObjects; + const callAsCurrentUser = esClient.asCurrentUser; + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + try { + const inProgressOps = await reindexActions.findAllByStatus(ReindexStatus.inProgress); + const { queue } = sortAndOrderReindexOperations(inProgressOps); + const result: GetBatchQueueResponse = { + queue: queue.map((savedObject) => savedObject.attributes), + }; + return response.ok({ + body: result, + }); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); + } + return mapAnyErrorToKibanaHttpResponse(error); + } + } + ) + ); + + // Add indices for reindexing to the worker's batch + router.post( + { + path: `${BASE_PATH}/batch`, + validate: { + body: schema.object({ + indexNames: schema.arrayOf(schema.string()), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects: { client: savedObjectsClient }, + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + const { indexNames } = request.body; + const results: PostBatchResponse = { + enqueued: [], + errors: [], + }; + for (const indexName of indexNames) { + try { + const result = await reindexHandler({ + savedObjects: savedObjectsClient, + dataClient: esClient, + indexName, + log, + licensing, + request, + credentialStore, + reindexOptions: { + enqueue: true, + }, + security: getSecurityPlugin(), + }); + results.enqueued.push(result); + } catch (e) { + results.errors.push({ + indexName, + message: e.message, + }); + } + } + + if (results.errors.length < indexNames.length) { + // Kick the worker on this node to immediately pickup the batch. + getWorker().forceRefresh(); + } + + return response.ok({ body: results }); + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts new file mode 100644 index 000000000000..72d68fc132cb --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ElasticsearchServiceStart, + Logger, + SavedObjectsClient, +} from '../../../../../../src/core/server'; + +import { LicensingPluginSetup } from '../../../../licensing/server'; +import { SecurityPluginStart } from '../../../../security/server'; +import { ReindexWorker } from '../../lib/reindexing'; +import { CredentialStore } from '../../lib/reindexing/credential_store'; + +interface CreateReindexWorker { + logger: Logger; + elasticsearchService: ElasticsearchServiceStart; + credentialStore: CredentialStore; + savedObjects: SavedObjectsClient; + licensing: LicensingPluginSetup; + security: SecurityPluginStart; +} + +export function createReindexWorker({ + logger, + elasticsearchService, + credentialStore, + savedObjects, + licensing, + security, +}: CreateReindexWorker) { + const esClient = elasticsearchService.client; + return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing, security); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts index 97d8f495c16b..038f0c07c11f 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts @@ -5,4 +5,6 @@ * 2.0. */ -export { createReindexWorker, registerReindexIndicesRoutes } from './reindex_indices'; +export { createReindexWorker } from './create_reindex_worker'; +export { registerReindexIndicesRoutes } from './reindex_indices'; +export { registerBatchReindexIndicesRoutes } from './batch_reindex_indices'; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.ts new file mode 100644 index 000000000000..f36e52ffb0ea --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.ts @@ -0,0 +1,45 @@ +/* + * 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 '../../../../../../src/core/server'; + +import { + AccessForbidden, + CannotCreateIndex, + IndexNotFound, + MultipleReindexJobsFound, + ReindexAlreadyInProgress, + ReindexCannotBeCancelled, + ReindexTaskCannotBeDeleted, + ReindexTaskFailed, +} from '../../lib/reindexing/error_symbols'; +import { ReindexError } from '../../lib/reindexing/error'; + +export const mapAnyErrorToKibanaHttpResponse = (e: any) => { + if (e instanceof ReindexError) { + switch (e.symbol) { + case AccessForbidden: + return kibanaResponseFactory.forbidden({ body: e.message }); + case IndexNotFound: + return kibanaResponseFactory.notFound({ body: e.message }); + case CannotCreateIndex: + case ReindexTaskCannotBeDeleted: + throw e; + case ReindexTaskFailed: + // Bad data + return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); + case ReindexAlreadyInProgress: + case MultipleReindexJobsFound: + case ReindexCannotBeCancelled: + return kibanaResponseFactory.badRequest({ body: e.message }); + default: + // nothing matched + } + } + + throw e; +}; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts index fe9b95787b7d..d81dc8cec4c5 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts @@ -6,9 +6,15 @@ */ import { i18n } from '@kbn/i18n'; -import { IScopedClusterClient, Logger, SavedObjectsClientContract } from 'kibana/server'; +import { + IScopedClusterClient, + Logger, + SavedObjectsClientContract, + KibanaRequest, +} from 'kibana/server'; import { LicensingPluginSetup } from '../../../../licensing/server'; +import { SecurityPluginStart } from '../../../../security/server'; import { ReindexOperation, ReindexStatus } from '../../../common/types'; @@ -23,22 +29,24 @@ interface ReindexHandlerArgs { indexName: string; log: Logger; licensing: LicensingPluginSetup; - headers: Record; + request: KibanaRequest; credentialStore: CredentialStore; reindexOptions?: { enqueue?: boolean; }; + security?: SecurityPluginStart; } export const reindexHandler = async ({ credentialStore, dataClient, - headers, + request, indexName, licensing, log, savedObjects, reindexOptions, + security, }: ReindexHandlerArgs): Promise => { const callAsCurrentUser = dataClient.asCurrentUser; const reindexActions = reindexActionsFactory(savedObjects, callAsCurrentUser); @@ -62,7 +70,7 @@ export const reindexHandler = async ({ : await reindexService.createReindexOperation(indexName, reindexOptions); // Add users credentials for the worker to use - credentialStore.set(reindexOp, headers); + await credentialStore.set({ reindexOp, request, security }); return reindexOp.attributes; }; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts index 08d9995ee621..9fcff5748a98 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts @@ -6,14 +6,17 @@ */ import { kibanaResponseFactory } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { licensingMock } from '../../../../licensing/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; import { createMockRouter, MockRouter, routeHandlerContextMock } from '../__mocks__/routes.mock'; import { createRequestMock } from '../__mocks__/request.mock'; +import { handleEsError } from '../../shared_imports'; +import { errors as esErrors } from '@elastic/elasticsearch'; const mockReindexService = { hasRequiredPrivileges: jest.fn(), detectReindexWarnings: jest.fn(), - getIndexGroup: jest.fn(), createReindexOperation: jest.fn(), findAllInProgressOperations: jest.fn(), findReindexOperation: jest.fn(), @@ -31,10 +34,12 @@ jest.mock('../../lib/reindexing', () => { }; }); -import { IndexGroup, ReindexSavedObject, ReindexStatus } from '../../../common/types'; +import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { credentialStoreFactory } from '../../lib/reindexing/credential_store'; import { registerReindexIndicesRoutes } from './reindex_indices'; +const logMock = loggingSystemMock.create().get(); + /** * Since these route callbacks are so thin, these serve simply as integration tests * to ensure they're wired up to the lib functions correctly. Business logic is tested @@ -44,7 +49,7 @@ describe('reindex API', () => { let routeDependencies: any; let mockRouter: MockRouter; - const credentialStore = credentialStoreFactory(); + const credentialStore = credentialStoreFactory(logMock); const worker = { includes: jest.fn(), forceRefresh: jest.fn(), @@ -56,12 +61,13 @@ describe('reindex API', () => { credentialStore, router: mockRouter, licensing: licensingMock.createSetup(), + lib: { handleEsError }, + getSecurityPlugin: () => securityMock.createStart(), }; registerReindexIndicesRoutes(routeDependencies, () => worker); mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); mockReindexService.detectReindexWarnings.mockReset(); - mockReindexService.getIndexGroup.mockReset(); mockReindexService.createReindexOperation.mockReset(); mockReindexService.findAllInProgressOperations.mockReset(); mockReindexService.findReindexOperation.mockReset(); @@ -120,9 +126,11 @@ describe('reindex API', () => { ]); }); - it("returns null for both if reindex operation doesn't exist and index doesn't exist", async () => { + it('returns es errors', async () => { mockReindexService.findReindexOperation.mockResolvedValueOnce(null); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); + mockReindexService.detectReindexWarnings.mockRejectedValueOnce( + new esErrors.ResponseError({ statusCode: 404 } as any) + ); const resp = await routeDependencies.router.getHandler({ method: 'get', @@ -133,16 +141,12 @@ describe('reindex API', () => { kibanaResponseFactory ); - expect(resp.status).toEqual(200); - const data = resp.payload; - expect(data.reindexOp).toBeNull(); - expect(data.warnings).toBeNull(); + expect(resp.status).toEqual(404); }); - it('returns the indexGroup for ML indices', async () => { + it("returns null for both if reindex operation doesn't exist and index doesn't exist", async () => { mockReindexService.findReindexOperation.mockResolvedValueOnce(null); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce([]); - mockReindexService.getIndexGroup.mockReturnValue(IndexGroup.ml); + mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); const resp = await routeDependencies.router.getHandler({ method: 'get', @@ -155,7 +159,8 @@ describe('reindex API', () => { expect(resp.status).toEqual(200); const data = resp.payload; - expect(data.indexGroup).toEqual(IndexGroup.ml); + expect(data.reindexOp).toBeNull(); + expect(data.warnings).toBeNull(); }); }); @@ -269,111 +274,6 @@ describe('reindex API', () => { }); }); - describe('POST /api/upgrade_assistant/reindex/batch', () => { - const queueSettingsArg = { - enqueue: true, - }; - it('creates a collection of index operations', async () => { - mockReindexService.createReindexOperation - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex1' }, - }) - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex2' }, - }) - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex3' }, - }); - - const resp = await routeDependencies.router.getHandler({ - method: 'post', - pathPattern: '/api/upgrade_assistant/reindex/batch', - })( - routeHandlerContextMock, - createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), - kibanaResponseFactory - ); - - // It called create correctly - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 1, - 'theIndex1', - queueSettingsArg - ); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 2, - 'theIndex2', - queueSettingsArg - ); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 3, - 'theIndex3', - queueSettingsArg - ); - - // It returned the right results - expect(resp.status).toEqual(200); - const data = resp.payload; - expect(data).toEqual({ - errors: [], - enqueued: [ - { indexName: 'theIndex1' }, - { indexName: 'theIndex2' }, - { indexName: 'theIndex3' }, - ], - }); - }); - - it('gracefully handles partial successes', async () => { - mockReindexService.createReindexOperation - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex1' }, - }) - .mockRejectedValueOnce(new Error('oops!')); - - mockReindexService.hasRequiredPrivileges - .mockResolvedValueOnce(true) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true); - - const resp = await routeDependencies.router.getHandler({ - method: 'post', - pathPattern: '/api/upgrade_assistant/reindex/batch', - })( - routeHandlerContextMock, - createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), - kibanaResponseFactory - ); - - // It called create correctly - expect(mockReindexService.createReindexOperation).toHaveBeenCalledTimes(2); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 1, - 'theIndex1', - queueSettingsArg - ); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 2, - 'theIndex3', - queueSettingsArg - ); - - // It returned the right results - expect(resp.status).toEqual(200); - const data = resp.payload; - expect(data).toEqual({ - errors: [ - { - indexName: 'theIndex2', - message: 'You do not have adequate privileges to reindex "theIndex2".', - }, - { indexName: 'theIndex3', message: 'oops!' }, - ], - enqueued: [{ indexName: 'theIndex1' }], - }); - }); - }); - describe('POST /api/upgrade_assistant/reindex/{indexName}/cancel', () => { it('returns a 501', async () => { mockReindexService.cancelReindexing.mockResolvedValueOnce({}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index 5528c0847822..30f7c77cf73a 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -6,84 +6,25 @@ */ import { schema } from '@kbn/config-schema'; -import { API_BASE_PATH } from '../../../common/constants'; -import { - ElasticsearchServiceStart, - kibanaResponseFactory, - Logger, - SavedObjectsClient, -} from '../../../../../../src/core/server'; - -import { LicensingPluginSetup } from '../../../../licensing/server'; - -import { ReindexStatus } from '../../../common/types'; +import { errors } from '@elastic/elasticsearch'; +import { API_BASE_PATH } from '../../../common/constants'; import { versionCheckHandlerWrapper } from '../../lib/es_version_precheck'; import { reindexServiceFactory, ReindexWorker } from '../../lib/reindexing'; -import { CredentialStore } from '../../lib/reindexing/credential_store'; import { reindexActionsFactory } from '../../lib/reindexing/reindex_actions'; -import { sortAndOrderReindexOperations } from '../../lib/reindexing/op_utils'; -import { ReindexError } from '../../lib/reindexing/error'; import { RouteDependencies } from '../../types'; -import { - AccessForbidden, - CannotCreateIndex, - IndexNotFound, - MultipleReindexJobsFound, - ReindexAlreadyInProgress, - ReindexCannotBeCancelled, - ReindexTaskCannotBeDeleted, - ReindexTaskFailed, -} from '../../lib/reindexing/error_symbols'; - +import { mapAnyErrorToKibanaHttpResponse } from './map_any_error_to_kibana_http_response'; import { reindexHandler } from './reindex_handler'; -import { GetBatchQueueResponse, PostBatchResponse } from './types'; - -interface CreateReindexWorker { - logger: Logger; - elasticsearchService: ElasticsearchServiceStart; - credentialStore: CredentialStore; - savedObjects: SavedObjectsClient; - licensing: LicensingPluginSetup; -} - -export function createReindexWorker({ - logger, - elasticsearchService, - credentialStore, - savedObjects, - licensing, -}: CreateReindexWorker) { - const esClient = elasticsearchService.client; - return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing); -} - -const mapAnyErrorToKibanaHttpResponse = (e: any) => { - if (e instanceof ReindexError) { - switch (e.symbol) { - case AccessForbidden: - return kibanaResponseFactory.forbidden({ body: e.message }); - case IndexNotFound: - return kibanaResponseFactory.notFound({ body: e.message }); - case CannotCreateIndex: - case ReindexTaskCannotBeDeleted: - throw e; - case ReindexTaskFailed: - // Bad data - return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); - case ReindexAlreadyInProgress: - case MultipleReindexJobsFound: - case ReindexCannotBeCancelled: - return kibanaResponseFactory.badRequest({ body: e.message }); - default: - // nothing matched - } - } - throw e; -}; export function registerReindexIndicesRoutes( - { credentialStore, router, licensing, log }: RouteDependencies, + { + credentialStore, + router, + licensing, + log, + getSecurityPlugin, + lib: { handleEsError }, + }: RouteDependencies, getWorker: () => ReindexWorker ) { const BASE_PATH = `${API_BASE_PATH}/reindex`; @@ -117,8 +58,9 @@ export function registerReindexIndicesRoutes( indexName, log, licensing, - headers: request.headers, + request, credentialStore, + security: getSecurityPlugin(), }); // Kick the worker on this node to immediately pickup the new reindex operation. @@ -127,102 +69,12 @@ export function registerReindexIndicesRoutes( return response.ok({ body: result, }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); - } - } - ) - ); - - // Get the current batch queue - router.get( - { - path: `${BASE_PATH}/batch/queue`, - validate: {}, - }, - async ( - { - core: { - elasticsearch: { client: esClient }, - savedObjects, - }, - }, - request, - response - ) => { - const { client } = savedObjects; - const callAsCurrentUser = esClient.asCurrentUser; - const reindexActions = reindexActionsFactory(client, callAsCurrentUser); - try { - const inProgressOps = await reindexActions.findAllByStatus(ReindexStatus.inProgress); - const { queue } = sortAndOrderReindexOperations(inProgressOps); - const result: GetBatchQueueResponse = { - queue: queue.map((savedObject) => savedObject.attributes), - }; - return response.ok({ - body: result, - }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); - } - } - ); - - // Add indices for reindexing to the worker's batch - router.post( - { - path: `${BASE_PATH}/batch`, - validate: { - body: schema.object({ - indexNames: schema.arrayOf(schema.string()), - }), - }, - }, - versionCheckHandlerWrapper( - async ( - { - core: { - savedObjects: { client: savedObjectsClient }, - elasticsearch: { client: esClient }, - }, - }, - request, - response - ) => { - const { indexNames } = request.body; - const results: PostBatchResponse = { - enqueued: [], - errors: [], - }; - for (const indexName of indexNames) { - try { - const result = await reindexHandler({ - savedObjects: savedObjectsClient, - dataClient: esClient, - indexName, - log, - licensing, - headers: request.headers, - credentialStore, - reindexOptions: { - enqueue: true, - }, - }); - results.enqueued.push(result); - } catch (e) { - results.errors.push({ - indexName, - message: e.message, - }); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); } + return mapAnyErrorToKibanaHttpResponse(error); } - - if (results.errors.length < indexNames.length) { - // Kick the worker on this node to immediately pickup the batch. - getWorker().forceRefresh(); - } - - return response.ok({ body: results }); } ) ); @@ -261,18 +113,19 @@ export function registerReindexIndicesRoutes( const warnings = hasRequiredPrivileges ? await reindexService.detectReindexWarnings(indexName) : []; - const indexGroup = reindexService.getIndexGroup(indexName); return response.ok({ body: { reindexOp: reindexOp ? reindexOp.attributes : null, warnings, - indexGroup, hasRequiredPrivileges, }, }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); + } + return mapAnyErrorToKibanaHttpResponse(error); } } ) @@ -314,8 +167,12 @@ export function registerReindexIndicesRoutes( await reindexService.cancelReindexing(indexName); return response.ok({ body: { acknowledged: true } }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); + } + + return mapAnyErrorToKibanaHttpResponse(error); } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts index bd5299ad8a4f..e442d3b4fd11 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts @@ -6,6 +6,8 @@ */ import { kibanaResponseFactory } from 'src/core/server'; + +import { handleEsError } from '../shared_imports'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; import { registerUpgradeStatusRoute } from './status'; @@ -31,6 +33,7 @@ describe('Status API', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerUpgradeStatusRoute(routeDependencies); }); @@ -74,7 +77,7 @@ describe('Status API', () => { expect(resp.payload).toEqual({ readyForUpgrade: false, details: - 'You have 1 Elasticsearch deprecation issues and 1 Kibana deprecation issues that must be resolved before upgrading.', + 'You have 1 Elasticsearch deprecation issue and 1 Kibana deprecation issue that must be resolved before upgrading.', }); }); @@ -97,7 +100,7 @@ describe('Status API', () => { expect(resp.status).toEqual(200); expect(resp.payload).toEqual({ readyForUpgrade: true, - details: 'All deprecation issues have been resolved.', + details: 'All deprecation warnings have been resolved.', }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.ts index 1e0a0060de03..ce9bb2e1c55d 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.ts @@ -11,9 +11,11 @@ import { getESUpgradeStatus } from '../lib/es_deprecations_status'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { getKibanaUpgradeStatus } from '../lib/kibana_status'; import { RouteDependencies } from '../types'; -import { handleEsError } from '../shared_imports'; -export function registerUpgradeStatusRoute({ router }: RouteDependencies) { +/** + * Note that this route is primarily intended for consumption by Cloud. + */ +export function registerUpgradeStatusRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/status`, @@ -45,14 +47,14 @@ export function registerUpgradeStatusRoute({ router }: RouteDependencies) { return i18n.translate( 'xpack.upgradeAssistant.status.allDeprecationsResolvedMessage', { - defaultMessage: 'All deprecation issues have been resolved.', + defaultMessage: 'All deprecation warnings have been resolved.', } ); } return i18n.translate('xpack.upgradeAssistant.status.deprecationsUnresolvedMessage', { defaultMessage: - 'You have {esTotalCriticalDeps} Elasticsearch deprecation issues and {kibanaTotalCriticalDeps} Kibana deprecation issues that must be resolved before upgrading.', + 'You have {esTotalCriticalDeps} Elasticsearch deprecation {esTotalCriticalDeps, plural, one {issue} other {issues}} and {kibanaTotalCriticalDeps} Kibana deprecation {kibanaTotalCriticalDeps, plural, one {issue} other {issues}} that must be resolved before upgrading.', values: { esTotalCriticalDeps, kibanaTotalCriticalDeps }, }); }; @@ -63,8 +65,8 @@ export function registerUpgradeStatusRoute({ router }: RouteDependencies) { details: getStatusMessage(), }, }); - } catch (e) { - return handleEsError({ error: e, response }); + } catch (error) { + return handleEsError({ error, response }); } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts new file mode 100644 index 000000000000..910748661ac4 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts @@ -0,0 +1,148 @@ +/* + * 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 'src/core/server'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; +import { handleEsError } from '../shared_imports'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +import { registerSystemIndicesMigrationRoutes } from './system_indices_migration'; + +const mockedResponse = { + features: [ + { + feature_name: 'security', + minimum_index_version: '7.1.1', + migration_status: 'NO_MIGRATION_NEEDED', + indices: [ + { + index: '.security-7', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'kibana', + minimum_index_version: '7.1.2', + upgrade_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.kibana', + version: '7.1.2', + }, + ], + }, + ], + migration_status: 'MIGRATION_NEEDED', +}; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. + */ +describe('Migrate system indices API', () => { + let mockRouter: MockRouter; + let routeDependencies: any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + router: mockRouter, + lib: { handleEsError }, + }; + registerSystemIndicesMigrationRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /api/upgrade_assistant/system_indices_migration', () => { + it('returns system indices migration status', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: mockedResponse, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'GET', + path: '/_migration/system_features', + }); + expect(resp.payload).toEqual({ + ...mockedResponse, + features: mockedResponse.features.filter( + (feature) => feature.migration_status !== 'NO_MIGRATION_NEEDED' + ), + }); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('POST /api/upgrade_assistant/system_indices_migration', () => { + it('returns system indices migration status', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: mockedResponse, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'POST', + path: '/_migration/system_features', + }); + expect(resp.payload).toEqual(mockedResponse); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts new file mode 100644 index 000000000000..67f91aa08a07 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; +import { + getESSystemIndicesMigrationStatus, + startESSystemIndicesMigration, +} from '../lib/es_system_indices_migration'; + +export function registerSystemIndicesMigrationRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { + // GET status of the system indices migration + router.get( + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const status = await getESSystemIndicesMigrationStatus(client.asCurrentUser); + + return response.ok({ + body: { + ...status, + features: status.features.filter( + (feature) => feature.migration_status !== 'NO_MIGRATION_NEEDED' + ), + }, + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + // POST starts the system indices migration + router.post( + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const status = await startESSystemIndicesMigration(client.asCurrentUser); + + return response.ok({ + body: status, + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts deleted file mode 100644 index 578cceb70275..000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ /dev/null @@ -1,187 +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 { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/mocks'; -import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; -import { createRequestMock } from './__mocks__/request.mock'; - -jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ - upsertUIOpenOption: jest.fn(), -})); - -jest.mock('../lib/telemetry/es_ui_reindex_apis', () => ({ - upsertUIReindexOption: jest.fn(), -})); - -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { registerTelemetryRoutes } from './telemetry'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry API', () => { - let routeDependencies: any; - let mockRouter: MockRouter; - beforeEach(() => { - mockRouter = createMockRouter(); - routeDependencies = { - getSavedObjectsService: () => savedObjectsServiceMock.create(), - router: mockRouter, - }; - registerTelemetryRoutes(routeDependencies); - }); - afterEach(() => jest.clearAllMocks()); - - describe('PUT /api/upgrade_assistant/stats/ui_open', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - overview: true, - elasticsearch: false, - kibana: false, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ body: returnPayload }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - overview: true, - elasticsearch: true, - kibana: true, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: true, - elasticsearch: true, - kibana: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); - - describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - close: false, - open: false, - start: true, - stop: false, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - close: true, - open: true, - start: true, - stop: true, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - close: true, - open: true, - start: true, - stop: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - start: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts deleted file mode 100644 index d083b38c7c24..000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ /dev/null @@ -1,64 +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 { schema } from '@kbn/config-schema'; -import { API_BASE_PATH } from '../../common/constants'; -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { RouteDependencies } from '../types'; - -export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { - router.put( - { - path: `${API_BASE_PATH}/stats/ui_open`, - validate: { - body: schema.object({ - overview: schema.boolean({ defaultValue: false }), - elasticsearch: schema.boolean({ defaultValue: false }), - kibana: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { elasticsearch, overview, kibana } = request.body; - return response.ok({ - body: await upsertUIOpenOption({ - savedObjects: getSavedObjectsService(), - elasticsearch, - overview, - kibana, - }), - }); - } - ); - - router.put( - { - path: `${API_BASE_PATH}/stats/ui_reindex`, - validate: { - body: schema.object({ - close: schema.boolean({ defaultValue: false }), - open: schema.boolean({ defaultValue: false }), - start: schema.boolean({ defaultValue: false }), - stop: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { close, open, start, stop } = request.body; - return response.ok({ - body: await upsertUIReindexOption({ - savedObjects: getSavedObjectsService(), - close, - open, - start, - stop, - }), - }); - } - ); -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/index.ts similarity index 74% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/index.ts rename to x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/index.ts index 185ec5f2540c..5e6e379bd9b2 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KibanaDeprecationStats } from './kibana_stats'; +export { telemetrySavedObjectMigrations } from './telemetry_saved_object_migrations'; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.ts new file mode 100644 index 000000000000..e1250ee0ebfe --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { telemetrySavedObjectMigrations } from './telemetry_saved_object_migrations'; + +describe('Telemetry saved object migration', () => { + describe('7.16.0', () => { + test('removes ui_open and ui_reindex attributes while preserving other attributes', () => { + const doc = { + type: 'upgrade-assistant-telemetry', + id: 'upgrade-assistant-telemetry', + attributes: { + 'test.property': 5, + 'ui_open.cluster': 1, + 'ui_open.indices': 1, + 'ui_open.overview': 1, + 'ui_reindex.close': 1, + 'ui_reindex.open': 1, + 'ui_reindex.start': 1, + 'ui_reindex.stop': 1, + }, + references: [], + updated_at: '2021-09-29T21:17:17.410Z', + migrationVersion: {}, + }; + + expect(telemetrySavedObjectMigrations['7.16.0'](doc)).toStrictEqual({ + type: 'upgrade-assistant-telemetry', + id: 'upgrade-assistant-telemetry', + attributes: { 'test.property': 5 }, + references: [], + updated_at: '2021-09-29T21:17:17.410Z', + migrationVersion: {}, + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts new file mode 100644 index 000000000000..88540d67b13d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts @@ -0,0 +1,40 @@ +/* + * 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 { get, omit, flow, some } from 'lodash'; +import type { SavedObjectMigrationFn } from 'kibana/server'; + +const v716RemoveUnusedTelemetry: SavedObjectMigrationFn = (doc) => { + // Dynamically defined in 6.7 (https://github.com/elastic/kibana/pull/28878) + // and then statically defined in 7.8 (https://github.com/elastic/kibana/pull/64332). + const attributesBlocklist = [ + 'ui_open.cluster', + 'ui_open.indices', + 'ui_open.overview', + 'ui_reindex.close', + 'ui_reindex.open', + 'ui_reindex.start', + 'ui_reindex.stop', + ]; + + const isDocEligible = some(attributesBlocklist, (attribute: string) => { + return get(doc, 'attributes', attribute); + }); + + if (isDocEligible) { + return { + ...doc, + attributes: omit(doc.attributes, attributesBlocklist), + }; + } + + return doc; +}; + +export const telemetrySavedObjectMigrations = { + '7.16.0': flow(v716RemoveUnusedTelemetry), +}; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts index 42d5d339dd05..43cf6c30fcca 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts @@ -7,50 +7,15 @@ import { SavedObjectsType } from 'src/core/server'; -import { UPGRADE_ASSISTANT_TYPE } from '../../common/types'; +import { UPGRADE_ASSISTANT_TELEMETRY } from '../../common/constants'; +import { telemetrySavedObjectMigrations } from './migrations'; export const telemetrySavedObjectType: SavedObjectsType = { - name: UPGRADE_ASSISTANT_TYPE, + name: UPGRADE_ASSISTANT_TELEMETRY, hidden: false, namespaceType: 'agnostic', mappings: { properties: { - ui_open: { - properties: { - overview: { - type: 'long', - null_value: 0, - }, - elasticsearch: { - type: 'long', - null_value: 0, - }, - kibana: { - type: 'long', - null_value: 0, - }, - }, - }, - ui_reindex: { - properties: { - close: { - type: 'long', - null_value: 0, - }, - open: { - type: 'long', - null_value: 0, - }, - start: { - type: 'long', - null_value: 0, - }, - stop: { - type: 'long', - null_value: 0, - }, - }, - }, features: { properties: { deprecation_logging: { @@ -65,4 +30,5 @@ export const telemetrySavedObjectType: SavedObjectsType = { }, }, }, + migrations: telemetrySavedObjectMigrations, }; diff --git a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts index 7f55d189457c..1c43f89469ac 100644 --- a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts @@ -6,3 +6,4 @@ */ export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export type { Privileges } from '../../../../src/plugins/es_ui_shared/common'; diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts index b25b73070e4c..376514c59d49 100644 --- a/x-pack/plugins/upgrade_assistant/server/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -6,13 +6,22 @@ */ import { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server'; -import { CredentialStore } from './lib/reindexing/credential_store'; import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginStart } from '../../security/server'; +import { CredentialStore } from './lib/reindexing/credential_store'; +import { handleEsError } from './shared_imports'; export interface RouteDependencies { router: IRouter; credentialStore: CredentialStore; log: Logger; getSavedObjectsService: () => SavedObjectsServiceStart; + getSecurityPlugin: () => SecurityPluginStart | undefined; licensing: LicensingPluginSetup; + lib: { + handleEsError: typeof handleEsError; + }; + config: { + isSecurityEnabled: () => boolean; + }; } diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 39d7404ebea9..4336acb77c2e 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -7,6 +7,7 @@ "declarationMap": true }, "include": [ + "../../../typings/**/*", "__jest__/**/*", "common/**/*", "public/**/*", diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts index 1674948fef32..2c0e81a6fb83 100644 --- a/x-pack/test/accessibility/apps/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts @@ -5,80 +5,199 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FtrProviderContext } from '../ftr_provider_context'; +const translogSettingsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'deprecated_settings', + body: { + settings: { + 'translog.retention.size': '1b', + 'translog.retention.age': '5m', + 'index.soft_deletes.enabled': true, + }, + }, +}; + +const multiFieldsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'nested_multi_fields', + body: { + mappings: { + properties: { + text: { + type: 'text', + fields: { + english: { + type: 'text', + analyzer: 'english', + fields: { + english: { + type: 'text', + analyzer: 'english', + }, + }, + }, + }, + }, + }, + }, + }, +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['upgradeAssistant', 'common']); const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const es = getService('es'); + const log = getService('log'); describe.skip('Upgrade Assistant', () => { before(async () => { await PageObjects.upgradeAssistant.navigateToPage(); + try { + // Create two indices that will trigger deprecation warnings to test the ES deprecations page + await es.indices.create(multiFieldsIndexDeprecation); + await es.indices.create(translogSettingsIndexDeprecation); + } catch (e) { + log.debug('[Setup error] Error creating indices'); + throw e; + } }); - // These tests will be skipped until the last minor of the next major release - describe('Upgrade Assistant content', () => { - it('Overview page', async () => { - await retry.waitFor('Upgrade Assistant overview page to be visible', async () => { - return testSubjects.exists('overviewPageContent'); + after(async () => { + try { + await es.indices.delete({ + index: [multiFieldsIndexDeprecation.index, translogSettingsIndexDeprecation.index], }); - await a11y.testAppSnapshot(); + } catch (e) { + log.debug('[Cleanup error] Error deleting indices'); + throw e; + } + }); + + describe('Upgrade Assistant - Overview', () => { + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + + try { + // Create two indices that will trigger deprecation warnings to test the ES deprecations page + await es.indices.create(multiFieldsIndexDeprecation); + await es.indices.create(translogSettingsIndexDeprecation); + } catch (e) { + log.debug('[Setup error] Error creating indices'); + throw e; + } + }); + + after(async () => { + try { + await es.indices.delete({ + index: [multiFieldsIndexDeprecation.index, translogSettingsIndexDeprecation.index], + }); + } catch (e) { + log.debug('[Cleanup error] Error deleting indices'); + throw e; + } }); - it('Elasticsearch cluster deprecations', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'stack/upgrade_assistant/es_deprecations/cluster', - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - } - ); - - await retry.waitFor('Cluster tab to be visible', async () => { - return testSubjects.exists('clusterTabContent'); + describe('Overview page', () => { + beforeEach(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + await retry.waitFor('Upgrade Assistant overview page to be visible', async () => { + return testSubjects.exists('overview'); + }); + }); + + it('with logs collection disabled', async () => { + await a11y.testAppSnapshot(); }); - await a11y.testAppSnapshot(); + it('with logs collection enabled', async () => { + await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle(); + + await retry.waitFor('UA external links title to be present', async () => { + return testSubjects.isDisplayed('externalLinksTitle'); + }); + + await a11y.testAppSnapshot(); + }); }); - it('Elasticsearch index deprecations', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'stack/upgrade_assistant/es_deprecations/indices', - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - } - ); - - await retry.waitFor('Indices tab to be visible', async () => { - return testSubjects.exists('indexTabContent'); + describe('Elasticsearch deprecations page', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrl( + 'management', + 'stack/upgrade_assistant/es_deprecations', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + } + ); + + await retry.waitFor('Elasticsearch deprecations table to be visible', async () => { + return testSubjects.exists('esDeprecationsTable'); + }); + }); + + it('Deprecations table', async () => { + await a11y.testAppSnapshot(); }); - await a11y.testAppSnapshot(); + it('Index settings deprecation flyout', async () => { + await PageObjects.upgradeAssistant.clickEsDeprecation( + 'indexSettings' // An index setting deprecation was added in the before() hook so should be guaranteed + ); + await retry.waitFor('ES index settings deprecation flyout to be visible', async () => { + return testSubjects.exists('indexSettingsDetails'); + }); + await a11y.testAppSnapshot(); + }); + + it('Default deprecation flyout', async () => { + await PageObjects.upgradeAssistant.clickEsDeprecation( + 'default' // A default deprecation was added in the before() hook so should be guaranteed + ); + await retry.waitFor('ES default deprecation flyout to be visible', async () => { + return testSubjects.exists('defaultDeprecationDetails'); + }); + await a11y.testAppSnapshot(); + }); }); - it('Kibana deprecations', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'stack/upgrade_assistant/kibana_deprecations', - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - } - ); - - await retry.waitFor('Kibana deprecations to be visible', async () => { - return testSubjects.exists('kibanaDeprecationsContent'); + describe('Kibana deprecations page', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrl( + 'management', + 'stack/upgrade_assistant/kibana_deprecations', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + } + ); + + await retry.waitFor('Kibana deprecations to be visible', async () => { + return testSubjects.exists('kibanaDeprecations'); + }); + }); + + it('Deprecations table', async () => { + await a11y.testAppSnapshot(); }); - await a11y.testAppSnapshot(); + it('Deprecation details flyout', async () => { + await PageObjects.upgradeAssistant.clickKibanaDeprecation( + 'xpack.securitySolution has a deprecated setting' // This deprecation was added to the test runner config so should be guaranteed + ); + + await retry.waitFor('Kibana deprecation details flyout to be visible', async () => { + return testSubjects.exists('kibanaDeprecationDetails'); + }); + + await a11y.testAppSnapshot(); + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts new file mode 100644 index 000000000000..b1a4d7e8b047 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts @@ -0,0 +1,93 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + + const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots'; + + const createCloudRepository = () => { + return es.snapshot.createRepository({ + name: CLOUD_SNAPSHOT_REPOSITORY, + body: { + type: 'fs', + settings: { + location: '/tmp/cloud-snapshots/', + }, + }, + verify: false, + }); + }; + + const createCloudSnapshot = (snapshotName: string) => { + return es.snapshot.create({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: snapshotName, + wait_for_completion: true, + // Configure snapshot so no indices are captured, so the request completes ASAP. + body: { + indices: 'this_index_doesnt_exist', + ignore_unavailable: true, + include_global_state: false, + }, + }); + }; + + const deleteCloudSnapshot = (snapshotName: string) => { + return es.snapshot.delete({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: snapshotName, + }); + }; + + describe('Cloud backup status', () => { + describe('get', () => { + describe('with backups present', () => { + // Needs SnapshotInfo type https://github.com/elastic/elasticsearch-specification/issues/685 + let mostRecentSnapshot: any; + + before(async () => { + await createCloudRepository(); + await createCloudSnapshot('test_snapshot_1'); + mostRecentSnapshot = (await createCloudSnapshot('test_snapshot_2')).snapshot; + }); + + after(async () => { + await deleteCloudSnapshot('test_snapshot_1'); + await deleteCloudSnapshot('test_snapshot_2'); + }); + + it('returns status based on most recent snapshot', async () => { + const { body: cloudBackupStatus } = await supertest + .get('/api/upgrade_assistant/cloud_backup_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(cloudBackupStatus.isBackedUp).to.be(true); + expect(cloudBackupStatus.lastBackupTime).to.be(mostRecentSnapshot.start_time); + }); + }); + + describe('without backups present', () => { + it('returns not-backed-up status', async () => { + const { body: cloudBackupStatus } = await supertest + .get('/api/upgrade_assistant/cloud_backup_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(cloudBackupStatus.isBackedUp).to.be(false); + expect(cloudBackupStatus.lastBackupTime).to.be(undefined); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts new file mode 100644 index 000000000000..aea003a31796 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts @@ -0,0 +1,41 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const security = getService('security'); + + describe('Elasticsearch deprecations', () => { + describe('GET /api/upgrade_assistant/es_deprecations', () => { + it('handles auth error', async () => { + const ROLE_NAME = 'authErrorRole'; + const USER_NAME = 'authErrorUser'; + const USER_PASSWORD = 'password'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + await supertestWithoutAuth + .get('/api/upgrade_assistant/es_deprecations') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(403); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts index 466d44ca460a..f6b231f03881 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts @@ -10,5 +10,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', () => { loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./cloud_backup_status')); + loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./es_deprecations')); }); } diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts new file mode 100644 index 000000000000..c5c00c9a3368 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts @@ -0,0 +1,58 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { DEPRECATION_LOGS_INDEX } from '../../../../plugins/upgrade_assistant/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + const security = getService('security'); + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('Privileges', () => { + describe('GET /api/upgrade_assistant/privileges', () => { + it('User with with index privileges', async () => { + const { body } = await supertest + .get('/api/upgrade_assistant/privileges') + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(body.hasAllPrivileges).to.be(true); + expect(body.missingPrivileges.index.length).to.be(0); + }); + + it('User without index privileges', async () => { + const ROLE_NAME = 'test_role'; + const USER_NAME = 'test_user'; + const USER_PASSWORD = 'test_user'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + const { body } = await supertestWithoutAuth + .get('/api/upgrade_assistant/privileges') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(200); + + expect(body.hasAllPrivileges).to.be(false); + expect(body.missingPrivileges.index[0]).to.be(DEPRECATION_LOGS_INDEX); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 7740f612bb11..e2c2e0b52dfd 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -43,7 +43,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi serverArgs: [ ...xPackFunctionalTestsConfig.get('esTestCluster.serverArgs'), 'node.attr.name=apiIntegrationTestNode', - 'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2', + 'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2,/tmp/cloud-snapshots/', ], }, }; diff --git a/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts new file mode 100644 index 000000000000..3024f8a5a720 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts @@ -0,0 +1,105 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const multiFieldsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'nested_multi_fields', + body: { + mappings: { + properties: { + text: { + type: 'text', + fields: { + english: { + type: 'text', + analyzer: 'english', + fields: { + english: { + type: 'text', + analyzer: 'english', + }, + }, + }, + }, + }, + }, + }, + }, +}; + +const translogSettingsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'deprecated_settings', + body: { + settings: { + 'translog.retention.size': '1b', + 'translog.retention.age': '5m', + 'index.soft_deletes.enabled': true, + }, + }, +}; + +export default function upgradeAssistantFunctionalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + const security = getService('security'); + const log = getService('log'); + + describe.skip('Deprecation pages', function () { + this.tags('skipFirefox'); + + before(async () => { + await security.testUser.setRoles(['global_upgrade_assistant_role']); + + try { + // Create two indices that will trigger deprecation warnings to test the ES deprecations page + await es.indices.create(multiFieldsIndexDeprecation); + await es.indices.create(translogSettingsIndexDeprecation); + } catch (e) { + log.debug('[Setup error] Error creating indices'); + throw e; + } + }); + + after(async () => { + try { + await es.indices.delete({ + index: [multiFieldsIndexDeprecation.index, translogSettingsIndexDeprecation.index], + }); + } catch (e) { + log.debug('[Cleanup error] Error deleting indices'); + throw e; + } + + await security.testUser.restoreDefaults(); + }); + + it('renders the Elasticsearch deprecations page', async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + await PageObjects.upgradeAssistant.clickEsDeprecationsPanel(); + + await retry.waitFor('Elasticsearch deprecations table to be visible', async () => { + return testSubjects.exists('esDeprecationsTable'); + }); + }); + + it('renders the Kibana deprecations page', async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + await PageObjects.upgradeAssistant.clickKibanaDeprecationsPanel(); + + await retry.waitFor('Kibana deprecations table to be visible', async () => { + return testSubjects.exists('kibanaDeprecations'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts index 7aa8bfe4eff6..dca3391ae546 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'settings', 'security']); const appsMenu = getService('appsMenu'); @@ -17,14 +16,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe.skip('security', function () { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.common.navigateToApp('home'); }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - }); - describe('global all privileges (aka kibana_admin)', () => { before(async () => { await security.testUser.setRoles(['kibana_admin'], true); diff --git a/x-pack/test/functional/apps/upgrade_assistant/index.ts b/x-pack/test/functional/apps/upgrade_assistant/index.ts index c25e0af41439..d99d1cd03332 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/index.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/index.ts @@ -8,10 +8,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function upgradeCheckup({ loadTestFile }: FtrProviderContext) { - describe('Upgrade checkup ', function upgradeAssistantTestSuite() { + describe('Upgrade Assistant', function upgradeAssistantTestSuite() { this.tags('ciGroup4'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./deprecation_pages')); + loadTestFile(require.resolve('./overview_page')); }); } diff --git a/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts b/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts new file mode 100644 index 000000000000..0b8d15695689 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts @@ -0,0 +1,77 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function upgradeAssistantOverviewPageFunctionalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const retry = getService('retry'); + const security = getService('security'); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + + describe.skip('Overview Page', function () { + this.tags('skipFirefox'); + + before(async () => { + await security.testUser.setRoles(['superuser']); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + beforeEach(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('shows coming soon prompt', async () => { + await retry.waitFor('Upgrade Assistant overview page to be visible', async () => { + return testSubjects.exists('comingSoonPrompt'); + }); + }); + + it('Should render all steps', async () => { + testSubjects.exists('backupStep-incomplete'); + testSubjects.exists('fixIssuesStep-incomplete'); + testSubjects.exists('fixLogsStep-incomplete'); + testSubjects.exists('upgradeStep'); + }); + + describe('fixLogsStep', () => { + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + // Access to system indices will be deprecated and should generate a deprecation log + await es.indices.get({ index: '.kibana' }); + // Only click deprecation logging toggle if its not already enabled + if (!(await testSubjects.isDisplayed('externalLinksTitle'))) { + await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle(); + } + + await retry.waitFor('UA external links title to be present', async () => { + return testSubjects.isDisplayed('externalLinksTitle'); + }); + }); + + beforeEach(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Shows warnings callout if there are deprecations', async () => { + testSubjects.exists('hasWarningsCallout'); + }); + + it('Shows no warnings callout if there are no deprecations', async () => { + await PageObjects.upgradeAssistant.clickResetLastCheckpointButton(); + testSubjects.exists('noWarningsCallout'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts deleted file mode 100644 index 93475c228ed2..000000000000 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ /dev/null @@ -1,79 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function upgradeAssistantFunctionalTests({ - getService, - getPageObjects, -}: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['upgradeAssistant', 'common']); - const security = getService('security'); - const log = getService('log'); - const retry = getService('retry'); - - // Updated for the hiding of the UA UI. - describe.skip('Upgrade Checkup', function () { - this.tags('skipFirefox'); - - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); - await security.testUser.setRoles(['global_upgrade_assistant_role']); - }); - - after(async () => { - await PageObjects.upgradeAssistant.waitForTelemetryHidden(); - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - await security.testUser.restoreDefaults(); - }); - - it.skip('allows user to navigate to upgrade checkup', async () => { - await PageObjects.upgradeAssistant.navigateToPage(); - }); - - it.skip('allows user to toggle deprecation logging', async () => { - log.debug('expect initial state to be ON'); - expect(await PageObjects.upgradeAssistant.deprecationLoggingEnabledLabel()).to.be('On'); - expect(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled()).to.be(true); - - await retry.try(async () => { - log.debug('Now toggle to off'); - await PageObjects.upgradeAssistant.toggleDeprecationLogging(); - - log.debug('expect state to be OFF after toggle'); - expect(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled()).to.be(false); - expect(await PageObjects.upgradeAssistant.deprecationLoggingEnabledLabel()).to.be('Off'); - }); - - log.debug('Now toggle back on.'); - await retry.try(async () => { - await PageObjects.upgradeAssistant.toggleDeprecationLogging(); - log.debug('expect state to be ON after toggle'); - expect(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled()).to.be(true); - expect(await PageObjects.upgradeAssistant.deprecationLoggingEnabledLabel()).to.be('On'); - }); - }); - - it.skip('allows user to open cluster tab', async () => { - await PageObjects.upgradeAssistant.navigateToPage(); - await PageObjects.upgradeAssistant.clickTab('cluster'); - expect(await PageObjects.upgradeAssistant.issueSummaryText()).to.be( - 'You have no cluster issues.' - ); - }); - - it.skip('allows user to open indices tab', async () => { - await PageObjects.upgradeAssistant.navigateToPage(); - await PageObjects.upgradeAssistant.clickTab('indices'); - expect(await PageObjects.upgradeAssistant.issueSummaryText()).to.be( - 'You have no index issues.' - ); - }); - }); -} diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts index 211bcbbd5921..54d7f3d45212 100644 --- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts +++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @@ -11,7 +11,6 @@ export class UpgradeAssistantPageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); private readonly log = this.ctx.getService('log'); private readonly browser = this.ctx.getService('browser'); - private readonly find = this.ctx.getService('find'); private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly common = this.ctx.getPageObject('common'); @@ -30,47 +29,57 @@ export class UpgradeAssistantPageObject extends FtrService { }); } - async toggleDeprecationLogging() { - this.log.debug('toggleDeprecationLogging()'); - await this.testSubjects.click('upgradeAssistantDeprecationToggle'); + async clickEsDeprecationsPanel() { + return await this.retry.try(async () => { + await this.testSubjects.click('esStatsPanel'); + }); } - async isDeprecationLoggingEnabled() { - const isDeprecationEnabled = await this.testSubjects.getAttribute( - 'upgradeAssistantDeprecationToggle', - 'aria-checked' - ); - this.log.debug(`Deprecation enabled == ${isDeprecationEnabled}`); - return isDeprecationEnabled === 'true'; + async clickDeprecationLoggingToggle() { + return await this.retry.try(async () => { + await this.testSubjects.click('deprecationLoggingToggle'); + }); } - async deprecationLoggingEnabledLabel() { - const loggingEnabledLabel = await this.find.byCssSelector( - '[data-test-subj="upgradeAssistantDeprecationToggle"] ~ span' - ); - return await loggingEnabledLabel.getVisibleText(); + async clickResetLastCheckpointButton() { + return await this.retry.try(async () => { + await this.testSubjects.click('resetLastStoredDate'); + }); } - async clickTab(tabId: string) { + async clickKibanaDeprecationsPanel() { return await this.retry.try(async () => { - this.log.debug('clickTab()'); - await this.find.clickByCssSelector(`.euiTabs .euiTab#${tabId}`); + await this.testSubjects.click('kibanaStatsPanel'); }); } - async waitForTelemetryHidden() { - const self = this; - await this.retry.waitFor('Telemetry to disappear.', async () => { - return (await self.isTelemetryExists()) === false; + async clickKibanaDeprecation(selectedIssue: string) { + const table = await this.testSubjects.find('kibanaDeprecationsTable'); + const rows = await table.findAllByTestSubject('row'); + + const selectedRow = rows.find(async (row) => { + const issue = await (await row.findByTestSubject('issueCell')).getVisibleText(); + return issue === selectedIssue; }); - } - async issueSummaryText() { - this.log.debug('expectIssueSummary()'); - return await this.testSubjects.getVisibleText('upgradeAssistantIssueSummary'); + if (selectedRow) { + const issueLink = await selectedRow.findByTestSubject('deprecationDetailsLink'); + await issueLink.click(); + } else { + this.log.debug('Unable to find selected deprecation row'); + } } - async isTelemetryExists() { - return await this.testSubjects.exists('upgradeAssistantTelemetryRunning'); + async clickEsDeprecation(deprecationType: 'indexSettings' | 'default' | 'reindex' | 'ml') { + const table = await this.testSubjects.find('esDeprecationsTable'); + const deprecationIssueLink = await ( + await table.findByTestSubject(`${deprecationType}TableCell-message`) + ).findByCssSelector('button'); + + if (deprecationIssueLink) { + await deprecationIssueLink.click(); + } else { + this.log.debug('Unable to find selected deprecation'); + } } }