Skip to content

Commit

Permalink
Add rolling upgrade interstitials to UA (elastic#112907)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
cjcenizal authored and sabarasaba committed Oct 26, 2021
1 parent 458a318 commit e4a524c
Show file tree
Hide file tree
Showing 22 changed files with 419 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -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),
};
};
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ export const getAppContextMock = () => ({
isCloudEnabled: false,
},
},
clusterUpgradeState: 'isPreparingForUpgrade',
isClusterUpgradeStateError: () => {},
handleClusterUpgradeStateError: () => {},
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
*/

import sinon, { SinonFakeServer } from 'sinon';

import { API_BASE_PATH } from '../../../common/constants';
import {
CloudBackupStatus,
ESUpgradeStatus,
DeprecationLoggingStatus,
ResponseError,
} from '../../../common/types';
import { ResponseError } from '../../../public/application/lib/api';

// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/upgrade_assistant/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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;
10 changes: 10 additions & 0 deletions x-pack/plugins/upgrade_assistant/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ 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,
Expand Down
123 changes: 116 additions & 7 deletions x-pack/plugins/upgrade_assistant/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,109 @@
* 2.0.
*/

import React from 'react';
import React, { useState, useEffect } from 'react';
import { Router, Switch, Route, Redirect } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt, EuiPageContent, EuiLoadingSpinner } from '@elastic/eui';
import { ScopedHistory } from 'src/core/public';

import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { API_BASE_PATH } from '../../common/constants';
import { ClusterUpgradeState } from '../../common/types';
import { APP_WRAPPER_CLASS, GlobalFlyout, AuthorizationProvider } from '../shared_imports';
import { AppDependencies } from '../types';
import { API_BASE_PATH } from '../../common/constants';
import { AppContextProvider, useAppContext } from './app_context';
import { EsDeprecations, ComingSoonPrompt, KibanaDeprecations, Overview } from './components';

const { GlobalFlyoutProvider } = GlobalFlyout;

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} />
Expand All @@ -36,10 +118,37 @@ const App: React.FunctionComponent = () => {
);
};

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>
);
};
Expand All @@ -56,7 +165,7 @@ export const RootComponent = (dependencies: AppDependencies) => {
<i18n.Context>
<AppContextProvider value={dependencies}>
<GlobalFlyoutProvider>
<AppWithRouter history={history} />
<App history={history} />
</GlobalFlyoutProvider>
</AppContextProvider>
</i18n.Context>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ import {
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 type { Status } from '../../../types';
import { DeprecationBadge } from '../../../shared';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit e4a524c

Please sign in to comment.