[Upgrade Assistant] use request hook (#94473)

This commit is contained in:
Alison Goryachev 2021-03-18 13:42:20 -04:00 committed by GitHub
parent 882248c671
commit 1592e3c01a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 712 additions and 1038 deletions

View file

@ -32,3 +32,5 @@ export const indexSettingDeprecations = {
settings: ['translog.retention.size', 'translog.retention.age'],
},
};
export const API_BASE_PATH = '/api/upgrade_assistant';

View file

@ -5,5 +5,6 @@
"ui": true,
"configPath": ["xpack", "upgrade_assistant"],
"requiredPlugins": ["management", "licensing", "features"],
"optionalPlugins": ["cloud", "usageCollection"]
"optionalPlugins": ["cloud", "usageCollection"],
"requiredBundles": ["esUiShared"]
}

View file

@ -7,6 +7,7 @@
import { DocLinksStart, HttpSetup, NotificationsStart } from 'src/core/public';
import React, { createContext, useContext } from 'react';
import { ApiService } from './lib/api';
export interface KibanaVersionContext {
currentMajor: number;
@ -21,6 +22,7 @@ export interface ContextValue {
kibanaVersionInfo: KibanaVersionContext;
notifications: NotificationsStart;
isReadOnlyMode: boolean;
api: ApiService;
}
export const AppContext = createContext<ContextValue>({} as any);

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { get } from 'lodash';
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
@ -16,7 +15,7 @@ import { UpgradeAssistantTabProps } from './types';
export const LoadingErrorBanner: React.FunctionComponent<
Pick<UpgradeAssistantTabProps, 'loadingError'>
> = ({ loadingError }) => {
if (get(loadingError, 'response.status') === 403) {
if (loadingError?.statusCode === 403) {
return (
<EuiCallOut
title={
@ -27,6 +26,7 @@ export const LoadingErrorBanner: React.FunctionComponent<
}
color="danger"
iconType="cross"
data-test-subj="permissionsError"
/>
);
}
@ -41,6 +41,7 @@ export const LoadingErrorBanner: React.FunctionComponent<
}
color="danger"
iconType="cross"
data-test-subj="upgradeStatusError"
>
{loadingError ? loadingError.message : null}
</EuiCallOut>

View file

@ -14,7 +14,7 @@ import { ComingSoonPrompt } from './coming_soon_prompt';
import { UpgradeAssistantTabs } from './tabs';
export const PageContent: React.FunctionComponent = () => {
const { kibanaVersionInfo, isReadOnlyMode, http } = useAppContext();
const { kibanaVersionInfo, isReadOnlyMode } = useAppContext();
const { nextMajor } = kibanaVersionInfo;
// Read-only mode will be enabled up until the last minor before the next major release
@ -38,7 +38,7 @@ export const PageContent: React.FunctionComponent = () => {
</EuiPageHeaderSection>
</EuiPageHeader>
<UpgradeAssistantTabs http={http} />
<UpgradeAssistantTabs />
</>
);
};

View file

@ -1,87 +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 { mountWithIntl } from '@kbn/test/jest';
import { httpServiceMock } from 'src/core/public/mocks';
import { mockKibanaSemverVersion } from '../../../common/constants';
import { UpgradeAssistantTabs } from './tabs';
import { LoadingState } from './types';
import { OverviewTab } from './tabs/overview';
// Used to wait for promises to resolve and renders to finish before reading updates
const promisesToResolve = () => new Promise((resolve) => setTimeout(resolve, 0));
const mockHttp = httpServiceMock.createSetupContract();
jest.mock('../app_context', () => {
return {
useAppContext: () => {
return {
docLinks: {
DOC_LINK_VERSION: 'current',
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
},
kibanaVersionInfo: {
currentMajor: mockKibanaSemverVersion.major,
prevMajor: mockKibanaSemverVersion.major - 1,
nextMajor: mockKibanaSemverVersion.major + 1,
},
};
},
};
});
describe('UpgradeAssistantTabs', () => {
test('renders loading state', async () => {
mockHttp.get.mockReturnValue(
new Promise((resolve) => {
/* never resolve */
})
);
const wrapper = mountWithIntl(<UpgradeAssistantTabs http={mockHttp} />);
// Should pass down loading status to child component
expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Loading);
});
test('successful data fetch', async () => {
// @ts-ignore
mockHttp.get.mockResolvedValue({
data: {
cluster: [],
indices: [],
},
});
const wrapper = mountWithIntl(<UpgradeAssistantTabs http={mockHttp as any} />);
expect(mockHttp.get).toHaveBeenCalled();
await promisesToResolve();
wrapper.update();
// Should pass down success status to child component
expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Success);
});
test('network failure', async () => {
// @ts-ignore
mockHttp.get.mockRejectedValue(new Error(`oh no!`));
const wrapper = mountWithIntl(<UpgradeAssistantTabs http={mockHttp as any} />);
await promisesToResolve();
wrapper.update();
// Should pass down error status to child component
expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Error);
});
it('upgrade error', async () => {
// @ts-ignore
mockHttp.get.mockRejectedValue({ response: { status: 426 } });
const wrapper = mountWithIntl(<UpgradeAssistantTabs http={mockHttp as any} />);
await promisesToResolve();
wrapper.update();
// Should display an informative message if the cluster is currently mid-upgrade
expect(wrapper.find('EuiEmptyPrompt').exists()).toBe(true);
});
});

View file

