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

This commit is contained in:
CJ Cenizal 2021-08-27 06:43:56 -07:00 committed by GitHub
parent f04c915622
commit 986ac3b373
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 314 additions and 63 deletions

View file

@ -26,7 +26,7 @@ const idToUrlMap = {
const shareMock = sharePluginMock.createSetupContract();
shareMock.url.locators.get = (id) => ({
// @ts-expect-error This object is missing some properties that we're not using in the UI
getUrl: (): string | undefined => idToUrlMap[id],
useUrl: (): string | undefined => idToUrlMap[id],
});
export const getAppContextMock = (mockHttpClient: HttpSetup) => ({

View file

@ -28,9 +28,15 @@ export const indexSettingDeprecations = {
export const API_BASE_PATH = '/api/upgrade_assistant';
/**
* This is the repository where Cloud stores its backup snapshots.
*/
export const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots';
export const DEPRECATION_WARNING_UPPER_LIMIT = 999999;
export const DEPRECATION_LOGS_SOURCE_ID = 'deprecation_logs';
export const DEPRECATION_LOGS_INDEX = '.logs-deprecation.elasticsearch-default';
export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-default';
export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000;
export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 60000;

View file

@ -218,6 +218,11 @@ export interface EnrichedDeprecationInfo
resolveDuringUpgrade: boolean;
}
export interface CloudBackupStatus {
isBackedUp: boolean;
lastBackupTime?: string;
}
export interface ESUpgradeStatus {
totalCriticalDeprecations: number;
deprecations: EnrichedDeprecationInfo[];

View file

@ -5,72 +5,40 @@
* 2.0.
*/
import React, { useState, useEffect } from 'react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import { useAppContext } from '../../../app_context';
import type { CloudSetup } from '../../../../../../cloud/public';
import { OnPremBackup } from './on_prem_backup';
import { CloudBackup, CloudBackupStatusResponse } from './cloud_backup';
const i18nTexts = {
backupStepTitle: i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', {
defaultMessage: 'Back up your data',
}),
const title = i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', {
defaultMessage: 'Back up your data',
});
backupStepDescription: i18n.translate('xpack.upgradeAssistant.overview.backupStepDescription', {
defaultMessage: 'Back up your data before addressing any deprecation warnings.',
}),
};
interface Props {
cloud?: CloudSetup;
cloudBackupStatusResponse?: CloudBackupStatusResponse;
}
const SnapshotRestoreAppLink: React.FunctionComponent = () => {
const { share } = useAppContext();
const [snapshotRestoreUrl, setSnapshotRestoreUrl] = useState<string | undefined>();
useEffect(() => {
const getSnapshotRestoreUrl = async () => {
const locator = share.url.locators.get('SNAPSHOT_RESTORE_LOCATOR');
if (!locator) {
return;
}
const url = await locator.getUrl({
page: 'snapshots',
});
setSnapshotRestoreUrl(url);
export const getBackupStep = ({ cloud, cloudBackupStatusResponse }: Props): EuiStepProps => {
if (cloud?.isCloudEnabled) {
return {
title,
status: cloudBackupStatusResponse!.data?.isBackedUp ? 'complete' : 'incomplete',
children: (
<CloudBackup
cloudBackupStatusResponse={cloudBackupStatusResponse!}
cloudSnapshotsUrl={`${cloud!.deploymentUrl}/elasticsearch/snapshots`}
/>
),
};
}
getSnapshotRestoreUrl();
}, [share]);
return (
<EuiButton href={snapshotRestoreUrl} data-test-subj="snapshotRestoreLink">
{i18n.translate('xpack.upgradeAssistant.overview.snapshotRestoreLink', {
defaultMessage: 'Create snapshot',
})}
</EuiButton>
);
};
const BackupStep: React.FunctionComponent = () => {
return (
<>
<EuiText>
<p>{i18nTexts.backupStepDescription}</p>
</EuiText>
<EuiSpacer size="s" />
<SnapshotRestoreAppLink />
</>
);
};
export const getBackupStep = (): EuiStepProps => {
return {
title: i18nTexts.backupStepTitle,
title,
status: 'incomplete',
children: <BackupStep />,
children: <OnPremBackup />,
};
};

View file

@ -0,0 +1,136 @@
/*
* 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 moment from 'moment-timezone';
import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiLoadingContent,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiText,
EuiButton,
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
import { CloudBackupStatus } from '../../../../../common/types';
import { UseRequestResponse } from '../../../../shared_imports';
import { ResponseError } from '../../../lib/api';
export type CloudBackupStatusResponse = UseRequestResponse<CloudBackupStatus, ResponseError>;
interface Props {
cloudBackupStatusResponse: UseRequestResponse<CloudBackupStatus, ResponseError>;
cloudSnapshotsUrl: string;
}
export const CloudBackup: React.FunctionComponent<Props> = ({
cloudBackupStatusResponse,
cloudSnapshotsUrl,
}) => {
const { isInitialRequest, isLoading, error, data, resendRequest } = cloudBackupStatusResponse;
if (isInitialRequest && isLoading) {
return <EuiLoadingContent lines={3} />;
}
if (error) {
return (
<EuiCallOut
title={i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.loadingError', {
defaultMessage: 'An error occurred while retrieving the latest snapshot status',
})}
color="danger"
iconType="alert"
data-test-subj="cloudBackupErrorCallout"
>
<p>
{error.statusCode} - {error.message}
</p>
<EuiButton color="danger" onClick={resendRequest} data-test-subj="cloudBackupRetryButton">
{i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.retryButton', {
defaultMessage: 'Try again',
})}
</EuiButton>
</EuiCallOut>
);
}
const lastBackupTime = moment(data!.lastBackupTime).toISOString();
const statusMessage = data!.isBackedUp ? (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="check" color="success" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.overview.cloudBackup.hasSnapshotMessage"
defaultMessage="Last snapshot created on {lastBackupTime}."
values={{
lastBackupTime: (
<>
<FormattedDate
value={lastBackupTime}
year="numeric"
month="long"
day="2-digit"
/>{' '}
<FormattedTime value={lastBackupTime} timeZoneName="short" hour12={false} />
</>
),
}}
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="alert" color="danger" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>
{i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.noSnapshotMessage', {
defaultMessage: `Your data isn't backed up.`,
})}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<>
{statusMessage}
<EuiSpacer size="s" />
<EuiButton
href={cloudSnapshotsUrl}
data-test-subj="cloudSnapshotsLink"
target="_blank"
iconType="popout"
iconSide="right"
>
<FormattedMessage
id="xpack.upgradeAssistant.overview.cloudBackup.snapshotsLink"
defaultMessage="Create snapshot"
/>
</EuiButton>
</>
);
};

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
import { useAppContext } from '../../../app_context';
const SnapshotRestoreAppLink: React.FunctionComponent = () => {
const { share } = useAppContext();
const snapshotRestoreUrl = share.url.locators
.get('SNAPSHOT_RESTORE_LOCATOR')
?.useUrl({ page: 'snapshots' });
return (
<EuiButton href={snapshotRestoreUrl} data-test-subj="snapshotRestoreLink">
<FormattedMessage
id="xpack.upgradeAssistant.overview.snapshotRestoreLink"
defaultMessage="Create snapshot"
/>
</EuiButton>
);
};
export const OnPremBackup: React.FunctionComponent = () => {
return (
<>
<EuiText>
<p>
{i18n.translate('xpack.upgradeAssistant.overview.backupStepDescription', {
defaultMessage: 'Back up your data before addressing any deprecation issues.',
})}
</p>
</EuiText>
<EuiSpacer size="s" />
<SnapshotRestoreAppLink />
</>
);
};

View file

@ -20,6 +20,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../shared_imports';
import { useAppContext } from '../../app_context';
import { getBackupStep } from './backup_step';
import { getFixIssuesStep } from './fix_issues_step';
@ -27,6 +28,9 @@ import { getFixLogsStep } from './fix_logs_step';
import { getUpgradeStep } from './upgrade_step';
export const Overview: FunctionComponent = () => {
const {
services: { cloud },
} = useKibana();
const { kibanaVersionInfo, breadcrumbs, docLinks, api } = useAppContext();
const { nextMajor } = kibanaVersionInfo;
@ -44,6 +48,12 @@ export const Overview: FunctionComponent = () => {
breadcrumbs.setBreadcrumbs('overview');
}, [breadcrumbs]);
let cloudBackupStatusResponse;
if (cloud?.isCloudEnabled) {
cloudBackupStatusResponse = api.useLoadCloudBackupStatus();
}
return (
<EuiPageBody restrictWidth={true}>
<EuiPageContent horizontalPosition="center" color="transparent" paddingSize="none">
@ -84,7 +94,7 @@ export const Overview: FunctionComponent = () => {
<EuiSteps
steps={[
getBackupStep(),
getBackupStep({ cloud, cloudBackupStatusResponse }),
getFixIssuesStep({ nextMajor }),
getFixLogsStep(),
getUpgradeStep({ docLinks, nextMajor }),

View file

@ -6,8 +6,13 @@
*/
import { HttpSetup } from 'src/core/public';
import { ESUpgradeStatus } from '../../../common/types';
import { API_BASE_PATH, DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS } from '../../../common/constants';
import { ESUpgradeStatus, CloudBackupStatus } from '../../../common/types';
import {
API_BASE_PATH,
DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS,
CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS,
} from '../../../common/constants';
import {
UseRequestConfig,
SendRequestConfig,
@ -45,6 +50,14 @@ export class ApiService {
this.client = httpClient;
}
public useLoadCloudBackupStatus() {
return this.useRequest<CloudBackupStatus>({
path: `${API_BASE_PATH}/cloud_backup_status`,
method: 'get',
pollIntervalMs: CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS,
});
}
public useLoadEsDeprecations() {
return this.useRequest<ESUpgradeStatus>({
path: `${API_BASE_PATH}/es_deprecations`,

View file

@ -14,6 +14,7 @@ export {
SendRequestResponse,
useRequest,
UseRequestConfig,
UseRequestResponse,
SectionLoading,
GlobalFlyout,
} from '../../../../src/plugins/es_ui_shared/public/';

View file

@ -20,6 +20,7 @@ import { InfraPluginSetup } from '../../infra/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants';
import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store';
import { ReindexWorker } from './lib/reindexing';
@ -32,7 +33,7 @@ import {
reindexOperationSavedObjectType,
mlSavedObjectType,
} from './saved_object_types';
import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants';
import { handleEsError } from './shared_imports';
import { RouteDependencies } from './types';
@ -119,6 +120,9 @@ export class UpgradeAssistantServerPlugin implements Plugin {
}
return this.savedObjectsServiceStart;
},
lib: {
handleEsError,
},
};
// Initialize version service with current kibana version

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { API_BASE_PATH, CLOUD_SNAPSHOT_REPOSITORY } from '../../common/constants';
import { versionCheckHandlerWrapper } from '../lib/es_version_precheck';
import { RouteDependencies } from '../types';
export function registerCloudBackupStatusRoutes({
router,
lib: { handleEsError },
}: RouteDependencies) {
// GET most recent Cloud snapshot
router.get(
{ path: `${API_BASE_PATH}/cloud_backup_status`, validate: false },
versionCheckHandlerWrapper(async (context, request, response) => {
const { client: clusterClient } = context.core.elasticsearch;
try {
const {
body: { snapshots },
} = await clusterClient.asCurrentUser.snapshot.get({
repository: CLOUD_SNAPSHOT_REPOSITORY,
snapshot: '_all',
ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable.
// @ts-expect-error @elastic/elasticsearch "desc" is a new param
order: 'desc',
sort: 'start_time',
size: 1,
});
let isBackedUp = false;
let lastBackupTime;
if (snapshots && snapshots[0]) {
isBackedUp = true;
lastBackupTime = snapshots![0].start_time;
}
return response.ok({
body: {
isBackedUp,
lastBackupTime,
},
});
} catch (error) {
return handleEsError({ error, response });
}
})
);
}

View file

@ -7,6 +7,7 @@
import { RouteDependencies } from '../types';
import { registerCloudBackupStatusRoutes } from './cloud_backup_status';
import { registerESDeprecationRoutes } from './es_deprecations';
import { registerDeprecationLoggingRoutes } from './deprecation_logging';
import { registerReindexIndicesRoutes } from './reindex_indices';
@ -17,6 +18,7 @@ import { ReindexWorker } from '../lib/reindexing';
import { registerUpgradeStatusRoute } from './status';
export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) {
registerCloudBackupStatusRoutes(dependencies);
registerESDeprecationRoutes(dependencies);
registerDeprecationLoggingRoutes(dependencies);
registerReindexIndicesRoutes(dependencies, getWorker);

View file

@ -6,8 +6,9 @@
*/
import { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server';
import { CredentialStore } from './lib/reindexing/credential_store';
import { LicensingPluginSetup } from '../../licensing/server';
import { CredentialStore } from './lib/reindexing/credential_store';
import { handleEsError } from './shared_imports';
export interface RouteDependencies {
router: IRouter;
@ -15,4 +16,7 @@ export interface RouteDependencies {
log: Logger;
getSavedObjectsService: () => SavedObjectsServiceStart;
licensing: LicensingPluginSetup;
lib: {
handleEsError: typeof handleEsError;
};
}