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 on-Cloud state to Upgrade Assistant 'Back up data' step #109956

Merged
merged 8 commits into from
Aug 27, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/upgrade_assistant/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export const API_BASE_PATH = '/api/upgrade_assistant';
export const DEPRECATION_WARNING_UPPER_LIMIT = 999999;
export const DEPRECATION_LOGS_SOURCE_ID = 'deprecation_logs';
export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-default';

export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000;
5 changes: 5 additions & 0 deletions x-pack/plugins/upgrade_assistant/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ export interface EnrichedDeprecationInfo
resolveDuringUpgrade: boolean;
}

export interface CloudBackupStatus {
isBackedUp: boolean;
time?: string;
}

export interface ESUpgradeStatus {
totalCriticalDeprecations: number;
deprecations: EnrichedDeprecationInfo[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

const i18nTexts = {
backupStepTitle: 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.',
}),
};

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);
import { CloudSetup } from '../../../../../../cloud/public';
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Should we put this in shared_imports ?

import { OnPremBackup } from './on_prem_backup';
import { CloudBackup, CloudBackupStatus } from './cloud_backup';

const title = i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', {
defaultMessage: 'Back up your data',
});

interface Props {
cloud?: CloudSetup;
cloudBackupStatus: CloudBackupStatus;
}

export const getBackupStep = ({ cloud, cloudBackupStatus }: Props): EuiStepProps => {
if (cloud?.isCloudEnabled) {
return {
title,
status: cloudBackupStatus?.data?.isBackedUp ? 'complete' : 'incomplete',
children: (
<CloudBackup
cloudBackupStatus={cloudBackupStatus}
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 />,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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';

interface Props {
cloudBackupStatus: CloudBackupStatus;
cloudSnapshotsUrl: string;
}

export interface CloudBackupStatus {
isLoading?: boolean;
isInitialRequest?: boolean;
resendRequest?: () => void;
error?: any;
data?: {
isBackedUp: boolean;
time: string;
};
}

export const CloudBackup: React.FunctionComponent<Props> = ({
cloudBackupStatus: { isLoading, isInitialRequest, resendRequest, error, data },
cloudSnapshotsUrl,
}) => {
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 time = moment(data!.time).toISOString();
Copy link
Member

Choose a reason for hiding this comment

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

[nit] If we rename the server response, we should probably also rename this one


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 {time}."
values={{
time: (
<>
<FormattedDate value={time} year="numeric" month="long" day="2-digit" />{' '}
<FormattedTime value={time} 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" />

{/* TODO: move this link to the Cloud plugin where it's easier to track */}
Copy link
Member

Choose a reason for hiding this comment

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

Should we create a ticket for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! Done: #110311

<EuiButton
href={cloudSnapshotsUrl}
data-test-subj="cloudSnapshotsLink"
target="_blank"
Copy link
Member

Choose a reason for hiding this comment

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

As per Dmitrys feedback we should not open this in a new tab

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it's linking to Cloud, I think we want to open it in a new tab? @dborodyansky Could you please clarify?

Copy link
Member

Choose a reason for hiding this comment

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

Ohh you are right, I didn't notice it goes to cloud! 🙈

Copy link
Contributor

Choose a reason for hiding this comment

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

When linking to Snapshots on cloud.elastic.co, new tab. When linking to Snapshot and Restore in Kibana, same tab.

iconType="popout"
iconSide="right"
>
<FormattedMessage
id="xpack.upgradeAssistant.overview.cloudBackup.snapshotsLink"
defaultMessage="Create snapshot"
/>
</EuiButton>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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' });
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sabarasaba Do you know if it's possible to use useUrl instead of getUrl in the "Fix logs" step? It seems like this might let us remove a lot of code since it's a synchronous call.

Copy link
Member

Choose a reason for hiding this comment

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

I dont really know tbh, but Ill have a look at it as part of #109537


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.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
defaultMessage: 'Back up your data before addressing any deprecation issues.',
defaultMessage: 'Back up your data before addressing any deprecation warnings.',

})}
</p>
</EuiText>

<EuiSpacer size="s" />

<SnapshotRestoreAppLink />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ 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';
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;

Expand All @@ -44,6 +48,26 @@ export const Overview: FunctionComponent = () => {
breadcrumbs.setBreadcrumbs('overview');
}, [breadcrumbs]);

let cloudBackupStatus = {};

if (cloud?.isCloudEnabled) {
Copy link
Member

Choose a reason for hiding this comment

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

wdyt about instead of destructing everything and defining a new interface with everything optional, just passing whatever is returned from the useRequest into getBackupStep with a partial UseRequestConfig?

const {
data,
isLoading,
error,
isInitialRequest,
resendRequest,
} = api.useLoadCloudBackupStatus();

cloudBackupStatus = {
data,
isLoading,
error,
isInitialRequest,
resendRequest,
};
}

return (
<EuiPageBody restrictWidth={true}>
<EuiPageContent horizontalPosition="center" color="transparent" paddingSize="none">
Expand Down Expand Up @@ -84,7 +108,7 @@ export const Overview: FunctionComponent = () => {

<EuiSteps
steps={[
getBackupStep(),
getBackupStep({ cloud, cloudBackupStatus }),
getFixIssuesStep({ nextMajor }),
getFixLogsStep(),
getUpgradeStep({ docLinks, nextMajor }),
Expand Down
12 changes: 10 additions & 2 deletions x-pack/plugins/upgrade_assistant/public/application/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

import { HttpSetup } from 'src/core/public';
import { ESUpgradeStatus } from '../../../common/types';
import { API_BASE_PATH } from '../../../common/constants';
import { ESUpgradeStatus, CloudBackupStatus } from '../../../common/types';
import { API_BASE_PATH, CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS } from '../../../common/constants';
import {
UseRequestConfig,
SendRequestConfig,
Expand Down Expand Up @@ -45,6 +45,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`,
Expand Down
Loading