@ -5,9 +5,8 @@
* 2.0.
*/
import { set } from '@elastic/safer-lodash-set';
import { findIndex, get } from 'lodash';
import React from 'react';
import { findIndex } from 'lodash';
import React, { useEffect, useState, useMemo } from 'react';
import {
EuiEmptyPrompt,
@ -18,173 +17,28 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { HttpSetup } from 'src/core/public';
import { UpgradeAssistantStatus } from '../../../common/types';
import { LatestMinorBanner } from './latest_minor_banner';
import { CheckupTab } from './tabs/checkup';
import { OverviewTab } from './tabs/overview';
import { LoadingState, TelemetryState, UpgradeAssistantTabProps } from './types';
import { TelemetryState, UpgradeAssistantTabProps } from './types';
import { useAppContext } from '../app_context';
enum ClusterUpgradeState {
needsUpgrade,
partiallyUpgraded,
upgraded,
}
export const UpgradeAssistantTabs: React.FunctionComponent = () => {
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [telemetryState, setTelemetryState] = useState<TelemetryState>(TelemetryState.Complete);
interface TabsState {
loadingState: LoadingState;
loadingError?: Error;
checkupData?: UpgradeAssistantStatus;
selectedTabIndex: number;
telemetryState: TelemetryState;
clusterUpgradeState: ClusterUpgradeState;
}
const { api } = useAppContext();
interface Props {
http: HttpSetup;
}
const { data: checkupData, isLoading, error, resendRequest } = api.useLoadUpgradeStatus();
export class UpgradeAssistantTabs extends React.Component<Props, TabsState> {
constructor(props: Props) {
super(props);
this.state = {
loadingState: LoadingState.Loading,
clusterUpgradeState: ClusterUpgradeState.needsUpgrade,
selectedTabIndex: 0,
telemetryState: TelemetryState.Complete,
};
}
public async componentDidMount() {
await this.loadData();
// Send telemetry info about the default selected tab
this.sendTelemetryInfo(this.tabs[this.state.selectedTabIndex].id);
}
public render() {
const { selectedTabIndex, telemetryState, clusterUpgradeState } = this.state;
const tabs = this.tabs;
if (clusterUpgradeState === ClusterUpgradeState.partiallyUpgraded) {
return (
<EuiPageContent>
<EuiPageContentBody>
<EuiEmptyPrompt
iconType="logoElasticsearch"
title={
<h2>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle"
defaultMessage="Your cluster is upgrading"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription"
defaultMessage="One or more Elasticsearch nodes have a newer version of
Elasticsearch than Kibana. Once all your nodes are upgraded, upgrade Kibana."
/>
</p>
}
/>
</EuiPageContentBody>
</EuiPageContent>
);
} else if (clusterUpgradeState === ClusterUpgradeState.upgraded) {
return (
<EuiPageContent>
<EuiPageContentBody>
<EuiEmptyPrompt
iconType="logoElasticsearch"
title={
<h2>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteTitle"
defaultMessage="Your cluster has been upgraded"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteDescription"
defaultMessage="All Elasticsearch nodes have been upgraded. You may now upgrade Kibana."
/>
</p>
}
/>
</EuiPageContentBody>
</EuiPageContent>
);
}
return (
<EuiTabbedContent
data-test-subj={
telemetryState === TelemetryState.Running ? 'upgradeAssistantTelemetryRunning' : undefined
}
tabs={tabs}
onTabClick={this.onTabClick}
selectedTab={tabs[selectedTabIndex]}
/>
);
}
private onTabClick = (selectedTab: EuiTabbedContentTab) => {
const selectedTabIndex = findIndex(this.tabs, { id: selectedTab.id });
if (selectedTabIndex === -1) {
throw new Error(`Clicked tab did not exist in tabs array`);
}
// Send telemetry info about the current selected tab
// only in case the clicked tab id it's different from the
// current selected tab id
if (this.tabs[this.state.selectedTabIndex].id !== selectedTab.id) {
this.sendTelemetryInfo(selectedTab.id);
}
this.setSelectedTabIndex(selectedTabIndex);
};
private setSelectedTabIndex = (selectedTabIndex: number) => {
this.setState({ selectedTabIndex });
};
private loadData = async () => {
try {
this.setState({ loadingState: LoadingState.Loading });
const resp = await this.props.http.get('/api/upgrade_assistant/status');
this.setState({
loadingState: LoadingState.Success,
// resp.data is specifically to handle the CITs which uses axios to mock HTTP requests
checkupData: resp.data ? resp.data : resp,
});
} catch (e) {
if (get(e, 'response.status') === 426) {
this.setState({
loadingState: LoadingState.Success,
clusterUpgradeState: get(e, 'response.data.attributes.allNodesUpgraded', false)
? ClusterUpgradeState.upgraded
: ClusterUpgradeState.partiallyUpgraded,
});
} else {
this.setState({ loadingState: LoadingState.Error, loadingError: e });
}
}
};
private get tabs() {
const { loadingError, loadingState, checkupData } = this.state;
const commonProps: UpgradeAssistantTabProps = {
loadingError,
loadingState,
refreshCheckupData: this.loadData,
setSelectedTabIndex: this.setSelectedTabIndex,
// Remove this in last minor of the current major (eg. 6.7)
const tabs = useMemo(() => {
const commonTabProps: UpgradeAssistantTabProps = {
loadingError: error,
isLoading,
refreshCheckupData: resendRequest,
setSelectedTabIndex,
// Remove this in last minor of the current major (e.g., 7.15)
alertBanner: <LatestMinorBanner />,
};
@ -195,7 +49,7 @@ export class UpgradeAssistantTabs extends React.Component<Props, TabsState> {
name: i18n.translate('xpack.upgradeAssistant.overviewTab.overviewTabTitle', {
defaultMessage: 'Overview',
}),
content: <OverviewTab checkupData={checkupData} {...commonProps} />,
content: <OverviewTab checkupData={checkupData} {...commonTabProps} />,
},
{
id: 'cluster',
@ -210,7 +64,7 @@ export class UpgradeAssistantTabs extends React.Component<Props, TabsState> {
checkupLabel={i18n.translate('xpack.upgradeAssistant.tabs.checkupTab.clusterLabel', {
defaultMessage: 'cluster',
})}
{...commonProps}
{...commonTabProps}
/>
),
},
@ -228,26 +82,103 @@ export class UpgradeAssistantTabs extends React.Component<Props, TabsState> {
defaultMessage: 'index',
})}
showBackupWarning
{...commonProps}
{...commonTabProps}
/>
),
},
];
}
}, [checkupData, error, isLoading, resendRequest]);
private async sendTelemetryInfo(tabName: string) {
// In case we don't have any data yet, we wanna to ignore the
// telemetry info update
if (this.state.loadingState !== LoadingState.Success) {
return;
const tabName = tabs[selectedTabIndex].id;
useEffect(() => {
if (isLoading === false) {
setTelemetryState(TelemetryState.Running);
async function sendTelemetryData() {
await api.sendTelemetryData({
[tabName]: true,
});
setTelemetryState(TelemetryState.Complete);
}
sendTelemetryData();
}
}, [api, selectedTabIndex, tabName, isLoading]);
this.setState({ telemetryState: TelemetryState.Running });
const onTabClick = (selectedTab: EuiTabbedContentTab) => {
const newSelectedTabIndex = findIndex(tabs, { id: selectedTab.id });
if (selectedTabIndex === -1) {
throw new Error('Clicked tab did not exist in tabs array');
}
setSelectedTabIndex(newSelectedTabIndex);
};
await this.props.http.put('/api/upgrade_assistant/stats/ui_open', {
body: JSON.stringify(set({}, tabName, true)),
});
this.setState({ telemetryState: TelemetryState.Complete });
if (error?.statusCode === 426 && error.attributes?.allNodesUpgraded === false) {
return (
<EuiPageContent>
<EuiPageContentBody>
<EuiEmptyPrompt
iconType="logoElasticsearch"
data-test-subj="partiallyUpgradedPrompt"
title={
<h2>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle"
defaultMessage="Your cluster is upgrading"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription"
defaultMessage="One or more Elasticsearch nodes have a newer version of
Elasticsearch than Kibana. Once all your nodes are upgraded, upgrade Kibana."
/>
</p>
}
/>
</EuiPageContentBody>
</EuiPageContent>
);
} else if (error?.statusCode === 426 && error.attributes?.allNodesUpgraded === true) {
return (
<EuiPageContent>
<EuiPageContentBody>
<EuiEmptyPrompt
iconType="logoElasticsearch"
data-test-subj="upgradedPrompt"
title={
<h2>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteTitle"
defaultMessage="Your cluster has been upgraded"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteDescription"
defaultMessage="All Elasticsearch nodes have been upgraded. You may now upgrade Kibana."
/>
</p>
}
/>
</EuiPageContentBody>
</EuiPageContent>
);
}
}
return (
<EuiTabbedContent
data-test-subj={
telemetryState === TelemetryState.Running ? 'upgradeAssistantTelemetryRunning' : undefined
}
tabs={tabs}
onTabClick={onTabClick}
selectedTab={tabs[selectedTabIndex]}
/>
);
};

View file

