[Upgrade Assistant] Forwardport from 7.x (#114966)

* Fix link to Cloud deployment URL in upgrade step. (#109528)

* [Upgrade Assistant] Refactor CITs

* Rename UA steps to fix_issues_step and fix_logs_step. (#109526)

* Rename tests accordingly.

* [Upgrade Assistant] Cleanup scss (#109524)

* [Upgrade Assistant] Update readme (#109502)

* Add "Back up data" step to UA (#109543)

* Add backup step with static content and link to Snapshot and Restore.
* Add snapshot_restore locator.
* Remove unnecessary describe block from Upgrade Step tests.
* Remove unused render_app.tsx.

* Change copy references of 'deprecation issues' to 'deprecation warnings'. (#109963)

* [Upgrade Assistant] Address design feedback for ES deprecations page (#109726)

* [Upgrade Assistant] Add checkpoint feature to Overview page (#109449)

* Add on-Cloud state to Upgrade Assistant 'Back up data' step (#109956)

* [Upgrade Assistant] Refactor external links to use locators (#110435)

* [Upgrade Assistant] Use AppContext for services instead of KibanaContext (#109801)

* Remove kibana context dependency in favour of app context

* Add missing type to ContextValue

* Fix mock type

* Refactor app mount flow and types

* Refactor to use useServices hook

* Fix linter issues

* Keep mount_management_section and initialize breadcrumbs and api there

* Remove useServices and usePlugins in favour of just useAppContext

* Remove unnecessary mocks

* [Upgrade Assistant] Enable functional and a11y tests (#109909)

* [Upgrade Assistant] Remove version from UA nav title (#110739)

* [Upgrade Assistant] New Kibana deprecations page (#110101)

* Use injected lib.handleEsError instead of importing it in Upgrade Assistant API route handlers. (#111067)

* Add tests for UA back up data step on Cloud (#111066)

* Update UA to consume snapshotsUrl as provided by the Cloud plugin. (#111239)

* Skip flaky UA Backup step polling test.

* [Upgrade Assistant] Refactor kibana deprecation service mocks (#111168)

* [Upgrade Assistant] Remove unnecessary EuiScreenReaderOnly from stat panels (#111518)

* Remove EuiScreenReaderOnly implementations

* Remove unused translations

* Remove extra string after merge conflict

* Use consistent 'issues' and 'critical' vs. 'warning' terminology in UA. (#111221)

* Refactor UA Overview to support step-completion (#111243)

* Refactor UA Overview to store step-completion state at the root and delegate step-completion logic to each step component.
* Add completion status to logs and issues steps

* [Upgrade Assistant] External links with checkpoint time-range applied (#111252)

* Bound query around last checkpoint date

* Fix tests

* Also test discover url contains search params

* Small refactor

* Keep state about lastCheckpoint in parent component

* Remove space

* Address CR changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Tests for updating step state accordingly if API poll receives count followed by error (#111701)

* Add test for logs count polling

* Test when count api fails

* [Upgrade Assistant] Add a11y tests for es deprecation flyouts (#110843)

* [Upgrade Assistant] Set fix_logs step as incomplete if log collection is not enabled (#111827)

* set step as incomplete if toggle is disabled

* Fix test names

* Remove unnecessary mocks

* [Upgrade Assistant] Update copy to use "issues" instead of "warnings" (#111817)

* Create common deprecation issues panel component in UA (#111231)

* Refine success state behavior and add tests.
* Refactor components into a components directory.
* Refactor SCSS to colocate styles with their components.
* Refactor tests to reduce boilerplate and clarify conditions under test.

* [Upgrade Assistant] Fix Kibana deprecations warning message

* [Upgrade Assistant] Add support for API keys when reindexing (#111451)

* [Upgrade Assistant] Update readme (#112154)

* [Upgrade Assistant] Make infra plugin optional (#111960)

* Make infra plugin optional

* Fix CR requests

* [Upgrade Assistant] Improve flyout information architecture (#111713)

* Make sure longstrings inside flyout body are text-wrap

* Show resolved badge for reindex flyout and row

* Finish off rest of ES deprecation flyouts

* Refactor deprecation badge into its own component

* Add tests for kibana deprecations

* Add tests for es deprecations

* Also check that we have status=error before rendering error callout

* Check for non-complete states instead of just error

* Small refactor

* Default deprecation is not resolvable

* Add a bit more spacing between title and badge

* Address CR changes

* Use EuiSpacer instead of flexitems

* [Upgrade Assistant] Update readme (#112195)

* [Upgrade Assistant] Add integration tests for Overview page (#111370)

* Add a11y tests for when overview page has toggle enabled

* Add functional and accessibility tests for overview page

* Load test files

* Fix linter error

* Navigate before asserting

* Steps have now completion state

* Remove duped word

* Run setup only once, not per test

* Address CR changes

* No need to renavigate to the page

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Add note about compatibility headers (#110469)

* Improve error states for Upgrade Assistant deprecation issues (#112457)

* Simplify error state for deprecation issues panels. Remove <EsStatsError />.

* Rename components from stats -> panel.

* Create common error-reporting component for use in both Kibana and ES deprecations pages.
* Align order of loading, error, and success states between these pages.
* Change references to 'deprecations' -> 'deprecation issues'.

* Fix tests for panels.

* Add API integration test for handling auth error.

* Fix TS errors.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Change count poll time to 15s (#112669)

* [Upgrade Assistant] Add permissions check to logs step (#112420)

* [Upgrade Assistant] Refactor telemetry (#112177)

* [Upgrade Assistant] Check for ML upgrade mode before enabling flyout actions (#112555)

* Add missing error handlers for deprecation logging route (#113109)

* [Upgrade Assistant] Batch reindex docs (#112960)

* [UA] Added batch reindexing docs link to the ES deprecations page. Added a link from "batch reindexing" docs page to "start or resume reindex" docs page and from there to ES reindexing docs page. Also renamed "reindexing operation" to "reindexing task" for consistency.

* [Upgrade Assistant] Added docs build files

* Update x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx

Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com>

* Update x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx

Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com>

* [Upgrade Assistant] Added review suggestions and fixed eslint issues

Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Improve error messages for GET /api/upgrade_assistant/reindex/<index> (#112961)

* Add support for single manual steps to Upgrade Assistant. (#113344)

* Revert "[Upgrade Assistant] Refactor telemetry (#112177)" (#113665)

This reverts commit 991d24bad2.

* [Upgrade Assistant] Use skipFetchFields when creating the indexPattern in order to avoid errors if index doesn't exist (#113821)

* Use skipFetchFields when creating the indexPatter in order to avoid errors when index doesnt exist

* Address CR feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Hide system indices from es deprecations list (#113627)

* Refactor reindex routes into separate single and batch reindex files. Apply version precheck to batch routes. (#113822)

* [Upgrade Assistant] Remove ML/Watcher logic (#113224)

* Add show upgrade flag to url (#114243)

* [Upgrade Assistant] Delete deprecation log cache (#114113)

* [Upgrade Assistant] Add upgrade system indices section (#110593)

* [Upgrade Assistant] Reindexing progress (#114275)

* [Upgrade Assistant] Added reindexing progress in % to the reindex flyout and es deprecations table

* [Upgrade Assistant] Renamed first argument in `getReindexProgressLabel` to `reindexTaskPercComplete` for consistency

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Remove Fix manually heading when there are no manual steps

* Add rolling upgrade interstitials to UA (#112907)

* Refactor FixLogsStep to be explicit in which props are passed to DeprecationLoggingToggle.

* Centralize error-handling logic in the api service, instead of handling it within each individual API request. Covers:
- Cloud backup status
- ES deprecations
- Deprecation logging
- Remove index settings
- ML
- Reindexing

Also:
- Handle 426 error state and surface in UI.
- Move ResponseError type into common/types.

* Add note about intended use case of status API route.

* Add endpoint dedicated to surfacing the cluster upgrade state, and a client-side poll.

* Merge App and AppWithRouter components.

* [Upgrade Assistant] Added "accept changes" header to the warnings list in the reindex flyout (#114798)

* Refactor kibana deprecation tests (#114763)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Fix linter issues

* Remove unused translation

* Prefer master changes over 7.x for ml docs

* Prefer master changes over 7.x

* Skip tests

* Move everything to a single describe

* Fix types

* Add missing prop to mock

* [Upgrade Assistant] Removed "closed index" warning from reindex flyout (#114861)

* [Upgrade Assistant] Removed "closed index" warning that reindexing might take longer than usual, which is not the case

* [Upgrade Assistant] Also deleted i18n strings that are not needed anymore

* Add LevelIconTips to be more explicit about the difference between critical and warning issues. (#115121)

* Extract common DeprecationFlyoutLearnMoreLink component and change wording to 'Learn more'. (#115117)

* [Upgrade Assistant] Reindexing cancellation (#114636)

* [Upgrade Assistant] Updated the reindexing cancellation to look less like an error

* [Upgrade Assistant] Fixed an i18n issue and updated a jest snapshot

* [Upgrade Assistant] Updated cancelled reindexing state with a unified label and cross icon

* [Upgrade Assistant] Fixed snapshot test

* [Upgrade Assistant] Updated spacing to the reindex cancel button

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Fix test errors (#115183)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Overview page UI clean up (#115258)

- Scaling down deprecation issue panel title size to flow with typographic hierarchy.
- Removing panel around deprecation logging switch to reduce visual elements.
- Using success instead of green color for migration complete message.

* Revert "Revert "[Upgrade Assistant] Refactor telemetry (#112177)" (#113665)" (#114804)

This reverts commit c385d49887.
* Add migration to remove obsolete attributes from telemetry saved object.
* Refactor UA telemetry constants by extracting it from common/types.

* [Upgrade Assistant] Rename upgrade_status to migration_status (#114755)

* [Upgrade Assistant] Swapped reindexing flyouts order (#115046)

* [Upgrade Assistant] Changed reindexing steps order, replaced a warning callout with a text element

* [Upgrade Assistant] Fixed reindex flyout test and changed warning callout from danger color to warning color

* [Upgrade Assistant] Fixed the correct status to show warnings

* [Upgrade Assistant] Fixed i18n strings

* [Upgrade Assistant] Moved reindex with warnings logic into a function

* [Upgrade Assistant] Updated reindex flyout copy

* [Upgrade Assistant] Also added a trailing period to the reindex step 3

* [Upgrade Assistant] Fixed i18n strings and step 3 wording

* [Upgrade Assistant] Added docs changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* [Upgrade Assistant] Hide features that don't need to be migrated from flyout (#115535)

* Filter out system indices that dont require migration on server side
* Rename to attrs to migration
* Update flyout snapshot.

* Refine Upgrade Assistant copy. (#115472)

* Remove unused file

* Fix kibanaVersion dep

* Updated config.ts to fix UA test

UA functional API integration test to check cloud backup status creates a snapshot repo, which fails to be created with my changes to config.ts `'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2,'`. Adding `/tmp/cloud-snapshots/'` to the config fixes the test.

* Address CR changes

* Add missing error handler for system indices migration (#116088)

* Fix broken tests

* Fix test

* Skip tests

* Fix linter errors and import

* [Upgrade Assistant] Fix typo in retrieval of cluster settings (#116335)

* Fix typos

* Fix typo also in server tests

* Make sure log collection remains enabled throughout the test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Fix type errors

* Fix integration test types

* Fix accessibility test type errors

* Fix linter errors in shared_imports

* Fix functional test types

Co-authored-by: CJ Cenizal <cj@cenizal.com>
Co-authored-by: Alison Goryachev <alison.goryachev@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com>
Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com>
Co-authored-by: Dmitry Borodyansky <dborodyansky@gmail.com>
This commit is contained in:
Ignacio Rivas 2021-11-09 14:48:12 +01:00 committed by GitHub
parent 7f50f34358
commit 8819bd8fae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
247 changed files with 10129 additions and 6465 deletions

View file

@ -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 <<start-resume-reindex, reindexing>> 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 <<start-resume-reindex, start or resume endpoint>>, 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.

View file

@ -4,7 +4,7 @@
<titleabbrev>Cancel reindex</titleabbrev>
++++
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`.

View file

@ -4,7 +4,9 @@
<titleabbrev>Check reindex status</titleabbrev>
++++
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 <<status-code,Status codes>>.
<3> Last successfully completed step of the reindex. For details, see <<step-code,Step codes>> 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 <<warning-code,Warning codes>>.
<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.

View file

@ -4,9 +4,18 @@
<titleabbrev>Start or resume reindex</titleabbrev>
++++
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 <<status-code,Status codes>>.
<3> The last successfully completed step of the reindex. For more information, refer to <<step-code,Step codes>>.
<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.

View file

@ -4,7 +4,7 @@
<titleabbrev>Upgrade readiness status</titleabbrev>
++++
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.

View file

@ -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]

View file

@ -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<string, string>;
readonly siem: {

View file

@ -22,6 +22,7 @@ export class DocLinksService {
// Documentation for `main` branches is still published at a `master` URL.
const DOC_LINK_VERSION = kibanaBranch === 'main' ? 'master' : kibanaBranch;
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}/`;
@ -36,6 +37,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`,
@ -158,7 +162,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}`,
@ -222,10 +230,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`,
@ -289,6 +298,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<string, string>;
readonly siem: {

View file

@ -478,6 +478,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;
@ -601,7 +604,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<string, string>;
readonly siem: {

View file

@ -103,8 +103,13 @@ export const useRequest = <D = any, E = Error>(
: 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]
);

View file

@ -8079,44 +8079,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"
}
}
}
}
},

View file

@ -25100,28 +25100,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": "再インデックスを実行",
@ -25132,17 +25118,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": "次の廃止予定のインデックス設定が検出されました。",
@ -25150,16 +25128,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}にアップグレードするときにここに戻って確認してください。",
@ -25178,35 +25146,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": "廃止予定ログを分析",
@ -25226,8 +25169,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をアップグレードできます。",

View file

@ -25526,28 +25526,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": "运行重新索引",
@ -25558,17 +25544,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": "检测到以下弃用的索引设置:",
@ -25576,16 +25554,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} 时,回到这里查看。",
@ -25604,37 +25572,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": "分析弃用日志",
@ -25654,8 +25595,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。",

View file

@ -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.
#### Elasticsearch
* **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.
Elasticsearch deprecations can be handled in a number of ways:
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.
* **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)).
#### 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).

View file

@ -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<typeof createActions>;
};
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<string, unknown>): Promise<AppTestBed> => {
const initTestBed = registerTestBed(WithAppDependencies(App, overrides), testBedConfig);
const testBed = await initTestBed();
return {
...testBed,
actions: createActions(testBed),
};
};

View file

@ -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<typeof setupEnvironment>['server'];
let httpRequestsMockHelpers: ReturnType<typeof setupEnvironment>['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);
});
});
});

View file

@ -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
);

View file

@ -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);

View file

@ -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.'
);
});
});

View file

@ -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<typeof createActions>;
};
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<string, unknown>
): Promise<ElasticsearchTestBed> => {
const initTestBed = registerTestBed(
WithAppDependencies(EsDeprecations, overrides),
testBedConfig
);
const testBed = await initTestBed();
return {
...testBed,
actions: createActions(testBed),
};
};

View file

@ -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(

View file

@ -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(

View file

@ -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);
});
});
});

View file

@ -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<string, any>) => {
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, any>): 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: () => {},
});

View file

@ -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<typeof createActions>;
};
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<string, unknown>): Promise<ElasticsearchTestBed> => {
const initTestBed = registerTestBed(
WithAppDependencies(EsDeprecations, overrides),
testBedConfig
);
const testBed = await initTestBed();
return {
...testBed,
actions: createActions(testBed),
};
};

View file

@ -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,
};
};

View file

@ -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';

View file

@ -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<KibanaTestSubjects> & {
actions: ReturnType<typeof createActions>;
};
const createActions = (testBed: TestBed) => {
/**
* User Actions
*/
const clickExpandAll = () => {
const { find } = testBed;
find('expandAll').simulate('click');
};
return {
clickExpandAll,
};
};
export const setup = async (overrides?: Record<string, unknown>): Promise<KibanaTestBed> => {
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;

View file

@ -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: {},
})
),
},
},
};

View file

@ -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<string, unknown> = {}) =>
(Comp: any, { privileges, ...overrides }: Record<string, unknown> = {}) =>
(props: Record<string, unknown>) => {
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 (
<KibanaContextProvider services={{ ...servicesMock, ...(servicesOverrides as {}) }}>
<AppContextProvider value={{ ...contextValue, ...contextOverrides }}>
<AuthorizationContext.Provider
value={createAuthorizationContextValue(privileges as Privileges)}
>
<AppContextProvider value={merge(appContextMock, overrides)}>
<GlobalFlyoutProvider>
<Comp {...props} />
</GlobalFlyoutProvider>
</AppContextProvider>
</KibanaContextProvider>
</AuthorizationContext.Provider>
);
};
export const setupEnvironment = () => {
const { server, httpRequestsMockHelpers } = initHttpRequests();
const { server, setServerAsync, httpRequestsMockHelpers } = initHttpRequests();
return {
server,
setServerAsync,
httpRequestsMockHelpers,
};
};

View file

@ -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();
});
};

View file

@ -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<Element> | 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);
});
});
});

View file

@ -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');
});
});
});

View file

@ -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<DeprecationsServiceStart>;
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'
);
});
});
});

View file

@ -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'
);
});
});

View file

@ -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<typeof createActions>;
};
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<string, unknown>
): Promise<KibanaTestBed> => {
const initTestBed = registerTestBed(
WithAppDependencies(KibanaDeprecations, overrides),
testBedConfig
);
const testBed = await initTestBed();
return {
...testBed,
actions: createActions(testBed),
};
};

View file

@ -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<DeprecationsServiceStart>;
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<DeprecationsServiceStart>;
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'
),
},
};

View file

@ -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<typeof setupEnvironment>['server'];
let setServerAsync: ReturnType<typeof setupEnvironment>['setServerAsync'];
let httpRequestsMockHelpers: ReturnType<typeof setupEnvironment>['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);
});
});
});
});

View file

@ -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');
});
});
});

View file

@ -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.');
});
});
});

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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);
});
});
});

View file

@ -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.'
);
});
});
});

View file

@ -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',
},
];

View file

@ -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);
});
});
});

View file

@ -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",
],
]
`;

View file

@ -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();
});
});

View file

@ -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);
});
});
});

View file

@ -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',
},
],
},
],
};

View file

@ -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);
});
});
});

View file

@ -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<OverviewTestSubjects> & {
export type OverviewTestBed = TestBed & {
actions: ReturnType<typeof createActions>;
};
@ -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<string, unknown>): Promise<OverviewTestBed> => {
export const setupOverviewPage = async (
overrides?: Record<string, unknown>
): Promise<OverviewTestBed> => {
const initTestBed = registerTestBed(WithAppDependencies(Overview, overrides), testBedConfig);
const testBed = await initTestBed();
@ -51,31 +97,3 @@ export const setup = async (overrides?: Record<string, unknown>): Promise<Overvi
actions: createActions(testBed),
};
};
export type OverviewTestSubjects =
| 'esStatsPanel'
| 'esStatsPanel.warningDeprecations'
| 'esStatsPanel.criticalDeprecations'
| 'kibanaStatsPanel'
| 'kibanaStatsPanel.warningDeprecations'
| 'kibanaStatsPanel.criticalDeprecations'
| 'deprecationLoggingFormRow'
| 'esRequestErrorIconTip'
| 'kibanaRequestErrorIconTip'
| 'partiallyUpgradedErrorIconTip'
| 'upgradedErrorIconTip'
| 'unauthorizedErrorIconTip'
| 'upgradedPrompt'
| 'partiallyUpgradedPrompt'
| 'deprecationLoggingToggle'
| 'updateLoggingError'
| 'fetchLoggingError'
| 'noDeprecationsLabel'
| 'viewObserveLogs'
| 'viewDiscoverLogs'
| 'upgradeSetupDocsLink'
| 'upgradeSetupCloudLink'
| 'deprecationWarningCallout'
| 'whatsNewLink'
| 'documentationLink'
| 'upgradeStatusError';

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { OverviewTestBed, setupOverviewPage, setupEnvironment, kibanaVersion } from '../helpers';
import { setupEnvironment } from '../helpers';
import { OverviewTestBed, setupOverviewPage } from './overview.helpers';
describe('Overview Page', () => {
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', () => {

View file

@ -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');
});
});
});

View file

@ -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'
);
});
});
});

View file

@ -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;

View file

@ -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;
}

View file

@ -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"]
}

View file

@ -1 +0,0 @@
@import 'components/index';

View file

@ -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 { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt, EuiPageContent, EuiLoadingSpinner } from '@elastic/eui';
import { ScopedHistory } from 'src/core/public';
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 { 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<ClusterUpgradeState>('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 <ComingSoonPrompt />;
}
if (clusterUpgradeState === 'isUpgrading') {
return (
<EuiPageContent
hasShadow={false}
paddingSize="none"
verticalPosition="center"
horizontalPosition="center"
data-test-subj="isUpgradingMessage"
>
<EuiEmptyPrompt
iconType="logoElasticsearch"
title={
<h1>
<FormattedMessage
id="xpack.upgradeAssistant.upgradingTitle"
defaultMessage="Your cluster is upgrading"
/>
</h1>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.upgradingDescription"
defaultMessage="One or more Elasticsearch nodes have a newer version of
Elasticsearch than Kibana. Once all your nodes are upgraded, upgrade Kibana."
/>
</p>
}
data-test-subj="emptyPrompt"
/>
</EuiPageContent>
);
}
if (clusterUpgradeState === 'isUpgradeComplete') {
return (
<EuiPageContent
hasShadow={false}
paddingSize="none"
verticalPosition="center"
horizontalPosition="center"
data-test-subj="isUpgradeCompleteMessage"
>
<EuiEmptyPrompt
iconType="logoElasticsearch"
title={
<h1>
<FormattedMessage
id="xpack.upgradeAssistant.upgradedTitle"
defaultMessage="Your cluster has been upgraded"
/>
</h1>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.upgradedDescription"
defaultMessage="All Elasticsearch nodes have been upgraded. You may now upgrade Kibana."
/>
</p>
}
data-test-subj="emptyPrompt"
/>
</EuiPageContent>
);
}
return (
<Switch>
<Route exact path="/overview" component={Overview} />
<Route exact path="/es_deprecations" component={EsDeprecations} />
<Route exact path="/kibana_deprecations" component={KibanaDeprecationsContent} />
<Route exact path="/kibana_deprecations" component={KibanaDeprecations} />
<Redirect from="/" to="/overview" />
</Switch>
);
};
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 (
<EuiPageContent
hasShadow={false}
paddingSize="none"
verticalPosition="center"
horizontalPosition="center"
>
<EuiEmptyPrompt body={<EuiLoadingSpinner size="l" />} />
</EuiPageContent>
);
}
return (
<Router history={history}>
<App />
<AppHandlingClusterUpgradeState />
</Router>
);
};
export const RootComponent = ({
i18n,
history,
services,
application,
...contextValue
}: AppDependencies) => {
export const RootComponent = (dependencies: AppDependencies) => {
const {
history,
core: { i18n, application, http },
} = dependencies.services;
return (
<RedirectAppLinks application={application}>
<i18n.Context>
<KibanaContextProvider services={services}>
<AppContextProvider value={contextValue}>
<RedirectAppLinks application={application} className={APP_WRAPPER_CLASS}>
<AuthorizationProvider httpClient={http} privilegesEndpoint={`${API_BASE_PATH}/privileges`}>
<i18n.Context>
<AppContextProvider value={dependencies}>
<GlobalFlyoutProvider>
<AppWithRouter history={history} />
<App history={history} />
</GlobalFlyoutProvider>
</AppContextProvider>
</KibanaContextProvider>
</i18n.Context>
</i18n.Context>
</AuthorizationProvider>
</RedirectAppLinks>
);
};

View file

@ -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<ContextValue>({} as any);
export const AppContext = createContext<AppDependencies | undefined>(undefined);
export const AppContextProvider = ({
children,
value,
}: {
children: React.ReactNode;
value: ContextValue;
value: AppDependencies;
}) => {
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

View file

@ -1,2 +0,0 @@
@import 'es_deprecations/index';
@import 'overview/index';

View file

@ -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;

View file

@ -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],
};

View file

@ -1 +0,0 @@
@import 'deprecation_types/index';

View file

@ -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 (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge isCritical={deprecation.isCritical} isResolved={false} />
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2>{message}</h2>
<h2 id="defaultDeprecationDetailsFlyoutTitle">{message}</h2>
</EuiTitle>
{index && (
<EuiText data-test-subj="flyoutDescription">
@ -74,11 +71,9 @@ export const DefaultDeprecationFlyout = ({
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>{details}</p>
<p className="eui-textBreakWord">{details}</p>
<p>
<EuiLink target="_blank" href={url}>
{i18nTexts.learnMoreLinkLabel}
</EuiLink>
<DeprecationFlyoutLearnMoreLink documentationUrl={url} />
</p>
</EuiText>
</EuiFlyoutBody>

View file

@ -42,6 +42,7 @@ export const DefaultTableRow: React.FunctionComponent<Props> = ({ rowFieldNames,
},
flyoutProps: {
onClose: closeFlyout,
className: 'eui-textBreakWord',
'data-test-subj': 'defaultDeprecationDetails',
'aria-labelledby': 'defaultDeprecationDetailsFlyoutTitle',
},
@ -60,8 +61,8 @@ export const DefaultTableRow: React.FunctionComponent<Props> = ({ rowFieldNames,
>
<EsDeprecationsTableCells
fieldName={field}
openFlyout={() => setShowFlyout(true)}
deprecation={deprecation}
openFlyout={() => setShowFlyout(true)}
/>
</EuiTableRowCell>
);

View file

@ -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 (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge
isCritical={deprecation.isCritical}
isResolved={statusType === 'complete'}
/>
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2>{message}</h2>
<h2 id="indexSettingsDetailsFlyoutTitle">{message}</h2>
</EuiTitle>
<EuiText>
<p>
@ -136,9 +146,7 @@ export const RemoveIndexSettingsFlyout = ({
<EuiText>
<p>{details}</p>
<p>
<EuiLink target="_blank" href={url}>
{i18nTexts.learnMoreLinkLabel}
</EuiLink>
<DeprecationFlyoutLearnMoreLink documentationUrl={url} />
</p>
</EuiText>
@ -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

View file

@ -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.',
}
),
};

View file

@ -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<Props> = ({
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<Props> = ({
},
flyoutProps: {
onClose: closeFlyout,
className: 'eui-textBreakWord',
'data-test-subj': 'indexSettingsDetails',
'aria-labelledby': 'indexSettingsDetailsFlyoutTitle',
},

View file

@ -12,6 +12,7 @@ import { useSnapshotState, SnapshotState } from './use_snapshot_state';
export interface MlSnapshotContext {
snapshotState: SnapshotState;
mlUpgradeModeEnabled: boolean;
upgradeSnapshot: () => Promise<void>;
deleteSnapshot: () => Promise<void>;
}
@ -31,12 +32,14 @@ interface Props {
children: React.ReactNode;
snapshotId: string;
jobId: string;
mlUpgradeModeEnabled: boolean;
}
export const MlSnapshotsStatusProvider: React.FunctionComponent<Props> = ({
api,
snapshotId,
jobId,
mlUpgradeModeEnabled,
children,
}) => {
const { updateSnapshotStatus, snapshotState, upgradeSnapshot, deleteSnapshot } = useSnapshotState(
@ -57,6 +60,7 @@ export const MlSnapshotsStatusProvider: React.FunctionComponent<Props> = ({
snapshotState,
upgradeSnapshot,
deleteSnapshot,
mlUpgradeModeEnabled,
}}
>
{children}

View file

@ -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) => (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledErrorDescription"
defaultMessage="No actions can be taken on Machine Learning snapshots while upgrade mode is enabled. {docsLink}."
values={{
docsLink: (
<EuiLink href={docsLink} target="_blank" data-test-subj="setUpgradeModeDocsLink">
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledDocsLink"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
),
};
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 (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge isCritical={deprecation.isCritical} isResolved={isResolved} />
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2>{i18nTexts.flyoutTitle}</h2>
<h2 id="mlSnapshotDetailsFlyoutTitle">{i18nTexts.flyoutTitle}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{snapshotState.error && (
{snapshotState.error && !isResolved && (
<>
<EuiCallOut
title={
@ -130,12 +211,27 @@ export const FixSnapshotsFlyout = ({
<EuiSpacer />
</>
)}
{mlUpgradeModeEnabled && (
<>
<EuiCallOut
title={i18nTexts.upgradeModeEnabledErrorTitle}
color="warning"
iconType="alert"
data-test-subj="mlUpgradeModeEnabledError"
>
<p>
{i18nTexts.upgradeModeEnabledErrorDescription(docLinks.links.ml.setUpgradeMode)}
</p>
</EuiCallOut>
<EuiSpacer />
</>
)}
<EuiText>
<p>{deprecation.details}</p>
<p>
<EuiLink target="_blank" href={deprecation.url}>
{i18nTexts.learnMoreLinkLabel}
</EuiLink>
<DeprecationFlyoutLearnMoreLink documentationUrl={deprecation.url} />
</p>
</EuiText>
</EuiFlyoutBody>
@ -147,7 +243,7 @@ export const FixSnapshotsFlyout = ({
</EuiButtonEmpty>
</EuiFlexItem>
{isResolvable && (
{!isResolved && !mlUpgradeModeEnabled && (
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem>
@ -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)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
fill
onClick={onUpgradeSnapshot}
isLoading={false}
isLoading={
snapshotState.action === 'upgrade' && snapshotState.status === 'in_progress'
}
isDisabled={snapshotState.status === 'in_progress'}
data-test-subj="upgradeSnapshotButton"
>
{snapshotState.action === 'upgrade' && snapshotState.error
? i18nTexts.retryUpgradeButtonLabel
: i18nTexts.upgradeButtonLabel}
{getUpgradeButtonLabel(snapshotState)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -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.',
}
),
};

View file

@ -21,6 +21,7 @@ const { useGlobalFlyout } = GlobalFlyout;
interface TableRowProps {
deprecation: EnrichedDeprecationInfo;
rowFieldNames: DeprecationTableColumns[];
mlUpgradeModeEnabled: boolean;
}
export const MlSnapshotsTableRowCells: React.FunctionComponent<TableRowProps> = ({
@ -50,6 +51,7 @@ export const MlSnapshotsTableRowCells: React.FunctionComponent<TableRowProps> =
},
flyoutProps: {
onClose: closeFlyout,
className: 'eui-textBreakWord',
'data-test-subj': 'mlSnapshotDetails',
'aria-labelledby': 'mlSnapshotDetailsFlyoutTitle',
},
@ -76,12 +78,15 @@ export const MlSnapshotsTableRowCells: React.FunctionComponent<TableRowProps> =
};
export const MlSnapshotsTableRow: React.FunctionComponent<TableRowProps> = (props) => {
const { api } = useAppContext();
const {
services: { api },
} = useAppContext();
return (
<MlSnapshotsStatusProvider
snapshotId={(props.deprecation.correctiveAction as MlAction).snapshotId}
jobId={(props.deprecation.correctiveAction as MlAction).jobId}
mlUpgradeModeEnabled={props.mlUpgradeModeEnabled}
api={api}
>
<MlSnapshotsTableRowCells {...props} />

View file

@ -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]);

View file

@ -3,44 +3,32 @@
exports[`ChecklistFlyout renders 1`] = `
<Fragment>
<EuiFlyoutBody>
<EuiCallOut
color="warning"
iconType="alert"
title={
<FormattedMessage
defaultMessage="Index is unable to ingest, update, or delete documents while reindexing"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.calloutTitle"
values={Object {}}
/>
}
>
<EuiText>
<p>
<FormattedMessage
defaultMessage="If you cant stop document updates or need to reindex into a new cluster, consider using a different upgrade strategy."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.cantStopDetail"
values={Object {}}
defaultMessage="The index will be read-only during reindexing. You won't be able to add, update, or delete documents until reindexing is complete. If you need to reindex to a new cluster, use the reindex API. {docsLink}"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexDescription"
values={
Object {
"docsLink": <EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/docs-reindex.html#reindex-from-remote"
target="_blank"
>
Learn more
</EuiLink>,
}
}
/>
</p>
<p>
<FormattedMessage
defaultMessage="Reindexing will continue in the background, but if Kibana shuts down or restarts you will need to return to this page to resume reindexing."
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress or resume reindexing after a Kibana restart."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
values={Object {}}
/>
</p>
</EuiCallOut>
</EuiText>
<EuiSpacer />
<EuiTitle
size="xs"
>
<h3>
<FormattedMessage
defaultMessage="Reindexing process"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle"
values={Object {}}
/>
</h3>
</EuiTitle>
<ReindexProgress
cancelReindex={[MockFunction]}
reindexState={
@ -80,13 +68,14 @@ exports[`ChecklistFlyout renders 1`] = `
>
<EuiButton
color="primary"
data-test-subj="startReindexingButton"
disabled={false}
fill={true}
isLoading={false}
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Run reindex"
defaultMessage="Start reindexing"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel"
values={Object {}}
/>

View file

@ -2,28 +2,7 @@
exports[`WarningsFlyoutStep renders 1`] = `
<Fragment>
<EuiFlyoutBody>
<EuiCallOut
color="danger"
iconType="alert"
title={
<FormattedMessage
defaultMessage="This index requires destructive changes that cannot be reversed"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="Back up the index before continuing. To proceed with the reindex, accept each change."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail"
values={Object {}}
/>
</p>
</EuiCallOut>
<EuiSpacer />
</EuiFlyoutBody>
<EuiFlyoutBody />
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="spaceBetween"
@ -33,11 +12,11 @@ exports[`WarningsFlyoutStep renders 1`] = `
>
<EuiButtonEmpty
flush="left"
iconType="cross"
iconType="arrowLeft"
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Cancel"
defaultMessage="Back"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel"
values={Object {}}
/>
@ -47,13 +26,13 @@ exports[`WarningsFlyoutStep renders 1`] = `
grow={false}
>
<EuiButton
color="danger"
color="primary"
disabled={false}
fill={true}
onClick={[MockFunction]}
>
<FormattedMessage
defaultMessage="Continue with reindex"
defaultMessage="Continue reindexing"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel"
values={Object {}}
/>

View file

@ -18,7 +18,7 @@ $stepStatusToCallOutColor: (
failed: 'danger',
complete: 'success',
paused: 'warning',
cancelled: 'danger',
cancelled: 'warning',
);
.upgStepProgress__status--circle {

View file

@ -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(<ChecklistFlyoutStep {...props} />);
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(<ChecklistFlyoutStep {...props} />);
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(<ChecklistFlyoutStep {...props} />);
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(<ChecklistFlyoutStep {...props} />);
expect(wrapper.exists('[data-test-subj="reindexingFailedCallout"]')).toBe(true);
});
it('calls startReindex when button is clicked', () => {

View file

@ -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:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.doneLabel"
defaultMessage="Resolved"
/>
);
case ReindexStatus.paused:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.resumeLabel"
defaultMessage="Resume"
defaultMessage="Resume reindexing"
/>
);
case ReindexStatus.cancelled:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.restartLabel"
defaultMessage="Restart reindexing"
/>
);
default:
return (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel"
defaultMessage="Run reindex"
defaultMessage="Start reindexing"
/>
);
}
@ -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 (
<Fragment>
<EuiFlyoutBody>
{renderGlobalCallouts()}
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.calloutTitle"
defaultMessage="Index is unable to ingest, update, or delete documents while reindexing"
/>
}
color="warning"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.cantStopDetail"
defaultMessage="If you cant stop document updates or need to reindex into a new cluster,
consider using a different upgrade strategy."
/>
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
defaultMessage="Reindexing will continue in the background, but if Kibana shuts down or restarts you will
need to return to this page to resume reindexing."
/>
</p>
</EuiCallOut>
{!hasRequiredPrivileges && (
{hasRequiredPrivileges === false && (
<Fragment>
<EuiSpacer />
<EuiCallOut
@ -122,15 +107,58 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
/>
</Fragment>
)}
<EuiSpacer />
<EuiTitle size="xs">
<h3>
{(hasFetchFailed || hasReindexingFailed) && (
<>
<EuiSpacer />
<EuiCallOut
color="danger"
iconType="alert"
data-test-subj={hasFetchFailed ? 'fetchFailedCallout' : 'reindexingFailedCallout'}
title={
hasFetchFailed ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.fetchFailedCalloutTitle"
defaultMessage="Reindex status not available"
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingFailedCalloutTitle"
defaultMessage="Reindexing error"
/>
)
}
>
{reindexState.errorMessage}
</EuiCallOut>
</>
)}
<EuiText>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle"
defaultMessage="Reindexing process"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexDescription"
defaultMessage="The index will be read-only during reindexing. You won't be able to add, update, or delete documents until reindexing is complete. If you need to reindex to a new cluster, use the reindex API. {docsLink}"
values={{
docsLink: (
<EuiLink target="_blank" href={docLinks.links.upgradeAssistant.remoteReindex}>
{i18n.translate(
'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
),
}}
/>
</h3>
</EuiTitle>
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail"
defaultMessage="Reindexing is performed in the background. You can return to the Upgrade Assistant to view progress or resume reindexing after a Kibana restart."
/>
</p>
</EuiText>
<EuiSpacer />
<ReindexProgress reindexState={reindexState} cancelReindex={cancelReindex} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
@ -143,18 +171,21 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color={status === ReindexStatus.paused ? 'warning' : 'primary'}
iconType={status === ReindexStatus.paused ? 'play' : undefined}
onClick={startReindex}
isLoading={loading}
disabled={loading || status === ReindexStatus.completed || !hasRequiredPrivileges}
>
{buttonLabel(status)}
</EuiButton>
</EuiFlexItem>
{!hasFetchFailed && !isCompleted && hasRequiredPrivileges && (
<EuiFlexItem grow={false}>
<EuiButton
fill
color={status === ReindexStatus.paused ? 'warning' : 'primary'}
iconType={status === ReindexStatus.paused ? 'play' : undefined}
onClick={startReindex}
isLoading={loading}
disabled={loading || !hasRequiredPrivileges}
data-test-subj="startReindexingButton"
>
{buttonLabel(status)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</Fragment>

View file

@ -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) => (
<EuiLink target="_blank" href={`${docLinks.links.apis.openIndex}`}>
{i18n.translate(
'xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation',
{ defaultMessage: 'documentation' }
)}
</EuiLink>
);
const getIndexClosedCallout = (docLinks: DocLinksStart) => (
<>
<EuiCallOut
title={i18n.translate(
'xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutTitle',
{ defaultMessage: 'Index closed' }
)}
color="warning"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails"
defaultMessage="This index is currently closed. The Upgrade Assistant will open, reindex and then close the index. {reindexingMayTakeLongerEmph}. Please see the {docs} for more information."
values={{
docs: getOpenAndCloseIndexDocLink(docLinks),
reindexingMayTakeLongerEmph: (
<b>
{i18n.translate(
'xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis',
{ defaultMessage: 'Reindexing may take longer than usual' }
)}
</b>
),
}}
/>
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</>
);
export const ReindexFlyout: React.FunctionComponent<ReindexFlyoutProps> = ({
reindexState,
startReindex,
@ -81,53 +35,60 @@ export const ReindexFlyout: React.FunctionComponent<ReindexFlyoutProps> = ({
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<ReindexFlyoutStep>(
reindexWarnings && reindexWarnings.length > 0 && status === undefined
? ReindexFlyoutStep.reindexWarnings
: ReindexFlyoutStep.checklist
const { index } = deprecation;
const [showWarningsStep, setShowWarningsStep] = useState(false);
const onStartReindex = useCallback(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK);
startReindex();
}, [startReindex]);
const onStopReindex = useCallback(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK);
cancelReindex();
}, [cancelReindex]);
const startReindexWithWarnings = () => {
if (
reindexWarnings &&
reindexWarnings.length > 0 &&
status !== ReindexStatus.inProgress &&
status !== ReindexStatus.completed
) {
setShowWarningsStep(true);
} else {
onStartReindex();
}
};
const flyoutContents = showWarningsStep ? (
<WarningsFlyoutStep
warnings={reindexState.reindexWarnings ?? []}
hideWarningsStep={() => setShowWarningsStep(false)}
continueReindex={() => {
setShowWarningsStep(false);
onStartReindex();
}}
/>
) : (
<ChecklistFlyoutStep
closeFlyout={closeFlyout}
startReindex={startReindexWithWarnings}
reindexState={reindexState}
cancelReindex={onStopReindex}
/>
);
let flyoutContents: React.ReactNode;
const globalCallout =
(correctiveAction as ReindexAction).blockerForReindexing === 'index-closed' &&
reindexState.status !== ReindexStatus.completed
? getIndexClosedCallout(docLinks)
: undefined;
switch (currentFlyoutStep) {
case ReindexFlyoutStep.reindexWarnings:
flyoutContents = (
<WarningsFlyoutStep
renderGlobalCallouts={() => globalCallout}
closeFlyout={closeFlyout}
warnings={reindexState.reindexWarnings!}
advanceNextStep={() => setCurrentFlyoutStep(ReindexFlyoutStep.checklist)}
/>
);
break;
case ReindexFlyoutStep.checklist:
flyoutContents = (
<ChecklistFlyoutStep
renderGlobalCallouts={() => globalCallout}
closeFlyout={closeFlyout}
reindexState={reindexState}
startReindex={startReindex}
cancelReindex={cancelReindex}
/>
);
break;
default:
throw new Error(`Invalid flyout step: ${currentFlyoutStep}`);
}
return (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge
isCritical={deprecation.isCritical}
isResolved={status === ReindexStatus.completed}
/>
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2>
<h2 id="reindexDetailsFlyoutTitle">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.flyoutHeader"
defaultMessage="Reindex {index}"
@ -136,6 +97,7 @@ export const ReindexFlyout: React.FunctionComponent<ReindexFlyoutProps> = ({
</h2>
</EuiTitle>
</EuiFlyoutHeader>
{flyoutContents}
</>
);

View file

@ -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(`
<StepProgress
steps={
Array [
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Setting old index to read-only"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Creating new index"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Reindexing documents"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Swapping original index with alias"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasSwapStepTitle"
values={Object {}}
/>,
},
]
}
/>
`);
<Fragment>
<EuiTitle
data-test-subj="reindexChecklistTitle"
size="xs"
>
<h3>
<FormattedMessage
defaultMessage="Reindexing in progress… {percents}"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingInProgressTitle"
values={
Object {
"percents": "0%",
}
}
/>
</h3>
</EuiTitle>
<StepProgress
steps={
Array [
Object {
"status": "inProgress",
"title": <FormattedMessage
defaultMessage="Setting original index to read-only."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.readonlyStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Create new index."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <ReindexingDocumentsStepTitle
cancelReindex={[MockFunction]}
reindexState={
Object {
"errorMessage": null,
"lastCompletedStep": 0,
"reindexTaskPercComplete": null,
"status": 0,
}
}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Swap original index with alias."
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasSwapStepTitle"
values={Object {}}
/>,
},
]
}
/>
</Fragment>
`);
});
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(
<ReindexProgress
reindexState={
{
lastCompletedStep: ReindexStep.reindexStarted,
status: ReindexStatus.inProgress,
reindexTaskPercComplete: 0.25,
errorMessage: null,
} as ReindexState
}
cancelReindex={jest.fn()}
/>
);
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(
<ReindexProgress
reindexState={
{
lastCompletedStep: ReindexStep.created,
status: ReindexStatus.inProgress,
indexGroup: IndexGroup.ml,
reindexTaskPercComplete: null,
errorMessage: null,
} as ReindexState
}
cancelReindex={jest.fn()}
/>
);
expect(wrapper).toMatchInlineSnapshot(`
<StepProgress
steps={
Array [
Object {
"status": "inProgress",
"title": <FormattedMessage
defaultMessage="Pausing Machine Learning jobs"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.pauseMlStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Setting old index to read-only"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Creating new index"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Reindexing documents"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Swapping original index with alias"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasSwapStepTitle"
values={Object {}}
/>,
},
Object {
"status": "incomplete",
"title": <FormattedMessage
defaultMessage="Resuming Machine Learning jobs"
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeMlStepTitle"
values={Object {}}
/>,
},
]
}
/>
`);
});
});

View file

@ -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 ? (
<EuiProgress size="s" value={reindexTaskPercComplete} max={1} />
) : (
<EuiProgress size="s" />
);
}> = ({ reindexState: { lastCompletedStep, status, cancelLoadingState }, cancelReindex }) => {
if (status === ReindexStatus.cancelled) {
return (
<>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelledTitle"
defaultMessage="Reindexing cancelled."
/>
</>
);
}
// 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 = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel"
@ -62,7 +68,7 @@ const ReindexProgressBar: React.FunctionComponent<{
/>
);
break;
case LoadingState.Success:
case CancelLoadingState.Success:
cancelText = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancelledLabel"
@ -70,8 +76,7 @@ const ReindexProgressBar: React.FunctionComponent<{
/>
);
break;
case LoadingState.Error:
cancelText = 'Could not cancel';
case CancelLoadingState.Error:
cancelText = (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel"
@ -89,26 +94,77 @@ const ReindexProgressBar: React.FunctionComponent<{
}
return (
<EuiFlexGroup alignItems={'center'}>
<EuiFlexItem>{progressBar}</EuiFlexItem>
<EuiFlexGroup component="span">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={cancelReindex}
disabled={
cancelLoadingState === LoadingState.Loading ||
status !== ReindexStatus.inProgress ||
lastCompletedStep !== ReindexStep.reindexStarted
}
isLoading={cancelLoadingState === LoadingState.Loading}
>
{cancelText}
</EuiButtonEmpty>
{stepInProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.reindexingDocumentsStepTitle"
defaultMessage="Reindexing documents."
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle"
defaultMessage="Reindex documents."
/>
)}
</EuiFlexItem>
{showCancelLink && (
<EuiFlexItem>
<EuiLink
data-test-subj="cancelReindexingDocumentsButton"
onClick={cancelReindex}
disabled={cancelLoadingState !== undefined}
>
{cancelText}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
const orderedSteps = Object.values(ReindexStep).sort() as number[];
const getStepTitle = (step: ReindexStep, inProgress?: boolean): ReactNode => {
if (step === ReindexStep.readonly) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.readonlyStepTitle"
defaultMessage="Setting original index to read-only."
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle"
defaultMessage="Set original index to read-only."
/>
);
}
if (step === ReindexStep.newIndexCreated) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.createIndexStepTitle"
defaultMessage="Creating new index."
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
defaultMessage="Create new index."
/>
);
}
if (step === ReindexStep.aliasCreated) {
return inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.inProgress.aliasSwapStepTitle"
defaultMessage="Swapping original index with alias."
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasSwapStepTitle"
defaultMessage="Swap original index with alias."
/>
);
}
};
/**
* 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<StepProgressStep, 'status' | 'children'> => {
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: <ErrorCallout {...{ errorMessage }} />,
};
} else if (status === ReindexStatus.paused && lastCompletedStep === previousStep) {
return {
title: getStepTitle(thisStep),
status: 'paused',
children: <PausedCallout />,
};
} 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: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle"
defaultMessage="Reindexing documents"
/>
),
title: <ReindexingDocumentsStepTitle {...props} />,
} as StepProgressStep;
if (
@ -189,82 +250,38 @@ export const ReindexProgress: React.FunctionComponent<{
lastCompletedStep === ReindexStep.reindexStarted
) {
reindexingDocsStep.status = 'inProgress';
reindexingDocsStep.children = <ReindexProgressBar {...props} />;
} else {
reindexingDocsStep.status = 'complete';
}
const steps = [
{
title: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle"
defaultMessage="Setting old index to read-only"
/>
),
...stepDetails(ReindexStep.readonly),
},
{
title: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle"
defaultMessage="Creating new index"
/>
),
...stepDetails(ReindexStep.newIndexCreated),
},
getProgressStep(ReindexStep.readonly),
getProgressStep(ReindexStep.newIndexCreated),
reindexingDocsStep,
{
title: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.aliasSwapStepTitle"
defaultMessage="Swapping original index with alias"
/>
),
...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: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.pauseMlStepTitle"
defaultMessage="Pausing Machine Learning jobs"
/>
),
...stepDetails(ReindexStep.indexGroupServicesStopped),
});
steps.push({
title: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeMlStepTitle"
defaultMessage="Resuming Machine Learning jobs"
/>
),
...stepDetails(ReindexStep.indexGroupServicesStarted),
});
} else if (indexGroup === IndexGroup.watcher) {
steps.unshift({
title: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.stopWatcherStepTitle"
defaultMessage="Stopping Watcher"
/>
),
...stepDetails(ReindexStep.indexGroupServicesStopped),
});
steps.push({
title: (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeWatcherStepTitle"
defaultMessage="Resuming Watcher"
/>
),
...stepDetails(ReindexStep.indexGroupServicesStarted),
});
}
return <StepProgress steps={steps} />;
return (
<>
<EuiTitle size="xs" data-test-subj="reindexChecklistTitle">
<h3>
{status === ReindexStatus.inProgress ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingInProgressTitle"
defaultMessage="Reindexing in progress… {percents}"
values={{
percents: getReindexProgressLabel(reindexTaskPercComplete, lastCompletedStep),
}}
/>
) : (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle"
defaultMessage="Reindexing process"
/>
)}
</h3>
</EuiTitle>
<StepProgress steps={steps} />
</>
);
};

View file

@ -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<StepProgressStep & { idx: number }> = ({
}) => {
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 (
<Fragment>
<div className="upgStepProgress__step">
<StepStatus status={status} idx={idx} />
<p className={titleClassName}>{title}</p>
<div className={titleClassName}>{title}</div>
</div>
{children && <div className="upgStepProgress__content">{children}</div>}
</Fragment>

View file

@ -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();
});
}
});

View file

@ -105,7 +105,7 @@ export const CustomTypeNameWarningCheckbox: React.FunctionComponent<WarningCheck
description={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningDetail"
defaultMessage="Mapping types are no longer supported in 8.0. Ensure no application code or scripts rely on {mappingType}."
defaultMessage="Mapping types are no longer supported in Elastic 8.x. Ensure no application code or scripts rely on {mappingType}."
values={{
mappingType: <EuiCode>{meta!.typeName as string}</EuiCode>,
}}

View file

@ -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<WarningsConfirmationFlyoutProps> = ({
warnings,
renderGlobalCallouts,
closeFlyout,
advanceNextStep,
hideWarningsStep,
continueReindex,
}) => {
const { docLinks } = useAppContext();
const {
services: {
core: { docLinks },
},
} = useAppContext();
const { links } = docLinks;
const [checkedIds, setCheckedIds] = useState<CheckedIds>(
@ -83,57 +86,66 @@ export const WarningsFlyoutStep: React.FunctionComponent<WarningsConfirmationFly
return (
<>
<EuiFlyoutBody>
{renderGlobalCallouts()}
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle"
defaultMessage="This index requires destructive changes that cannot be reversed"
/>
}
color="danger"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail"
defaultMessage="Back up the index before continuing. To proceed with the reindex, accept each change."
/>
</p>
</EuiCallOut>
<EuiSpacer />
{warnings.map((warning, index) => {
const WarningCheckbox = warningToComponentMap[warning.warningType];
return (
<WarningCheckbox
key={idForWarning(index)}
isChecked={checkedIds[idForWarning(index)]}
onChange={onChange}
docLinks={links}
id={idForWarning(index)}
meta={warning.meta}
/>
);
})}
{warnings.length > 0 && (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle"
defaultMessage="This index requires destructive changes that cannot be reversed"
/>
}
color="warning"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail"
defaultMessage="Back up the index before continuing. To proceed with the reindex, accept each change."
/>
</p>
</EuiCallOut>
<EuiSpacer />
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.acceptChangesTitle"
defaultMessage="Accept changes"
/>
</h3>
</EuiTitle>
<EuiSpacer />
{warnings.map((warning, index) => {
const WarningCheckbox = warningToComponentMap[warning.warningType];
return (
<WarningCheckbox
key={idForWarning(index)}
isChecked={checkedIds[idForWarning(index)]}
onChange={onChange}
docLinks={links}
id={idForWarning(index)}
meta={warning.meta}
/>
);
})}
</>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<EuiButtonEmpty iconType="arrowLeft" onClick={hideWarningsStep} flush="left">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel"
defaultMessage="Cancel"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill color="danger" onClick={advanceNextStep} disabled={blockAdvance}>
<EuiButton fill color="primary" onClick={continueReindex} disabled={blockAdvance}>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel"
defaultMessage="Continue with reindex"
defaultMessage="Continue reindexing"
/>
</EuiButton>
</EuiFlexItem>

View file

@ -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 = () => {
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexInProgressText}</EuiText>
<EuiText size="s">
{i18nTexts.reindexInProgressText}{' '}
{getReindexProgressLabel(
reindexState.reindexTaskPercComplete,
reindexState.lastCompletedStep
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -119,6 +132,17 @@ export const ReindexResolutionCell: React.FunctionComponent = () => {
</EuiFlexItem>
</EuiFlexGroup>
);
case ReindexStatus.fetchFailed:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="alert" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexFetchFailedText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
case ReindexStatus.paused:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
@ -130,17 +154,6 @@ export const ReindexResolutionCell: React.FunctionComponent = () => {
</EuiFlexItem>
</EuiFlexGroup>
);
case ReindexStatus.cancelled:
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="alert" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{i18nTexts.reindexCanceledText}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (

View file

@ -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<TableRowProps> = ({
}) => {
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<TableRowProps> = ({
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<TableRowProps> = ({
},
flyoutProps: {
onClose: closeFlyout,
className: 'eui-textBreakWord',
'data-test-subj': 'reindexDetails',
'aria-labelledby': 'reindexDetailsFlyoutTitle',
},
@ -61,13 +67,9 @@ const ReindexTableRowCells: React.FunctionComponent<TableRowProps> = ({
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<TableRowProps> = ({
};
export const ReindexTableRow: React.FunctionComponent<TableRowProps> = (props) => {
const { api } = useAppContext();
const {
services: { api },
} = useAppContext();
return (
<ReindexStatusProvider indexName={props.deprecation.index!} api={api}>

View file

@ -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;

View file

@ -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<Props> = ({ error }) => {
const { code: errorType, message } = getEsDeprecationError(error);
switch (errorType) {
case 'unauthorized_error':
return (
<EuiCallOut
title={message}
color="danger"
iconType="alert"
data-test-subj="permissionsError"
/>
);
case 'partially_upgraded_error':
return (
<EuiCallOut
title={message}
color="warning"
iconType="alert"
data-test-subj="partiallyUpgradedWarning"
/>
);
case 'upgraded_error':
return <EuiCallOut title={message} iconType="pin" data-test-subj="upgradedCallout" />;
case 'request_error':
default:
return (
<EuiCallOut title={message} color="danger" iconType="alert" data-test-subj="requestError">
{error.message}
</EuiCallOut>
);
}
};

View file

@ -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 (
<FormattedMessage
id="xpack.upgradeAssistant.esDeprecations.batchReindexingDocsDescription"
defaultMessage="To start multiple reindexing tasks in a single request, use the Kibana {docsLink}."
values={{
docsLink: (
<EuiLink
href={docLinks.links.upgradeAssistant.batchReindex}
target="_blank"
external={true}
>
{i18n.translate('xpack.upgradeAssistant.esDeprecations.batchReindexingDocsLink', {
defaultMessage: 'batch reindexing API',
})}
</EuiLink>
),
}}
/>
);
};
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 <EsDeprecationErrors error={error} />;
return (
<DeprecationsPageLoadingError
deprecationSource="Elasticsearch"
message={getEsDeprecationError(error).message}
/>
);
}
if (isLoading) {
@ -82,7 +132,20 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
return (
<div data-test-subj="esDeprecationsContent">
<EuiPageHeader pageTitle={i18nTexts.pageTitle} description={i18nTexts.pageDescription} />
<EuiPageHeader
pageTitle={i18nTexts.pageTitle}
description={
<>
{i18nTexts.pageDescription}
{getBatchReindexLink(docLinks)}
</>
}
>
<DeprecationCount
totalCriticalDeprecations={deprecationsCountByLevel.criticalDeprecations}
totalWarningDeprecations={deprecationsCountByLevel.warningDeprecations}
/>
</EuiPageHeader>
<EuiSpacer size="l" />

View file

@ -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 <MlSnapshotsTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
return (
<MlSnapshotsTableRow
deprecation={deprecation}
rowFieldNames={cellTypes}
mlUpgradeModeEnabled={mlUpgradeModeEnabled}
/>
);
case 'indexSetting':
return <IndexSettingsTableRow deprecation={deprecation} rowFieldNames={cellTypes} />;
@ -146,12 +156,19 @@ export const EsDeprecationsTable: React.FunctionComponent<Props> = ({
deprecations = [],
reload,
}) => {
const {
services: { api },
} = useAppContext();
const { data } = api.useLoadMlUpgradeMode();
const mlUpgradeModeEnabled = !!data?.mlUpgradeModeEnabled;
const [sortConfig, setSortConfig] = useState<SortConfig>({
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<Query>(EuiSearchBar.Query.MATCH_ALL);
const [searchError, setSearchError] = useState<{ message: string } | undefined>(undefined);
@ -261,7 +278,7 @@ export const EsDeprecationsTable: React.FunctionComponent<Props> = ({
<EuiSpacer size="m" />
<EuiTable>
<EuiTable data-test-subj="esDeprecationsTable">
<EuiTableHeader>
{Object.entries(cellToLabelMap).map(([fieldName, cell]) => {
return (
@ -291,7 +308,7 @@ export const EsDeprecationsTable: React.FunctionComponent<Props> = ({
{visibleDeprecations.map((deprecation, index) => {
return (
<EuiTableRow data-test-subj="deprecationTableRow" key={`deprecation-row-${index}`}>
{renderTableRowCells(deprecation)}
{renderTableRowCells(deprecation, mlUpgradeModeEnabled)}
</EuiTableRow>
);
})}

View file

@ -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<Props> = ({
}) => {
// "Status column"
if (fieldName === 'isCritical') {
if (deprecation.isCritical === true) {
return <EuiBadge color="danger">{i18nTexts.criticalBadgeLabel}</EuiBadge>;
}
return <>{''}</>;
return <DeprecationBadge isCritical={deprecation.isCritical} />;
}
// "Issue" column
@ -66,7 +69,13 @@ export const EsDeprecationsTableCells: React.FunctionComponent<Props> = ({
return <>{resolutionTableCell}</>;
}
return <>{''}</>;
return (
<EuiToolTip position="top" content={i18nTexts.manualCellTooltipLabel}>
<EuiText size="s" color="subdued">
{i18nTexts.manualCellLabel}
</EuiText>
</EuiToolTip>
);
}
// Default behavior: render value or empty string if undefined

View file

@ -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';

View file

@ -0,0 +1,4 @@
// Used to add spacing between the list of manual deprecation steps
.upgResolveStep {
margin-bottom: $euiSizeL;
}

View file

@ -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<void>;
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: (
<FormattedMessage
id="xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveCalloutTitle"
defaultMessage="Click {quickResolve} to fix this issue automatically."
values={{
quickResolve: (
<strong>
{i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveText', {
defaultMessage: 'Quick resolve',
})}
</strong>
),
}}
/>
),
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 (
<>
<EuiFlyoutHeader hasBorder>
<DeprecationBadge isCritical={deprecation.level === 'critical'} isResolved={isResolved} />
<EuiSpacer size="s" />
<EuiTitle size="s" data-test-subj="flyoutTitle">
<h2 id="kibanaDeprecationDetailsFlyoutTitle" className="eui-textBreakWord">
{title}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{deprecationResolutionState?.resolveDeprecationStatus === 'fail' && (
<>
<EuiCallOut
title={i18nTexts.quickResolveErrorTitle}
color="danger"
iconType="alert"
data-test-subj="quickResolveError"
>
{deprecationResolutionState.resolveDeprecationError}
</EuiCallOut>
<EuiSpacer />
</>
)}
<EuiText>
<p className="eui-textBreakWord">{message}</p>
{documentationUrl && (
<p>
<DeprecationFlyoutLearnMoreLink documentationUrl={documentationUrl} />
</p>
)}
</EuiText>
<EuiSpacer />
{/* Hide resolution steps if already resolved */}
{!isResolved && (
<div data-test-subj="resolveSection">
{correctiveActions.api && (
<>
<EuiCallOut
title={i18nTexts.quickResolveCalloutTitle}
color="primary"
iconType="iInCircle"
data-test-subj="quickResolveCallout"
/>
<EuiSpacer />
</>
)}
{correctiveActions.manualSteps.length > 0 && (
<>
<EuiTitle size="s" data-test-subj="manualStepsTitle">
<h3>{i18nTexts.manualFixTitle}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText>
{correctiveActions.manualSteps.length === 1 ? (
<p data-test-subj="manualStep" className="eui-textBreakWord">
{correctiveActions.manualSteps[0]}
</p>
) : (
<ol data-test-subj="manualStepsList">
{correctiveActions.manualSteps.map((step, stepIndex) => (
<li
data-test-subj="manualStepsListItem"
key={`step-${stepIndex}`}
className="upgResolveStep eui-textBreakWord"
>
{step}
</li>
))}
</ol>
)}
</EuiText>
</>
)}
</div>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={closeFlyout} flush="left">
{i18nTexts.closeButtonLabel}
</EuiButtonEmpty>
</EuiFlexItem>
{/* Only show the "Quick resolve" button if deprecation supports it and deprecation is not yet resolved */}
{correctiveActions.api && !isResolved && (
<EuiFlexItem grow={false}>
<EuiButton
fill
data-test-subj="resolveButton"
onClick={onResolveDeprecation}
isLoading={Boolean(
deprecationResolutionState?.resolveDeprecationStatus === 'in_progress'
)}
>
{getQuickResolveButtonLabel(deprecationResolutionState)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

View file

@ -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<Props> = ({
deprecation,
forceExpand,
index,
showStepsModal,
showResolveModal,
}) => {
const { domainId, level, message, documentationUrl, correctiveActions } = deprecation;
return (
<EuiAccordion
id={`${domainId}-${index}`}
data-test-subj={`${domainId}Deprecation`}
initialIsOpen={forceExpand}
buttonContent={i18nTexts.getDeprecationTitle(domainId)}
paddingSize="m"
extraAction={<DeprecationHealth single deprecationLevels={[LEVEL_MAP[level]]} />}
>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiText size="s">
{level === 'fetch_error' ? (
<EuiCallOut
title={message}
color="warning"
iconType="alert"
data-test-subj={`${domainId}Error`}
size="s"
/>
) : (
<>
<p>{message}</p>
{(documentationUrl || correctiveActions?.manualSteps) && (
<EuiFlexGroup>
{correctiveActions?.api && (
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
data-test-subj="resolveButton"
onClick={() => showResolveModal(deprecation)}
>
{i18nTexts.resolveButtonLabel}
</EuiButton>
</EuiFlexItem>
)}
{correctiveActions?.manualSteps && (
<EuiFlexItem grow={false}>
<EuiButton
size="s"
data-test-subj="stepsButton"
onClick={() =>
showStepsModal({
domainId,
steps: correctiveActions.manualSteps!,
documentationUrl,
})
}
>
{i18nTexts.manualFixButtonLabel}
</EuiButton>
</EuiFlexItem>
)}
{documentationUrl && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
href={documentationUrl}
iconType="help"
target="_blank"
>
{i18nTexts.docLinkText}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</>
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
);
};

View file

@ -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<void>;
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<Props> = ({
deprecations,
showStepsModal,
showResolveModal,
reloadDeprecations,
isLoading,
}) => {
const [currentFilter, setCurrentFilter] = useState<LevelFilterOption>('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 (
<>
<SearchBar
isLoading={isLoading}
loadData={reloadDeprecations}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSearchChange={setSearch}
totalDeprecationsCount={deprecations.length}
levelToDeprecationCountMap={levelToDeprecationCountMap}
/>
{deprecationsWithErrors.length > 0 && (
<>
<KibanaDeprecationErrors errorType="pluginError" />
<EuiSpacer />
</>
)}
<DeprecationListBar
allDeprecationsCount={deprecations.length}
filteredDeprecationsCount={filteredDeprecations.length}
setExpandAll={setExpandAll}
/>
<EuiHorizontalRule margin="m" />
<>
{filteredDeprecations
.slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE)
.map((deprecation, index) => [
<div key={`kibana-deprecation-${index}`} data-test-subj="kibanaDeprecationItem">
<KibanaDeprecationAccordion
{...{
key: expandState.expandNumber,
index,
deprecation,
forceExpand: expandState.forceExpand,
showStepsModal,
showResolveModal,
}}
/>
<EuiHorizontalRule margin="s" />
</div>,
])}
{/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE */}
{filteredDeprecations.length > DEPRECATIONS_PER_PAGE && (
<>
<EuiSpacer />
<DeprecationPagination
pageCount={Math.ceil(filteredDeprecations.length / DEPRECATIONS_PER_PAGE)}
activePage={currentPage}
setPage={setCurrentPage}
/>
</>
)}
</>
</>
);
};

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