Skip to content

Commit

Permalink
Refactor UA Overview to support step-completion (#111243)
Browse files Browse the repository at this point in the history
* Refactor UA Overview to store step-completion state at the root and delegate step-completion logic to each step component.
* Add completion status to logs and issues steps
  • Loading branch information
cjcenizal authored Sep 8, 2021
1 parent df07fbc commit af9d3f5
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
*/

export { setupEnvironment, WithAppDependencies } from './setup_environment';
export { advanceTime } from './time_manipulation';
export { kibanaDeprecationsServiceHelpers } from './kibana_deprecations_service.mock';
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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';

/**
* These helpers are intended to be used in conjunction with jest.useFakeTimers().
*/

const flushPromiseJobQueue = async () => {
// See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function
await Promise.resolve();
};

export const advanceTime = async (ms: number) => {
await act(async () => {
jest.advanceTimersByTime(ms);
await flushPromiseJobQueue();
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
* 2.0.
*/

import { act } from 'react-dom/test-utils';

import { setupEnvironment } from '../../helpers';
import { CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS } from '../../../../common/constants';
import { setupEnvironment, advanceTime } from '../../helpers';
import { OverviewTestBed, setupOverviewPage } from '../overview.helpers';

describe('Overview - Backup Step', () => {
Expand All @@ -34,6 +33,11 @@ describe('Overview - Backup Step', () => {
expect(exists('snapshotRestoreLink')).toBe(true);
expect(find('snapshotRestoreLink').props().href).toBe('snapshotAndRestoreUrl');
});

test('renders step as incomplete ', () => {
const { exists } = testBed;
expect(exists('backupStep-incomplete')).toBe(true);
});
});

describe('On Cloud', () => {
Expand Down Expand Up @@ -104,6 +108,11 @@ describe('Overview - Backup Step', () => {
expect(exists('cloudSnapshotsLink')).toBe(true);
expect(find('dataBackedUpStatus').text()).toContain('Last snapshot created on');
});

test('renders step as complete ', () => {
const { exists } = testBed;
expect(exists('backupStep-complete')).toBe(true);
});
});

describe(`when data isn't backed up`, () => {
Expand All @@ -121,24 +130,47 @@ describe('Overview - Backup Step', () => {
expect(exists('dataNotBackedUpStatus')).toBe(true);
expect(exists('cloudSnapshotsLink')).toBe(true);
});

test('renders step as incomplete ', () => {
const { exists } = testBed;
expect(exists('backupStep-incomplete')).toBe(true);
});
});
});

// FLAKY: https://github.com/elastic/kibana/issues/111255
test.skip('polls for new status', async () => {
// The behavior we're testing involves state changes over time, so we need finer control over
// timing.
describe('poll for new status', () => {
beforeEach(async () => {
jest.useFakeTimers();
testBed = await setupCloudOverviewPage();
expect(server.requests.length).toBe(4);

// Resolve the polling timeout.
await act(async () => {
jest.runAllTimers();
// First request will succeed.
httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({
isBackedUp: true,
lastBackupTime: '2021-08-25T19:59:59.863Z',
});

expect(server.requests.length).toBe(5);
testBed = await setupCloudOverviewPage();
});

afterEach(() => {
jest.useRealTimers();
});

test('renders step as incomplete when a success state is followed by an error state', async () => {
const { exists } = testBed;
expect(exists('backupStep-complete')).toBe(true);

// Second request will error.
httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, {
statusCode: 400,
message: 'error',
});

// Resolve the polling timeout.
await advanceTime(CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS);
testBed.component.update();

expect(exists('backupStep-incomplete')).toBe(true);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,37 @@ describe('Overview - Fix deprecation issues step', () => {
server.restore();
});

describe('Step status', () => {
test(`It's complete when there are no critical deprecations`, async () => {
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsEmpty);

await act(async () => {
const deprecationService = deprecationsServiceMock.createStartContract();
deprecationService.getAllDeprecations = jest.fn().mockRejectedValue([]);

testBed = await setupOverviewPage({
services: {
core: {
deprecations: deprecationService,
},
},
});
});

const { exists, component } = testBed;

component.update();

expect(exists(`fixIssuesStep-complete`)).toBe(true);
});

test(`It's incomplete when there are critical deprecations`, async () => {
const { exists } = testBed;

expect(exists(`fixIssuesStep-incomplete`)).toBe(true);
});
});

describe('ES deprecations', () => {
test('Shows deprecation warning and critical counts', () => {
const { exists, find } = testBed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,49 @@ describe('Overview - Fix deprecation logs step', () => {
server.restore();
});

describe('Step status', () => {
test(`It's complete when there are no deprecation logs since last checkpoint`, async () => {
httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(true));

httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({
count: 0,
});

await act(async () => {
testBed = await setupOverviewPage();
});

const { exists, component } = testBed;

component.update();

expect(exists(`fixLogsStep-complete`)).toBe(true);
});

test(`It's incomplete when there are deprecation logs since last checkpoint`, async () => {
httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(true));

httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({
count: 5,
});

await act(async () => {
testBed = await setupOverviewPage();
});

const { exists, component } = testBed;

component.update();

expect(exists(`fixLogsStep-incomplete`)).toBe(true);
});
});

describe('Step 1 - Toggle log writing and collecting', () => {
test('toggles deprecation logging', async () => {
const { find, actions } = testBed;

httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse({
isDeprecationLogIndexingEnabled: false,
isDeprecationLoggingEnabled: false,
});
httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(false));

expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,34 @@ import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';

import type { CloudSetup } from '../../../../../../cloud/public';
import { OnPremBackup } from './on_prem_backup';
import { CloudBackup, CloudBackupStatusResponse } from './cloud_backup';
import { CloudBackup } from './cloud_backup';
import type { OverviewStepProps } from '../../types';

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

interface Props {
interface Props extends OverviewStepProps {
cloud?: CloudSetup;
cloudBackupStatusResponse?: CloudBackupStatusResponse;
}

export const getBackupStep = ({ cloud, cloudBackupStatusResponse }: Props): EuiStepProps => {
export const getBackupStep = ({ cloud, isComplete, setIsComplete }: Props): EuiStepProps => {
const status = isComplete ? 'complete' : 'incomplete';

if (cloud?.isCloudEnabled) {
return {
status,
title,
status: cloudBackupStatusResponse!.data?.isBackedUp ? 'complete' : 'incomplete',
'data-test-subj': `backupStep-${status}`,
children: (
<CloudBackup
cloudBackupStatusResponse={cloudBackupStatusResponse!}
cloudSnapshotsUrl={cloud!.snapshotsUrl!}
/>
<CloudBackup cloudSnapshotsUrl={cloud!.snapshotsUrl!} setIsComplete={setIsComplete} />
),
};
}

return {
title,
'data-test-subj': 'backupStep-incomplete',
status: 'incomplete',
children: <OnPremBackup />,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React from 'react';
import React, { useEffect } from 'react';
import moment from 'moment-timezone';
import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
Expand All @@ -20,22 +20,39 @@ import {
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>;
import { useAppContext } from '../../../app_context';

interface Props {
cloudBackupStatusResponse: UseRequestResponse<CloudBackupStatus, ResponseError>;
cloudSnapshotsUrl: string;
setIsComplete: (isComplete: boolean) => void;
}

export const CloudBackup: React.FunctionComponent<Props> = ({
cloudBackupStatusResponse,
cloudSnapshotsUrl,
setIsComplete,
}) => {
const { isInitialRequest, isLoading, error, data, resendRequest } = cloudBackupStatusResponse;
const {
services: { api },
} = useAppContext();

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

// Tell overview whether the step is complete or not.
useEffect(() => {
// Loading shouldn't invalidate the previous state.
if (!isLoading) {
// An error should invalidate the previous state.
setIsComplete((!error && data?.isBackedUp) ?? false);
}
// Depending upon setIsComplete would create an infinite loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error, isLoading, data]);

if (isInitialRequest && isLoading) {
return <EuiLoadingContent data-test-subj="cloudBackupLoading" lines={3} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useEffect, useMemo } from 'react';
import { useHistory } from 'react-router-dom';

import { EuiStat, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiCard } from '@elastic/eui';
Expand Down Expand Up @@ -35,18 +35,33 @@ const i18nTexts = {
),
};

export const ESDeprecationStats: FunctionComponent = () => {
interface Props {
setIsFixed: (isFixed: boolean) => void;
}

export const ESDeprecationStats: FunctionComponent<Props> = ({ setIsFixed }) => {
const history = useHistory();
const {
services: { api },
} = useAppContext();

const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations();

const warningDeprecations =
esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) || [];
const criticalDeprecations =
esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical) || [];
const warningDeprecations = useMemo(
() =>
esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) || [],
[esDeprecations]
);
const criticalDeprecations = useMemo(
() => esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical) || [],
[esDeprecations]
);

useEffect(() => {
if (!isLoading && !error) {
setIsFixed(criticalDeprecations.length === 0);
}
}, [setIsFixed, criticalDeprecations, isLoading, error]);

const hasWarnings = warningDeprecations.length > 0;
const hasCritical = criticalDeprecations.length > 0;
Expand Down
Loading

0 comments on commit af9d3f5

Please sign in to comment.