Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rolling upgrade interstitials to UA #112907

Merged
merged 14 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -38,6 +38,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 @@ -13,6 +13,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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about merging this component and AppWithRouter? they both feel quite small and it might reduce some of the overhead of reading this file which already has a bunch of components

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