@ -1,472 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CheckupTab render with deprecations 1`] = `
<Fragment>
<EuiSpacer />
<EuiText
grow={false}
>
<p
data-test-subj="upgradeAssistantIndexTabDetail"
>
<FormattedMessage
defaultMessage="These {strongCheckupLabel} issues need your attention. Resolve them before upgrading to Elasticsearch {nextEsVersion}."
id="xpack.upgradeAssistant.checkupTab.tabDetail"
values={
Object {
"nextEsVersion": "9.x",
"strongCheckupLabel": <strong>
index
</strong>,
}
}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCallOut
color="warning"
iconType="help"
title={
<FormattedMessage
defaultMessage="Back up your indices now"
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="Back up your data using the {snapshotRestoreDocsButton}."
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail"
values={
Object {
"snapshotRestoreDocsButton": <EuiLink
external={true}
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html"
target="_blank"
>
<FormattedMessage
defaultMessage="snapshot and restore APIs"
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
<EuiSpacer />
<EuiPageContent>
<EuiPageContentBody>
<div
data-test-subj="deprecationsContainer"
>
<CheckupControls
allDeprecations={
Array [
Object {
"details": "[[type: doc, field: spins], [type: doc, field: mlockall], [type: doc, field: node_master], [type: doc, field: primary]]",
"index": ".monitoring-es-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: tweet, field: liked]]",
"index": "twitter",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: index-pattern, field: notExpandable], [type: config, field: xPackMonitoring:allowReport], [type: config, field: xPackMonitoring:showBanner], [type: dashboard, field: pause], [type: dashboard, field: timeRestore]]",
"index": ".kibana",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: doc, field: notify], [type: doc, field: created], [type: doc, field: attach_payload], [type: doc, field: met]]",
"index": ".watcher-history-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: doc, field: snapshot]]",
"index": ".monitoring-kibana-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: tweet, field: liked]]",
"index": "twitter2",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"actions": Array [
Object {
"label": "Reindex in Console",
"url": "/app/dev_tools#/console?load_from=%2Fapi%2Fupgrade_assistant%2Freindex%2Fconsole_template%2Ftwitter.json",
},
],
"details": "Reindexing is irreversible, so always back up your index before proceeding.",
"index": "twitter",
"level": "critical",
"message": "This index must be reindexed in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/reindex-upgrade.html",
},
Object {
"details": "Upgrading is irreversible, so always back up your index before proceeding.",
"index": ".triggered_watches",
"level": "critical",
"message": "This index must be upgraded in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html",
},
Object {
"actions": Array [
Object {
"label": "Reindex in Console",
"url": "/app/dev_tools#/console?load_from=%2Fapi%2Fupgrade_assistant%2Freindex%2Fconsole_template%2F.reindex-status.json",
},
],
"details": "Reindexing is irreversible, so always back up your index before proceeding.",
"index": ".reindex-status",
"level": "critical",
"message": "This index must be reindexed in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/reindex-upgrade.html",
},
Object {
"actions": Array [
Object {
"label": "Reindex in Console",
"url": "/app/dev_tools#/console?load_from=%2Fapi%2Fupgrade_assistant%2Freindex%2Fconsole_template%2Ftwitter2.json",
},
],
"details": "Reindexing is irreversible, so always back up your index before proceeding.",
"index": "twitter2",
"level": "critical",
"message": "This index must be reindexed in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/reindex-upgrade.html",
},
Object {
"details": "Upgrading is irreversible, so always back up your index before proceeding.",
"index": ".watches",
"level": "critical",
"message": "This index must be upgraded in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html",
},
]
}
availableGroupByOptions={
Array [
"message",
"index",
]
}
currentFilter="all"
currentGroupBy="message"
loadData={[MockFunction]}
loadingState={1}
onFilterChange={[Function]}
onGroupByChange={[Function]}
onSearchChange={[Function]}
/>
<EuiSpacer />
<GroupedDeprecations
allDeprecations={
Array [
Object {
"details": "[[type: doc, field: spins], [type: doc, field: mlockall], [type: doc, field: node_master], [type: doc, field: primary]]",
"index": ".monitoring-es-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: tweet, field: liked]]",
"index": "twitter",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: index-pattern, field: notExpandable], [type: config, field: xPackMonitoring:allowReport], [type: config, field: xPackMonitoring:showBanner], [type: dashboard, field: pause], [type: dashboard, field: timeRestore]]",
"index": ".kibana",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: doc, field: notify], [type: doc, field: created], [type: doc, field: attach_payload], [type: doc, field: met]]",
"index": ".watcher-history-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: doc, field: snapshot]]",
"index": ".monitoring-kibana-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"details": "[[type: tweet, field: liked]]",
"index": "twitter2",
"level": "warning",
"message": "Coercion of boolean fields",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
"actions": Array [
Object {
"label": "Reindex in Console",
"url": "/app/dev_tools#/console?load_from=%2Fapi%2Fupgrade_assistant%2Freindex%2Fconsole_template%2Ftwitter.json",
},
],
"details": "Reindexing is irreversible, so always back up your index before proceeding.",
"index": "twitter",
"level": "critical",
"message": "This index must be reindexed in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/reindex-upgrade.html",
},
Object {
"details": "Upgrading is irreversible, so always back up your index before proceeding.",
"index": ".triggered_watches",
"level": "critical",
"message": "This index must be upgraded in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html",
},
Object {
"actions": Array [
Object {
"label": "Reindex in Console",
"url": "/app/dev_tools#/console?load_from=%2Fapi%2Fupgrade_assistant%2Freindex%2Fconsole_template%2F.reindex-status.json",
},
],
"details": "Reindexing is irreversible, so always back up your index before proceeding.",
"index": ".reindex-status",
"level": "critical",
"message": "This index must be reindexed in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/reindex-upgrade.html",
},
Object {
"actions": Array [
Object {
"label": "Reindex in Console",
"url": "/app/dev_tools#/console?load_from=%2Fapi%2Fupgrade_assistant%2Freindex%2Fconsole_template%2Ftwitter2.json",
},
],
"details": "Reindexing is irreversible, so always back up your index before proceeding.",
"index": "twitter2",
"level": "critical",
"message": "This index must be reindexed in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/reindex-upgrade.html",
},
Object {
"details": "Upgrading is irreversible, so always back up your index before proceeding.",
"index": ".watches",
"level": "critical",
"message": "This index must be upgraded in order to upgrade the Elastic Stack.",
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html",
},
]
}
currentFilter="all"
currentGroupBy="message"
search=""
/>
</div>
</EuiPageContentBody>
</EuiPageContent>
</Fragment>
`;
exports[`CheckupTab render with error 1`] = `
<Fragment>
<EuiSpacer />
<EuiText
grow={false}
>
<p
data-test-subj="upgradeAssistantIndexTabDetail"
>
<FormattedMessage
defaultMessage="These {strongCheckupLabel} issues need your attention. Resolve them before upgrading to Elasticsearch {nextEsVersion}."
id="xpack.upgradeAssistant.checkupTab.tabDetail"
values={
Object {
"nextEsVersion": "9.x",
"strongCheckupLabel": <strong>
index
</strong>,
}
}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCallOut
color="warning"
iconType="help"
title={
<FormattedMessage
defaultMessage="Back up your indices now"
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="Back up your data using the {snapshotRestoreDocsButton}."
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail"
values={
Object {
"snapshotRestoreDocsButton": <EuiLink
external={true}
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html"
target="_blank"
>
<FormattedMessage
defaultMessage="snapshot and restore APIs"
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
<EuiSpacer />
<EuiPageContent>
<EuiPageContentBody>
<LoadingErrorBanner
loadingError={[Error: something bad!]}
/>
</EuiPageContentBody>
</EuiPageContent>
</Fragment>
`;
exports[`CheckupTab render without deprecations 1`] = `
<Fragment>
<EuiSpacer />
<EuiText
grow={false}
>
<p
data-test-subj="upgradeAssistantIndexTabDetail"
>
<FormattedMessage
defaultMessage="These {strongCheckupLabel} issues need your attention. Resolve them before upgrading to Elasticsearch {nextEsVersion}."
id="xpack.upgradeAssistant.checkupTab.tabDetail"
values={
Object {
"nextEsVersion": "9.x",
"strongCheckupLabel": <strong>
index
</strong>,
}
}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCallOut
color="warning"
iconType="help"
title={
<FormattedMessage
defaultMessage="Back up your indices now"
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutTitle"
values={Object {}}
/>
}
>
<p>
<FormattedMessage
defaultMessage="Back up your data using the {snapshotRestoreDocsButton}."
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail"
values={
Object {
"snapshotRestoreDocsButton": <EuiLink
external={true}
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html"
target="_blank"
>
<FormattedMessage
defaultMessage="snapshot and restore APIs"
id="xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</p>
</EuiCallOut>
<EuiSpacer />
<EuiPageContent>
<EuiPageContentBody>
<EuiEmptyPrompt
body={
<React.Fragment>
<p
data-test-subj="upgradeAssistantIssueSummary"
>
<FormattedMessage
defaultMessage="You have no {strongCheckupLabel} issues."
id="xpack.upgradeAssistant.checkupTab.noIssues.noIssuesLabel"
values={
Object {
"strongCheckupLabel": <strong>
index
</strong>,
}
}
/>
</p>
<p>
<FormattedMessage
defaultMessage="Check the {overviewTabButton} for next steps."
id="xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail"
values={
Object {
"overviewTabButton": <EuiLink
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Overview tab"
id="xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail.overviewTabButtonLabel"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</p>
</React.Fragment>
}
iconType="faceHappy"
title={
<h2>
<FormattedMessage
defaultMessage="All clear!"
id="xpack.upgradeAssistant.checkupTab.noIssues.noIssuesTitle"
values={Object {}}
/>
</h2>
}
/>
</EuiPageContentBody>
</EuiPageContent>
</Fragment>
`;

View file

@ -1,76 +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 { shallow } from 'enzyme';
import React from 'react';
import { mockKibanaSemverVersion } from '../../../../../common/constants';
import { LoadingState } from '../../types';
import AssistanceData from '../__fixtures__/checkup_api_response.json';
import { CheckupTab } from './checkup_tab';
const defaultProps = {
checkupLabel: 'index',
deprecations: AssistanceData.indices,
showBackupWarning: true,
refreshCheckupData: jest.fn(),
loadingState: LoadingState.Success,
setSelectedTabIndex: jest.fn(),
};
jest.mock('../../../app_context', () => {
return {
useAppContext: () => {
return {
docLinks: {
DOC_LINK_VERSION: 'current',
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
},
kibanaVersionInfo: {
currentMajor: mockKibanaSemverVersion.major,
prevMajor: mockKibanaSemverVersion.major - 1,
nextMajor: mockKibanaSemverVersion.major + 1,
},
};
},
};
});
/**
* Mostly a dumb container with copy, test the three main states.
*/
describe('CheckupTab', () => {
test('render with deprecations', () => {
// @ts-expect-error mock data is too loosely typed
expect(shallow(<CheckupTab {...defaultProps} />)).toMatchSnapshot();
});
test('render without deprecations', () => {
expect(
shallow(
<CheckupTab
{...{ ...defaultProps, deprecations: undefined, loadingState: LoadingState.Loading }}
/>
)
).toMatchSnapshot();
});
test('render with error', () => {
expect(
shallow(
<CheckupTab
{...{
...defaultProps,
deprecations: undefined,
loadingState: LoadingState.Error,
loadingError: new Error('something bad!'),
}}
/>
)
).toMatchSnapshot();
});
});

View file

@ -21,12 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { LoadingErrorBanner } from '../../error_banner';
import { useAppContext } from '../../../app_context';
import {
GroupByOption,
LevelFilterOption,
LoadingState,
UpgradeAssistantTabProps,
} from '../../types';
import { GroupByOption, LevelFilterOption, UpgradeAssistantTabProps } from '../../types';
import { CheckupControls } from './controls';
import { GroupedDeprecations } from './deprecations/grouped';
@ -44,7 +39,7 @@ export const CheckupTab: FunctionComponent<CheckupTabProps> = ({
checkupLabel,
deprecations,
loadingError,
loadingState,
isLoading,
refreshCheckupData,
setSelectedTabIndex,
showBackupWarning = false,
@ -159,13 +154,13 @@ export const CheckupTab: FunctionComponent<CheckupTabProps> = ({
<EuiPageContent>
<EuiPageContentBody>
{loadingState === LoadingState.Error ? (
{loadingError ? (
<LoadingErrorBanner loadingError={loadingError} />
) : deprecations && deprecations.length > 0 ? (
<div data-test-subj="deprecationsContainer">
<CheckupControls
allDeprecations={deprecations}
loadingState={loadingState}
isLoading={isLoading}
loadData={refreshCheckupData}
currentFilter={currentFilter}
onFilterChange={changeFilter}
@ -180,6 +175,7 @@ export const CheckupTab: FunctionComponent<CheckupTabProps> = ({
) : (
<EuiEmptyPrompt
iconType="faceHappy"
data-test-subj="noDeprecationsPrompt"
title={
<h2>
<FormattedMessage

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DeprecationInfo } from '../../../../../common/types';
import { GroupByOption, LevelFilterOption, LoadingState } from '../../types';
import { GroupByOption, LevelFilterOption } from '../../types';
import { FilterBar } from './filter_bar';
import { GroupByBar } from './group_by_bar';
@ -18,7 +18,7 @@ import { validateRegExpString } from '../../../utils';
interface CheckupControlsProps {
allDeprecations?: DeprecationInfo[];
loadingState: LoadingState;
isLoading: boolean;
loadData: () => void;
currentFilter: LevelFilterOption;
onFilterChange: (filter: LevelFilterOption) => void;
@ -30,7 +30,7 @@ interface CheckupControlsProps {
export const CheckupControls: FunctionComponent<CheckupControlsProps> = ({
allDeprecations,
loadingState,
isLoading,
loadData,
currentFilter,
onFilterChange,
@ -80,12 +80,7 @@ export const CheckupControls: FunctionComponent<CheckupControlsProps> = ({
<GroupByBar {...{ availableGroupByOptions, currentGroupBy, onGroupByChange }} />
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={loadData}
iconType="refresh"
isLoading={loadingState === LoadingState.Loading}
>
<EuiButton fill onClick={loadData} iconType="refresh" isLoading={isLoading}>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel"
defaultMessage="Refresh"

View file

@ -58,29 +58,23 @@ export const RemoveIndexSettingsProvider = ({ children }: Props) => {
const deprecatedSettings = useRef<string[]>([]);
const indexName = useRef<string | undefined>(undefined);
const { http, notifications } = useAppContext();
const { api, notifications } = useAppContext();
const removeIndexSettings = async () => {
setIsLoading(true);
try {
await http.post(`/api/upgrade_assistant/${indexName.current}/index_settings`, {
body: JSON.stringify({
settings: deprecatedSettings.current,
}),
});
setIsLoading(false);
const { error } = await api.updateIndexSettings(indexName.current!, deprecatedSettings.current);
setIsLoading(false);
closeModal();
if (error) {
notifications.toasts.addDanger(i18nTexts.errorNotificationText);
} else {
setSuccessfulRequests({
[indexName.current!]: true,
});
closeModal();
notifications.toasts.addSuccess(i18nTexts.successNotificationText);
} catch (e) {
setIsLoading(false);
closeModal();
notifications.toasts.addError(e, {
title: i18nTexts.errorNotificationText,
});
}
};

View file

@ -13,6 +13,7 @@ import { Subscription } from 'rxjs';
import { EuiButton, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DocLinksStart, HttpSetup } from 'src/core/public';
import { API_BASE_PATH } from '../../../../../../../common/constants';
import {
EnrichedDeprecationInfo,
ReindexStatus,
@ -240,7 +241,7 @@ export class ReindexButton extends React.Component<ReindexButtonProps, ReindexBu
};
private async sendUIReindexTelemetryInfo(uiReindexAction: UIReindexOption) {
await this.props.http.put('/api/upgrade_assistant/stats/ui_reindex', {
await this.props.http.put(`${API_BASE_PATH}/stats/ui_reindex`, {
body: JSON.stringify(set({}, uiReindexAction, true)),
});
}

View file

@ -8,6 +8,7 @@
import { BehaviorSubject } from 'rxjs';
import { HttpSetup } from 'src/core/public';
import { API_BASE_PATH } from '../../../../../../../common/constants';
import {
IndexGroup,
ReindexOperation,
@ -60,7 +61,7 @@ export class ReindexPollingService {
try {
const data = await this.http.get<StatusResponse>(
`/api/upgrade_assistant/reindex/${this.indexName}`
`${API_BASE_PATH}/reindex/${this.indexName}`
);
this.updateWithResponse(data);
@ -98,7 +99,7 @@ export class ReindexPollingService {
});
const data = await this.http.post<ReindexOperation>(
`/api/upgrade_assistant/reindex/${this.indexName}`
`${API_BASE_PATH}/reindex/${this.indexName}`
);
this.updateWithResponse({ reindexOp: data });
@ -115,7 +116,7 @@ export class ReindexPollingService {
cancelLoadingState: LoadingState.Loading,
});
await this.http.post(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`);
await this.http.post(`${API_BASE_PATH}/reindex/${this.indexName}/cancel`);
} catch (e) {
this.status$.next({
...this.status$.value,

View file

@ -5,71 +5,52 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { EuiLoadingSpinner, EuiSwitch } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { HttpSetup } from 'src/core/public';
import { useAppContext } from '../../../app_context';
import { ResponseError } from '../../../lib/api';
import { LoadingState } from '../../types';
export const DeprecationLoggingToggle: React.FunctionComponent = () => {
const { api } = useAppContext();
interface DeprecationLoggingTabProps {
http: HttpSetup;
}
const [isEnabled, setIsEnabled] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ResponseError | undefined>(undefined);
interface DeprecationLoggingTabState {
loadingState: LoadingState;
loggingEnabled?: boolean;
}
useEffect(() => {
async function getDeprecationLoggingStatus() {
setIsLoading(true);
export class DeprecationLoggingToggle extends React.Component<
DeprecationLoggingTabProps,
DeprecationLoggingTabState
> {
constructor(props: DeprecationLoggingTabProps) {
super(props);
const { data, error: responseError } = await api.getDeprecationLogging();
this.state = {
loadingState: LoadingState.Loading,
};
}
setIsLoading(false);
public UNSAFE_componentWillMount() {
this.loadData();
}
public render() {
const { loggingEnabled, loadingState } = this.state;
// Show a spinner until we've done the initial load.
if (loadingState === LoadingState.Loading && loggingEnabled === undefined) {
return <EuiLoadingSpinner size="l" />;
if (responseError) {
setError(responseError);
} else if (data) {
setIsEnabled(data.isEnabled);
}
}
return (
<EuiSwitch
id="xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch"
data-test-subj="upgradeAssistantDeprecationToggle"
label={this.renderLoggingState()}
checked={loggingEnabled || false}
onChange={this.toggleLogging}
disabled={loadingState === LoadingState.Loading || loadingState === LoadingState.Error}
/>
);
getDeprecationLoggingStatus();
}, [api]);
if (isLoading) {
return <EuiLoadingSpinner size="l" />;
}
private renderLoggingState() {
const { loggingEnabled, loadingState } = this.state;
if (loadingState === LoadingState.Error) {
const renderLoggingState = () => {
if (error) {
return i18n.translate(
'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.errorLabel',
{
defaultMessage: 'Could not load logging state',
}
);
} else if (loggingEnabled) {
} else if (isEnabled) {
return i18n.translate(
'xpack.upgradeAssistant.overviewTab.steps.deprecationLogsStep.enableDeprecationLoggingToggleSwitch.enabledLabel',
{
@ -84,39 +65,33 @@ export class DeprecationLoggingToggle extends React.Component<
}
);
}
}
};
private loadData = async () => {
try {
this.setState({ loadingState: LoadingState.Loading });
const resp = await this.props.http.get('/api/upgrade_assistant/deprecation_logging');
this.setState({
loadingState: LoadingState.Success,
loggingEnabled: resp.isEnabled,
});
} catch (e) {
this.setState({ loadingState: LoadingState.Error });
const toggleLogging = async () => {
const newIsEnabledValue = !isEnabled;
setIsLoading(true);
const { data, error: updateError } = await api.updateDeprecationLogging({
isEnabled: newIsEnabledValue,
});
setIsLoading(false);
if (updateError) {
setError(updateError);
} else if (data) {
setIsEnabled(data.isEnabled);
}
};
private toggleLogging = async () => {
try {
// Optimistically toggle the UI
const newEnabled = !this.state.loggingEnabled;
this.setState({ loadingState: LoadingState.Loading, loggingEnabled: newEnabled });
const resp = await this.props.http.put('/api/upgrade_assistant/deprecation_logging', {
body: JSON.stringify({
isEnabled: newEnabled,
}),
});
this.setState({
loadingState: LoadingState.Success,
loggingEnabled: resp.isEnabled,
});
} catch (e) {
this.setState({ loadingState: LoadingState.Error });
}
};
}
return (
<EuiSwitch
data-test-subj="upgradeAssistantDeprecationToggle"
label={renderLoggingState()}
checked={isEnabled}
onChange={toggleLogging}
disabled={isLoading || Boolean(error)}
/>
);
};

View file

@ -20,7 +20,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { useAppContext } from '../../../app_context';
import { LoadingErrorBanner } from '../../error_banner';
import { LoadingState, UpgradeAssistantTabProps } from '../../types';
import { UpgradeAssistantTabProps } from '../../types';
import { Steps } from './steps';
export const OverviewTab: FunctionComponent<UpgradeAssistantTabProps> = (props) => {
@ -56,9 +56,7 @@ export const OverviewTab: FunctionComponent<UpgradeAssistantTabProps> = (props)
<EuiPageContent>
<EuiPageContentBody>
{props.loadingState === LoadingState.Success && <Steps {...props} />}
{props.loadingState === LoadingState.Loading && (
{props.isLoading && (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner />
@ -66,9 +64,9 @@ export const OverviewTab: FunctionComponent<UpgradeAssistantTabProps> = (props)
</EuiFlexGroup>
)}
{props.loadingState === LoadingState.Error && (
<LoadingErrorBanner loadingError={props.loadingError} />
)}
{props.checkupData && <Steps {...props} />}
{props.loadingError && <LoadingErrorBanner loadingError={props.loadingError} />}
</EuiPageContentBody>
</EuiPageContent>
</>

View file

@ -33,6 +33,7 @@ const WAIT_FOR_RELEASE_STEP = (majorVersion: number, nextMajorVersion: number) =
nextEsVersion: `${nextMajorVersion}.0`,
},
}),
'data-test-subj': 'waitForReleaseStep',
children: (
<>
<EuiText grow={false}>
@ -58,6 +59,7 @@ const START_UPGRADE_STEP = (isCloudEnabled: boolean, esDocBasePath: string) => (
title: i18n.translate('xpack.upgradeAssistant.overviewTab.steps.startUpgradeStep.stepTitle', {
defaultMessage: 'Start your upgrade',
}),
'data-test-subj': 'startUpgradeStep',
children: (
<Fragment>
<EuiText grow={false}>
@ -100,7 +102,7 @@ export const Steps: FunctionComponent<UpgradeAssistantTabProps> = ({
}, {} as { [checkupType: string]: number });
// Uncomment when START_UPGRADE_STEP is in use!
const { kibanaVersionInfo, docLinks, http /* , isCloudEnabled */ } = useAppContext();
const { kibanaVersionInfo, docLinks /* , isCloudEnabled */ } = useAppContext();
const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks;
const esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`;
@ -127,6 +129,7 @@ export const Steps: FunctionComponent<UpgradeAssistantTabProps> = ({
}
),
status: countByType.cluster ? 'warning' : 'complete',
'data-test-subj': 'clusterIssuesStep',
children: (
<EuiText>
{countByType.cluster ? (
@ -185,6 +188,7 @@ export const Steps: FunctionComponent<UpgradeAssistantTabProps> = ({
}
),
status: countByType.indices ? 'warning' : 'complete',
'data-test-subj': 'indicesIssuesStep',
children: (
<EuiText>
{countByType.indices ? (
@ -235,6 +239,7 @@ export const Steps: FunctionComponent<UpgradeAssistantTabProps> = ({
defaultMessage: 'Review the Elasticsearch deprecation logs',
}
),
'data-test-subj': 'deprecationLoggingStep',
children: (
<Fragment>
<EuiText grow={false}>
@ -273,7 +278,7 @@ export const Steps: FunctionComponent<UpgradeAssistantTabProps> = ({
)}
describedByIds={['deprecation-logging']}
>
<DeprecationLoggingToggle http={http} />
<DeprecationLoggingToggle />
</EuiFormRow>
</Fragment>
),

View file

@ -8,14 +8,15 @@
import React from 'react';
import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../../common/types';
import { ResponseError } from '../lib/api';
export interface UpgradeAssistantTabProps {
alertBanner?: React.ReactNode;
checkupData?: UpgradeAssistantStatus;
checkupData?: UpgradeAssistantStatus | null;
deprecations?: EnrichedDeprecationInfo[];
refreshCheckupData: () => Promise<void>;
loadingError?: Error;
loadingState: LoadingState;
refreshCheckupData: () => void;
loadingError: ResponseError | null;
isLoading: boolean;
setSelectedTabIndex: (tabIndex: number) => void;
}

View file

@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpSetup } from 'src/core/public';
import { UpgradeAssistantStatus } from '../../../common/types';
import { API_BASE_PATH } from '../../../common/constants';
import {
UseRequestConfig,
SendRequestConfig,
SendRequestResponse,
sendRequest as _sendRequest,
useRequest as _useRequest,
} from '../../shared_imports';
export interface ResponseError {
statusCode: number;
message: string | Error;
attributes?: Record<string, any>;
}
export class ApiService {
private client: HttpSetup | undefined;
private useRequest<R = any, E = ResponseError>(config: UseRequestConfig) {
if (!this.client) {
throw new Error('API service has not be initialized.');
}
return _useRequest<R, E>(this.client, config);
}
private sendRequest<D = any, E = ResponseError>(
config: SendRequestConfig
): Promise<SendRequestResponse<D, E>> {
if (!this.client) {
throw new Error('API service has not be initialized.');
}
return _sendRequest<D, E>(this.client, config);
}
public setup(httpClient: HttpSetup): void {
this.client = httpClient;
}
public useLoadUpgradeStatus() {
return this.useRequest<UpgradeAssistantStatus>({
path: `${API_BASE_PATH}/status`,
method: 'get',
});
}
public async sendTelemetryData(telemetryData: { [tabName: string]: boolean }) {
const result = await this.sendRequest({
path: `${API_BASE_PATH}/stats/ui_open`,
method: 'put',
body: JSON.stringify(telemetryData),
});
return result;
}
public async getDeprecationLogging() {
const result = await this.sendRequest<{ isEnabled: boolean }>({
path: `${API_BASE_PATH}/deprecation_logging`,
method: 'get',
});
return result;
}
public async updateDeprecationLogging(loggingData: { isEnabled: boolean }) {
const result = await this.sendRequest({
path: `${API_BASE_PATH}/deprecation_logging`,
method: 'put',
body: JSON.stringify(loggingData),
});
return result;
}
public async updateIndexSettings(indexName: string, settings: string[]) {
const result = await this.sendRequest({
path: `${API_BASE_PATH}/${indexName}/index_settings`,
method: 'post',
body: {
settings: JSON.stringify(settings),
},
});
return result;
}
}
export const apiService = new ApiService();

View file

@ -10,6 +10,7 @@ import { ManagementAppMountParams } from '../../../../../src/plugins/management/
import { UA_READONLY_MODE } from '../../common/constants';
import { renderApp } from './render_app';
import { KibanaVersionContext } from './app_context';
import { apiService } from './lib/api';
export async function mountManagementSection(
coreSetup: CoreSetup,
@ -18,14 +19,19 @@ export async function mountManagementSection(
kibanaVersionInfo: KibanaVersionContext
) {
const [{ i18n, docLinks, notifications }] = await coreSetup.getStartServices();
const { http } = coreSetup;
apiService.setup(http);
return renderApp({
element: params.element,
isCloudEnabled,
http: coreSetup.http,
http,
i18n,
docLinks,
kibanaVersionInfo,
notifications,
isReadOnlyMode: UA_READONLY_MODE,
api: apiService,
});
}

View file

@ -8,9 +8,11 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { AppDependencies, RootComponent } from './app';
import { ApiService } from './lib/api';
interface BootDependencies extends AppDependencies {
element: HTMLElement;
api: ApiService;
}
export const renderApp = (deps: BootDependencies) => {

View file

@ -0,0 +1,14 @@
/*
* 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 {
sendRequest,
SendRequestConfig,
SendRequestResponse,
useRequest,
UseRequestConfig,
} from '../../../../src/plugins/es_ui_shared/public/';

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { API_BASE_PATH } from '../../common/constants';
import { getUpgradeAssistantStatus } from '../lib/es_migration_apis';
import { versionCheckHandlerWrapper } from '../lib/es_version_precheck';
import { RouteDependencies } from '../types';
@ -16,7 +17,7 @@ export function registerClusterCheckupRoutes({ cloud, router, licensing, log }:
router.get(
{
path: '/api/upgrade_assistant/status',
path: `${API_BASE_PATH}/status`,
validate: false,
},
versionCheckHandlerWrapper(

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../common/constants';
import {
getDeprecationLoggingStatus,
@ -17,7 +18,7 @@ import { RouteDependencies } from '../types';
export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) {
router.get(
{
path: '/api/upgrade_assistant/deprecation_logging',
path: `${API_BASE_PATH}/deprecation_logging`,
validate: false,
},
versionCheckHandlerWrapper(
@ -38,7 +39,7 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies)
router.put(
{
path: '/api/upgrade_assistant/deprecation_logging',
path: `${API_BASE_PATH}/deprecation_logging`,
validate: {
body: schema.object({
isEnabled: schema.boolean(),

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../../common/constants';
import {
ElasticsearchServiceStart,
kibanaResponseFactory,
@ -85,7 +86,7 @@ export function registerReindexIndicesRoutes(
{ credentialStore, router, licensing, log }: RouteDependencies,
getWorker: () => ReindexWorker
) {
const BASE_PATH = '/api/upgrade_assistant/reindex';
const BASE_PATH = `${API_BASE_PATH}/reindex`;
// Start reindex for an index
router.post(

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../common/constants';
import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis';
import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis';
import { RouteDependencies } from '../types';
@ -13,7 +14,7 @@ import { RouteDependencies } from '../types';
export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) {
router.put(
{
path: '/api/upgrade_assistant/stats/ui_open',
path: `${API_BASE_PATH}/stats/ui_open`,
validate: {
body: schema.object({
overview: schema.boolean({ defaultValue: false }),
@ -37,7 +38,7 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout
router.put(
{
path: '/api/upgrade_assistant/stats/ui_reindex',
path: `${API_BASE_PATH}/stats/ui_reindex`,
validate: {
body: schema.object({
close: schema.boolean({ defaultValue: false }),

View file

@ -6,13 +6,14 @@
*/
import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../common/constants';
import { versionCheckHandlerWrapper } from '../lib/es_version_precheck';
import { RouteDependencies } from '../types';
export function registerUpdateSettingsRoute({ router }: RouteDependencies) {
router.post(
{
path: '/api/upgrade_assistant/{indexName}/index_settings',
path: `${API_BASE_PATH}/{indexName}/index_settings`,
validate: {
params: schema.object({
indexName: schema.string(),

View file

@ -6,18 +6,45 @@
*/
import sinon, { SinonFakeServer } from 'sinon';
import { API_BASE_PATH } from '../../common/constants';
import { UpgradeAssistantStatus } from '../../common/types';
import { ResponseError } from '../../public/application/lib/api';
// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const setLoadStatusResponse = (
response?: UpgradeAssistantStatus,
error?: { body?: Error; status: number }
) => {
const status = error ? error.status || 400 : 200;
const body = error ? error.body : response;
const setLoadStatusResponse = (response?: UpgradeAssistantStatus, error?: ResponseError) => {
const status = error ? error.statusCode || 400 : 200;
const body = error ? error : response;
server.respondWith('GET', '/api/upgrade_assistant/status', [
server.respondWith('GET', `${API_BASE_PATH}/status`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(body),
]);
};
const setLoadDeprecationLoggingResponse = (
response?: { isEnabled: boolean },
error?: ResponseError
) => {
const status = error ? error.statusCode || 400 : 200;
const body = error ? error : response;
server.respondWith('GET', `${API_BASE_PATH}/deprecation_logging`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(body),
]);
};
const setUpdateDeprecationLoggingResponse = (
response?: { isEnabled: boolean },
error?: ResponseError
) => {
const status = error ? error.statusCode || 400 : 200;
const body = error ? error : response;
server.respondWith('PUT', `${API_BASE_PATH}/deprecation_logging`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(body),
@ -25,7 +52,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
const setUpdateIndexSettingsResponse = (response?: object) => {
server.respondWith('POST', `/api/upgrade_assistant/:indexName/index_settings`, [
server.respondWith('POST', `${API_BASE_PATH}/:indexName/index_settings`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
@ -34,6 +61,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
return {
setLoadStatusResponse,
setLoadDeprecationLoggingResponse,
setUpdateDeprecationLoggingResponse,
setUpdateIndexSettingsResponse,
};
};

View file

@ -59,4 +59,7 @@ export type IndicesTestSubjects =
| 'expandAll'
| 'removeIndexSettingsButton'
| 'deprecationsContainer'
| 'permissionsError'
| 'upgradeStatusError'
| 'noDeprecationsPrompt'
| string;

View file

@ -22,4 +22,11 @@ export const setup = async (overrides?: any): Promise<OverviewTestBed> => {
return testBed;
};
export type OverviewTestSubjects = 'comingSoonPrompt' | 'upgradeAssistantPageContent';
export type OverviewTestSubjects =
| 'comingSoonPrompt'
| 'upgradeAssistantPageContent'
| 'upgradedPrompt'
| 'partiallyUpgradedPrompt'
| 'upgradeAssistantDeprecationToggle'
| 'deprecationLoggingStep'
| 'upgradeStatusError';

View file

@ -16,26 +16,30 @@ import { HttpSetup } from '../../../../../src/core/public';
import { mockKibanaSemverVersion, UA_READONLY_MODE } from '../../common/constants';
import { AppContextProvider } from '../../public/application/app_context';
import { init as initHttpRequests } from './http_requests';
import { apiService } from '../../public/application/lib/api';
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
const contextValue = {
http: (mockHttpClient as unknown) as HttpSetup,
isCloudEnabled: false,
docLinks: docLinksServiceMock.createStartContract(),
kibanaVersionInfo: {
currentMajor: mockKibanaSemverVersion.major,
prevMajor: mockKibanaSemverVersion.major - 1,
nextMajor: mockKibanaSemverVersion.major + 1,
},
isReadOnlyMode: UA_READONLY_MODE,
notifications: notificationServiceMock.createStartContract(),
};
export const WithAppDependencies = (
Comp: React.FunctionComponent<Record<string, unknown>>,
overrides: Record<string, unknown> = {}
) => (props: Record<string, unknown>) => {
apiService.setup((mockHttpClient as unknown) as HttpSetup);
const contextValue = {
http: (mockHttpClient as unknown) as HttpSetup,
isCloudEnabled: false,
docLinks: docLinksServiceMock.createStartContract(),
kibanaVersionInfo: {
currentMajor: mockKibanaSemverVersion.major,
prevMajor: mockKibanaSemverVersion.major - 1,
nextMajor: mockKibanaSemverVersion.major + 1,
},
isReadOnlyMode: UA_READONLY_MODE,
notifications: notificationServiceMock.createStartContract(),
api: apiService,
};
return (
<AppContextProvider value={{ ...contextValue, ...overrides }}>
<Comp {...props} />

View file

@ -15,79 +15,179 @@ describe('Indices tab', () => {
let testBed: IndicesTestBed;
const { server, httpRequestsMockHelpers } = setupEnvironment();
const upgradeStatusMockResponse = {
readyForUpgrade: false,
cluster: [],
indices: [
{
level: 'warning' as MIGRATION_DEPRECATION_LEVEL,
message: indexSettingDeprecations.translog.deprecationMessage,
url: 'doc_url',
index: 'my_index',
deprecatedIndexSettings: indexSettingDeprecations.translog.settings,
},
],
};
httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
beforeEach(async () => {
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
});
// Navigate to the indices tab
testBed.actions.clickTab('indices');
});
afterAll(() => {
server.restore();
});
describe('Fix indices button', () => {
test('removes deprecated index settings', async () => {
const { component, actions, exists, find } = testBed;
describe('with deprecations', () => {
const upgradeStatusMockResponse = {
readyForUpgrade: false,
cluster: [],
indices: [
{
level: 'warning' as MIGRATION_DEPRECATION_LEVEL,
message: indexSettingDeprecations.translog.deprecationMessage,
url: 'doc_url',
index: 'my_index',
deprecatedIndexSettings: indexSettingDeprecations.translog.settings,
},
],
};
expect(exists('deprecationsContainer')).toBe(true);
// Open all deprecations
actions.clickExpandAll();
const accordionTestSubj = `depgroup_${indexSettingDeprecations.translog.deprecationMessage
.split(' ')
.join('_')}`;
beforeEach(async () => {
httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true });
await act(async () => {
find(`${accordionTestSubj}.removeIndexSettingsButton`).simulate('click');
testBed = await setupIndicesPage({ isReadOnlyMode: false });
});
const modal = document.body.querySelector(
'[data-test-subj="indexSettingsDeleteConfirmModal"]'
);
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
'[data-test-subj="confirmModalConfirmButton"]'
);
expect(modal).not.toBe(null);
expect(modal!.textContent).toContain('Remove deprecated settings');
const indexName = upgradeStatusMockResponse.indices[0].index;
httpRequestsMockHelpers.setUpdateIndexSettingsResponse({
acknowledged: true,
});
await act(async () => {
confirmButton!.click();
});
const { actions, component } = testBed;
component.update();
const request = server.requests[server.requests.length - 1];
// Navigate to the indices tab
await act(async () => {
actions.clickTab('indices');
});
expect(request.method).toBe('POST');
expect(request.url).toBe(`/api/upgrade_assistant/${indexName}/index_settings`);
expect(request.status).toEqual(200);
component.update();
});
test('renders deprecations', () => {
const { exists, find } = testBed;
expect(exists('deprecationsContainer')).toBe(true);
expect(find('indexCount').text()).toEqual('1');
});
describe('fix indices button', () => {
test('removes deprecated index settings', async () => {
const { component, actions, exists, find } = testBed;
expect(exists('deprecationsContainer')).toBe(true);
// Open all deprecations
actions.clickExpandAll();
const accordionTestSubj = `depgroup_${indexSettingDeprecations.translog.deprecationMessage
.split(' ')
.join('_')}`;
await act(async () => {
find(`${accordionTestSubj}.removeIndexSettingsButton`).simulate('click');
});
// We need to read the document "body" as the modal is added there and not inside
// the component DOM tree.
const modal = document.body.querySelector(
'[data-test-subj="indexSettingsDeleteConfirmModal"]'
);
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
'[data-test-subj="confirmModalConfirmButton"]'
);
expect(modal).not.toBe(null);
expect(modal!.textContent).toContain('Remove deprecated settings');
const indexName = upgradeStatusMockResponse.indices[0].index;
httpRequestsMockHelpers.setUpdateIndexSettingsResponse({
acknowledged: true,
});
await act(async () => {
confirmButton!.click();
});
component.update();
const request = server.requests[server.requests.length - 1];
expect(request.method).toBe('POST');
expect(request.url).toBe(`/api/upgrade_assistant/${indexName}/index_settings`);
expect(request.status).toEqual(200);
});
});
});
describe('no deprecations', () => {
beforeEach(async () => {
const noDeprecationsResponse = {
readyForUpgrade: false,
cluster: [],
indices: [],
};
httpRequestsMockHelpers.setLoadStatusResponse(noDeprecationsResponse);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
});
const { actions, component } = testBed;
component.update();
// Navigate to the indices tab
await act(async () => {
actions.clickTab('indices');
});
component.update();
});
test('renders prompt', () => {
const { exists, find } = testBed;
expect(exists('noDeprecationsPrompt')).toBe(true);
expect(find('noDeprecationsPrompt').text()).toContain('All clear!');
});
});
describe('error handling', () => {
test('handles 403', async () => {
const error = {
statusCode: 403,
error: 'Forbidden',
message: 'Forbidden',
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
});
const { component, exists, find } = testBed;
component.update();
expect(exists('permissionsError')).toBe(true);
expect(find('permissionsError').text()).toContain(
'You do not have sufficient privileges to view this page.'
);
});
test('handles generic error', async () => {
const error = {
statusCode: 500,
error: 'Internal server error',
message: 'Internal server error',
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
});
const { component, exists, find } = testBed;
component.update();
expect(exists('upgradeStatusError')).toBe(true);
expect(find('upgradeStatusError').text()).toContain(
'An error occurred while retrieving the checkup results.'
);
});
});
});

View file

@ -7,7 +7,7 @@
import { act } from 'react-dom/test-utils';
import { OverviewTestBed, setupOverviewPage } from './helpers';
import { OverviewTestBed, setupOverviewPage, setupEnvironment } from './helpers';
describe('Overview page', () => {
let testBed: OverviewTestBed;
@ -27,20 +27,160 @@ describe('Overview page', () => {
});
});
describe('Tabs', () => {
describe('Overview content', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
const upgradeStatusMockResponse = {
readyForUpgrade: false,
cluster: [],
indices: [],
};
httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true });
beforeEach(async () => {
await act(async () => {
// Override the default context value to verify tab content renders as expected
// This will be the default behavior on the last minor before the next major release (e.g., v7.15)
testBed = await setupOverviewPage({ isReadOnlyMode: false });
});
testBed.component.update();
});
test('renders the Upgrade Assistant overview tab', () => {
afterAll(() => {
server.restore();
});
test('renders the overview tab', () => {
const { exists } = testBed;
expect(exists('comingSoonPrompt')).toBe(false);
expect(exists('upgradeAssistantPageContent')).toBe(true);
});
describe('Deprecation logging', () => {
test('toggles deprecation logging', async () => {
const { form, find, component } = testBed;
httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse({ isEnabled: false });
expect(find('upgradeAssistantDeprecationToggle').props()['aria-checked']).toBe(true);
expect(find('upgradeAssistantDeprecationToggle').props().disabled).toBe(false);
expect(find('deprecationLoggingStep').find('.euiSwitch__label').text()).toContain('On');
await act(async () => {
form.toggleEuiSwitch('upgradeAssistantDeprecationToggle');
});
component.update();
expect(find('upgradeAssistantDeprecationToggle').props()['aria-checked']).toBe(false);
expect(find('upgradeAssistantDeprecationToggle').props().disabled).toBe(false);
expect(find('deprecationLoggingStep').find('.euiSwitch__label').text()).toContain('Off');
});
test('handles network error', async () => {
const error = {
statusCode: 500,
error: 'Internal server error',
message: 'Internal server error',
};
const { form, find, component } = testBed;
httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(undefined, error);
expect(find('upgradeAssistantDeprecationToggle').props()['aria-checked']).toBe(true);
expect(find('upgradeAssistantDeprecationToggle').props().disabled).toBe(false);
expect(find('deprecationLoggingStep').find('.euiSwitch__label').text()).toContain('On');
await act(async () => {
form.toggleEuiSwitch('upgradeAssistantDeprecationToggle');
});
component.update();
expect(find('upgradeAssistantDeprecationToggle').props()['aria-checked']).toBe(true);
expect(find('upgradeAssistantDeprecationToggle').props().disabled).toBe(true);
expect(find('deprecationLoggingStep').find('.euiSwitch__label').text()).toContain(
'Could not load logging state'
);
});
});
describe('Error handling', () => {
test('handles network failure', async () => {
const error = {
statusCode: 500,
error: 'Internal server error',
message: 'Internal server error',
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
});
const { component, exists, find } = testBed;
component.update();
expect(exists('upgradeStatusError')).toBe(true);
expect(find('upgradeStatusError').text()).toContain(
'An error occurred while retrieving the checkup results.'
);
});
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.setLoadStatusResponse(undefined, error);
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
});
const { component, exists, find } = testBed;
component.update();
expect(exists('partiallyUpgradedPrompt')).toBe(true);
expect(find('partiallyUpgradedPrompt').text()).toContain('Your cluster is upgrading');
});
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.setLoadStatusResponse(undefined, error);
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
});
const { component, exists, find } = testBed;
component.update();
expect(exists('upgradedPrompt')).toBe(true);
expect(find('upgradedPrompt').text()).toContain('Your cluster has been upgraded');
});
});
});
});