[Workplace Search] Add a download diagnostics button when error is shown (#121017)

* Factor out DownloadDiagnosticsButton for reuse

* Add logic for rendering button

* Add button to layout

* Fix test name

* Better method naming
This commit is contained in:
Scotty Bollinger 2021-12-09 18:26:59 -06:00 committed by GitHub
parent 34711539fa
commit 5e34758d43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 51 deletions

View file

@ -0,0 +1,51 @@
/*
* 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 { setMockValues } from '../../../../__mocks__/kea_logic';
import { fullContentSources } from '../../../__mocks__/content_sources.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiButton } from '@elastic/eui';
import { DownloadDiagnosticsButton } from './download_diagnostics_button';
describe('DownloadDiagnosticsButton', () => {
const label = 'foo123';
const contentSource = fullContentSources[0];
const buttonLoading = false;
const isOrganization = true;
const mockValues = {
contentSource,
buttonLoading,
isOrganization,
};
beforeEach(() => {
setMockValues(mockValues);
});
it('renders the Download diagnostics button with org href', () => {
const wrapper = shallow(<DownloadDiagnosticsButton label={label} />);
expect(wrapper.find(EuiButton).prop('href')).toEqual(
'/internal/workplace_search/org/sources/123/download_diagnostics'
);
});
it('renders the Download diagnostics button with account href', () => {
setMockValues({ ...mockValues, isOrganization: false });
const wrapper = shallow(<DownloadDiagnosticsButton label={label} />);
expect(wrapper.find(EuiButton).prop('href')).toEqual(
'/internal/workplace_search/account/sources/123/download_diagnostics'
);
});
});

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 { useValues } from 'kea';
import { EuiButton } from '@elastic/eui';
import { HttpLogic } from '../../../../shared/http';
import { AppLogic } from '../../../app_logic';
import { SourceLogic } from '../source_logic';
interface Props {
label: string;
}
export const DownloadDiagnosticsButton: React.FC<Props> = ({ label }) => {
const { http } = useValues(HttpLogic);
const { isOrganization } = useValues(AppLogic);
const {
contentSource: { id, serviceType },
buttonLoading,
} = useValues(SourceLogic);
const diagnosticsPath = isOrganization
? http.basePath.prepend(`/internal/workplace_search/org/sources/${id}/download_diagnostics`)
: http.basePath.prepend(
`/internal/workplace_search/account/sources/${id}/download_diagnostics`
);
return (
<EuiButton
target="_blank"
href={diagnosticsPath}
isLoading={buttonLoading}
data-test-subj="DownloadDiagnosticsButton"
download={`${id}_${serviceType}_${Date.now()}_diagnostics.json`}
>
{label}
</EuiButton>
);
};

View file

@ -18,6 +18,7 @@ import { EuiCallOut } from '@elastic/eui';
import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout';
import { DownloadDiagnosticsButton } from './download_diagnostics_button';
import { SourceInfoCard } from './source_info_card';
import { SourceLayout } from './source_layout';
@ -26,6 +27,7 @@ describe('SourceLayout', () => {
const mockValues = {
contentSource,
dataLoading: false,
diagnosticDownloadButtonVisible: false,
isOrganization: true,
};
@ -87,4 +89,14 @@ describe('SourceLayout', () => {
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
it('renders DownloadDiagnosticsButton', () => {
setMockValues({
...mockValues,
diagnosticDownloadButtonVisible: true,
});
const wrapper = shallow(<SourceLayout />);
expect(wrapper.find(DownloadDiagnosticsButton)).toHaveLength(1);
});
});

View file

@ -19,12 +19,14 @@ import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../c
import { NAV } from '../../../constants';
import {
DOWNLOAD_DIAGNOSTIC_BUTTON,
SOURCE_DISABLED_CALLOUT_TITLE,
SOURCE_DISABLED_CALLOUT_DESCRIPTION,
SOURCE_DISABLED_CALLOUT_BUTTON,
} from '../constants';
import { SourceLogic } from '../source_logic';
import { DownloadDiagnosticsButton } from './download_diagnostics_button';
import { SourceInfoCard } from './source_info_card';
export const SourceLayout: React.FC<PageTemplateProps> = ({
@ -32,7 +34,7 @@ export const SourceLayout: React.FC<PageTemplateProps> = ({
pageChrome = [],
...props
}) => {
const { contentSource, dataLoading } = useValues(SourceLogic);
const { contentSource, dataLoading, diagnosticDownloadButtonVisible } = useValues(SourceLogic);
const { isOrganization } = useValues(AppLogic);
const { name, createdAt, serviceType, isFederatedSource, supportedByLicense } = contentSource;
@ -61,6 +63,13 @@ export const SourceLayout: React.FC<PageTemplateProps> = ({
</>
);
const downloadDiagnosticButton = (
<>
<DownloadDiagnosticsButton label={DOWNLOAD_DIAGNOSTIC_BUTTON} />
<EuiSpacer size="xl" />
</>
);
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
return (
@ -69,6 +78,7 @@ export const SourceLayout: React.FC<PageTemplateProps> = ({
{...props}
pageChrome={[NAV.SOURCES, name || '...', ...pageChrome]}
>
{diagnosticDownloadButtonVisible && downloadDiagnosticButton}
{!supportedByLicense && callout}
{pageHeader}
{children}

View file

@ -18,6 +18,7 @@ import { EuiConfirmModal } from '@elastic/eui';
import { SourceConfigFields } from '../../../components/shared/source_config_fields';
import { DownloadDiagnosticsButton } from './download_diagnostics_button';
import { SourceSettings } from './source_settings';
describe('SourceSettings', () => {
@ -48,6 +49,7 @@ describe('SourceSettings', () => {
const wrapper = shallow(<SourceSettings />);
expect(wrapper.find('form')).toHaveLength(1);
expect(wrapper.find(DownloadDiagnosticsButton)).toHaveLength(1);
});
it('handles form submission', () => {
@ -104,36 +106,4 @@ describe('SourceSettings', () => {
sourceConfigData.configuredFields.publicKey
);
});
describe('DownloadDiagnosticsButton', () => {
it('renders for org with correct href', () => {
const wrapper = shallow(<SourceSettings />);
expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('href')).toEqual(
'/internal/workplace_search/org/sources/123/download_diagnostics'
);
});
it('renders for account with correct href', () => {
setMockValues({
...mockValues,
isOrganization: false,
});
const wrapper = shallow(<SourceSettings />);
expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('href')).toEqual(
'/internal/workplace_search/account/sources/123/download_diagnostics'
);
});
it('renders with the correct download file name', () => {
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => new Date('1970-01-01').valueOf());
const wrapper = shallow(<SourceSettings />);
expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('download')).toEqual(
'123_custom_0_diagnostics.json'
);
});
});
});

View file

@ -23,7 +23,6 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { HttpLogic } from '../../../../shared/http';
import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers';
import { AppLogic } from '../../../app_logic';
import { ContentSection } from '../../../components/shared/content_section';
@ -61,11 +60,11 @@ import {
import { staticSourceData } from '../source_data';
import { SourceLogic } from '../source_logic';
import { DownloadDiagnosticsButton } from './download_diagnostics_button';
import { SourceLayout } from './source_layout';
export const SourceSettings: React.FC = () => {
const { http } = useValues(HttpLogic);
const {
updateContentSource,
removeContentSource,
@ -110,12 +109,6 @@ export const SourceSettings: React.FC = () => {
const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {};
const diagnosticsPath = isOrganization
? http.basePath.prepend(`/internal/workplace_search/org/sources/${id}/download_diagnostics`)
: http.basePath.prepend(
`/internal/workplace_search/account/sources/${id}/download_diagnostics`
);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value);
const submitNameChange = (e: FormEvent) => {
@ -241,15 +234,7 @@ export const SourceSettings: React.FC = () => {
</ContentSection>
)}
<ContentSection title={SYNC_DIAGNOSTICS_TITLE} description={SYNC_DIAGNOSTICS_DESCRIPTION}>
<EuiButton
target="_blank"
href={diagnosticsPath}
isLoading={buttonLoading}
data-test-subj="DownloadDiagnosticsButton"
download={`${id}_${serviceType}_${Date.now()}_diagnostics.json`}
>
{SYNC_DIAGNOSTICS_BUTTON}
</EuiButton>
<DownloadDiagnosticsButton label={SYNC_DIAGNOSTICS_BUTTON} />
</ContentSection>
<ContentSection title={SOURCE_REMOVE_TITLE} description={SOURCE_REMOVE_DESCRIPTION}>
<EuiButton

View file

@ -370,6 +370,13 @@ export const SYNC_DIAGNOSTICS_BUTTON = i18n.translate(
}
);
export const DOWNLOAD_DIAGNOSTIC_BUTTON = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.downloadDiagnosticButton',
{
defaultMessage: 'Download diagnostic bundle',
}
);
export const SOURCE_NAME_LABEL = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.sourceName.label',
{

View file

@ -40,6 +40,7 @@ describe('SourceLogic', () => {
dataLoading: true,
sectionLoading: true,
buttonLoading: false,
diagnosticDownloadButtonVisible: false,
contentMeta: DEFAULT_META,
contentFilterValue: '',
isConfigurationUpdateButtonLoading: false,
@ -125,6 +126,12 @@ describe('SourceLogic', () => {
expect(SourceLogic.values.buttonLoading).toEqual(false);
});
it('showDiagnosticDownloadButton', () => {
SourceLogic.actions.showDiagnosticDownloadButton();
expect(SourceLogic.values.diagnosticDownloadButtonVisible).toEqual(true);
});
});
describe('listeners', () => {
@ -183,6 +190,27 @@ describe('SourceLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
it('handles error message with diagnostic bundle error message', async () => {
const showDiagnosticDownloadButtonSpy = jest.spyOn(
SourceLogic.actions,
'showDiagnosticDownloadButton'
);
// For contenst source errors, the API returns the source errors in an error property in the success
// response. We don't reject here because we still render the content source with the error.
const promise = Promise.resolve({
...contentSource,
errors: [
'The database is on fire. [Check diagnostic bundle for details - Message id: 123]',
],
});
http.get.mockReturnValue(promise);
SourceLogic.actions.initializeSource(contentSource.id);
await promise;
expect(showDiagnosticDownloadButtonSpy).toHaveBeenCalled();
});
describe('404s', () => {
const mock404 = Promise.reject({ response: { status: 404 } });

View file

@ -52,6 +52,7 @@ export interface SourceActions {
setButtonNotLoading(): void;
setStagedPrivateKey(stagedPrivateKey: string | null): string | null;
setConfigurationUpdateButtonNotLoading(): void;
showDiagnosticDownloadButton(): void;
}
interface SourceValues {
@ -59,6 +60,7 @@ interface SourceValues {
dataLoading: boolean;
sectionLoading: boolean;
buttonLoading: boolean;
diagnosticDownloadButtonVisible: boolean;
contentItems: SourceContentItem[];
contentMeta: Meta;
contentFilterValue: string;
@ -108,6 +110,7 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
setButtonNotLoading: () => false,
setStagedPrivateKey: (stagedPrivateKey: string) => stagedPrivateKey,
setConfigurationUpdateButtonNotLoading: () => false,
showDiagnosticDownloadButton: true,
},
reducers: {
contentSource: [
@ -147,6 +150,13 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
setSearchResults: () => false,
},
],
diagnosticDownloadButtonVisible: [
false,
{
showDiagnosticDownloadButton: () => true,
initializeSource: () => false,
},
],
contentItems: [
[],
{
@ -200,6 +210,9 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
}
if (response.errors) {
setErrorMessage(response.errors);
if (errorsHaveDiagnosticBundleString(response.errors as unknown as string[])) {
actions.showDiagnosticDownloadButton();
}
} else {
clearFlashMessages();
}
@ -343,3 +356,8 @@ const setPage = (state: Meta, page: number) => ({
current: page,
},
});
const errorsHaveDiagnosticBundleString = (errors: string[]) => {
const ERROR_SUBSTRING = 'Check diagnostic bundle for details';
return errors.find((e) => e.includes(ERROR_SUBSTRING));
};