[Workplace Search] Refactor Add Source Views to support base service types for external connectors (#131802)

This commit is contained in:
Byron Hulcher 2022-05-10 10:45:57 -04:00 committed by GitHub
parent 4ef0f1e0b3
commit 626b4ae4ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1423 additions and 1455 deletions

View file

@ -21,6 +21,8 @@ import {
SOURCES_PATH,
PRIVATE_SOURCES_PATH,
SOURCE_DETAILS_PATH,
getAddPath,
getEditPath,
} from './routes';
const TestComponent = ({ id, isOrg }: { id: string; isOrg?: boolean }) => {
@ -86,3 +88,32 @@ describe('getReindexJobRoute', () => {
);
});
});
describe('getAddPath', () => {
it('should handle a service type', () => {
expect(getAddPath('share_point')).toEqual('/sources/add/share_point');
});
it('should should handle an external service type with no base service type', () => {
expect(getAddPath('external')).toEqual('/sources/add/external');
});
it('should should handle an external service type with a base service type', () => {
expect(getAddPath('external', 'share_point')).toEqual('/sources/add/share_point/external');
});
it('should should handle a custom service type with no base service type', () => {
expect(getAddPath('external')).toEqual('/sources/add/external');
});
it('should should handle a custom service type with a base service type', () => {
expect(getAddPath('custom', 'share_point_server')).toEqual(
'/sources/add/share_point_server/custom'
);
});
});
describe('getEditPath', () => {
it('should handle a service type', () => {
expect(getEditPath('share_point')).toEqual('/settings/connectors/share_point/edit');
});
});

View file

@ -77,6 +77,14 @@ export const getReindexJobRoute = (
isOrganization: boolean
) =>
getSourcesPath(generatePath(REINDEX_JOB_PATH, { sourceId, activeReindexJobId }), isOrganization);
export const getAddPath = (serviceType: string): string => `${SOURCES_PATH}/add/${serviceType}`;
export const getAddPath = (serviceType: string, baseServiceType?: string): string => {
const baseServiceTypePath = baseServiceType
? `${baseServiceType}/${serviceType}`
: `${serviceType}`;
return `${SOURCES_PATH}/add/${baseServiceTypePath}`;
};
// TODO this should handle base service type once we are getting it back from registered external connectors
export const getEditPath = (serviceType: string): string =>
`${ORG_SETTINGS_CONNECTORS_PATH}/${serviceType}/edit`;

View file

@ -72,18 +72,14 @@ export interface Configuration {
export interface SourceDataItem {
name: string;
iconName: string;
categories?: string[];
serviceType: string;
baseServiceType?: string;
configuration: Configuration;
configured?: boolean;
connected?: boolean;
features?: Features;
objTypes?: string[];
accountContextOnly: boolean;
internalConnectorAvailable?: boolean;
externalConnectorAvailable?: boolean;
customConnectorAvailable?: boolean;
isBeta?: boolean;
}

View file

@ -1,17 +0,0 @@
/*
* 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 { SourceDataItem } from '../types';
export const hasMultipleConnectorOptions = ({
internalConnectorAvailable,
externalConnectorAvailable,
customConnectorAvailable,
}: SourceDataItem) =>
[externalConnectorAvailable, internalConnectorAvailable, customConnectorAvailable].filter(
(available) => !!available
).length > 1;

View file

@ -11,6 +11,5 @@ export { mimeType } from './mime_types';
export { readUploadedFileAsBase64 } from './read_uploaded_file_as_base64';
export { readUploadedFileAsText } from './read_uploaded_file_as_text';
export { handlePrivateKeyUpload } from './handle_private_key_upload';
export { hasMultipleConnectorOptions } from './has_multiple_connector_options';
export { isNotNullish } from './is_not_nullish';
export { sortByName } from './sort_by_name';

View file

@ -7,6 +7,7 @@
import '../../../../../../__mocks__/shallow_useeffect.mock';
import { setMockValues } from '../../../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../../../__mocks__/react_router';
import { sourceConfigData } from '../../../../../__mocks__/content_sources.mock';
import React from 'react';
@ -17,7 +18,6 @@ import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../../components/layout';
import { staticSourceData } from '../../../source_data';
import { AddCustomSource } from './add_custom_source';
import { AddCustomSourceSteps } from './add_custom_source_logic';
@ -25,11 +25,6 @@ import { ConfigureCustom } from './configure_custom';
import { SaveCustom } from './save_custom';
describe('AddCustomSource', () => {
const props = {
sourceData: staticSourceData[0],
initialValues: undefined,
};
const values = {
sourceConfigData,
isOrganization: true,
@ -37,17 +32,26 @@ describe('AddCustomSource', () => {
beforeEach(() => {
setMockValues({ ...values });
mockUseParams.mockReturnValue({ baseServiceType: 'share_point_server' });
});
it('renders', () => {
const wrapper = shallow(<AddCustomSource {...props} />);
const wrapper = shallow(<AddCustomSource />);
expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(1);
});
it('returns null if there is no matching source data for the service type', () => {
mockUseParams.mockReturnValue({ baseServiceType: 'doesnt_exist' });
const wrapper = shallow(<AddCustomSource />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('should show correct layout for personal dashboard', () => {
setMockValues({ isOrganization: false });
const wrapper = shallow(<AddCustomSource {...props} />);
const wrapper = shallow(<AddCustomSource />);
expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(0);
expect(wrapper.find(PersonalDashboardLayout)).toHaveLength(1);
@ -55,14 +59,14 @@ describe('AddCustomSource', () => {
it('should show Configure Custom for custom configuration step', () => {
setMockValues({ currentStep: AddCustomSourceSteps.ConfigureCustomStep });
const wrapper = shallow(<AddCustomSource {...props} />);
const wrapper = shallow(<AddCustomSource />);
expect(wrapper.find(ConfigureCustom)).toHaveLength(1);
});
it('should show Save Custom for save custom step', () => {
setMockValues({ currentStep: AddCustomSourceSteps.SaveCustomStep });
const wrapper = shallow(<AddCustomSource {...props} />);
const wrapper = shallow(<AddCustomSource />);
expect(wrapper.find(SaveCustom)).toHaveLength(1);
});

View file

@ -7,6 +7,8 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { useValues } from 'kea';
import { AppLogic } from '../../../../../app_logic';
@ -16,27 +18,38 @@ import {
} from '../../../../../components/layout';
import { NAV } from '../../../../../constants';
import { SourceDataItem } from '../../../../../types';
import { getSourceData } from '../../../source_data';
import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic';
import { ConfigureCustom } from './configure_custom';
import { SaveCustom } from './save_custom';
interface Props {
sourceData: SourceDataItem;
initialValue?: string;
}
export const AddCustomSource: React.FC<Props> = ({ sourceData, initialValue = '' }) => {
const addCustomSourceLogic = AddCustomSourceLogic({ sourceData, initialValue });
export const AddCustomSource: React.FC = () => {
const { baseServiceType } = useParams<{ baseServiceType?: string }>();
const sourceData = getSourceData('custom', baseServiceType);
const addCustomSourceLogic = AddCustomSourceLogic({
baseServiceType,
initialValue: sourceData?.name,
});
const { currentStep } = useValues(addCustomSourceLogic);
const { isOrganization } = useValues(AppLogic);
if (!sourceData) {
return null;
}
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
return (
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, sourceData.name || '...']}>
{currentStep === AddCustomSourceSteps.ConfigureCustomStep && <ConfigureCustom />}
{currentStep === AddCustomSourceSteps.SaveCustomStep && <SaveCustom />}
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, sourceData.name]}>
{currentStep === AddCustomSourceSteps.ConfigureCustomStep && (
<ConfigureCustom sourceData={sourceData} />
)}
{currentStep === AddCustomSourceSteps.SaveCustomStep && (
<SaveCustom sourceData={sourceData} />
)}
</Layout>
);
};

View file

@ -14,7 +14,6 @@ import { sourceConfigData } from '../../../../../__mocks__/content_sources.mock'
import { nextTick } from '@kbn/test-jest-helpers';
import { docLinks } from '../../../../../../shared/doc_links';
import { itShowsServerErrorAsFlashMessage } from '../../../../../../test_helpers';
jest.mock('../../../../../app_logic', () => ({
@ -22,35 +21,17 @@ jest.mock('../../../../../app_logic', () => ({
}));
import { AppLogic } from '../../../../../app_logic';
import { SOURCE_NAMES } from '../../../../../constants';
import { CustomSource, SourceDataItem } from '../../../../../types';
import { CustomSource } from '../../../../../types';
import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic';
const CUSTOM_SOURCE_DATA_ITEM: SourceDataItem = {
name: SOURCE_NAMES.CUSTOM,
iconName: SOURCE_NAMES.CUSTOM,
serviceType: 'custom',
configuration: {
isPublicKey: false,
hasOauthRedirect: false,
needsBaseUrl: false,
documentationUrl: docLinks.workplaceSearchCustomSources,
applicationPortalUrl: '',
},
accountContextOnly: false,
};
const DEFAULT_VALUES = {
currentStep: AddCustomSourceSteps.ConfigureCustomStep,
buttonLoading: false,
customSourceNameValue: '',
newCustomSource: {} as CustomSource,
sourceData: CUSTOM_SOURCE_DATA_ITEM,
};
const MOCK_PROPS = { initialValue: '', sourceData: CUSTOM_SOURCE_DATA_ITEM };
const MOCK_NAME = 'name';
describe('AddCustomSourceLogic', () => {
@ -60,7 +41,7 @@ describe('AddCustomSourceLogic', () => {
beforeEach(() => {
jest.clearAllMocks();
mount({}, MOCK_PROPS);
mount({});
});
it('has expected default values', () => {
@ -112,12 +93,9 @@ describe('AddCustomSourceLogic', () => {
describe('listeners', () => {
beforeEach(() => {
mount(
{
customSourceNameValue: MOCK_NAME,
},
MOCK_PROPS
);
mount({
customSourceNameValue: MOCK_NAME,
});
});
describe('organization context', () => {
@ -151,11 +129,7 @@ describe('AddCustomSourceLogic', () => {
customSourceNameValue: MOCK_NAME,
},
{
...MOCK_PROPS,
sourceData: {
...CUSTOM_SOURCE_DATA_ITEM,
serviceType: 'sharepoint-server',
},
baseServiceType: 'share_point_server',
}
);
@ -165,7 +139,7 @@ describe('AddCustomSourceLogic', () => {
body: JSON.stringify({
service_type: 'custom',
name: MOCK_NAME,
base_service_type: 'sharepoint-server',
base_service_type: 'share_point_server',
}),
});
});
@ -199,11 +173,7 @@ describe('AddCustomSourceLogic', () => {
customSourceNameValue: MOCK_NAME,
},
{
...MOCK_PROPS,
sourceData: {
...CUSTOM_SOURCE_DATA_ITEM,
serviceType: 'sharepoint-server',
},
baseServiceType: 'share_point_server',
}
);
@ -215,7 +185,7 @@ describe('AddCustomSourceLogic', () => {
body: JSON.stringify({
service_type: 'custom',
name: MOCK_NAME,
base_service_type: 'sharepoint-server',
base_service_type: 'share_point_server',
}),
}
);

View file

@ -10,11 +10,11 @@ import { kea, MakeLogicType } from 'kea';
import { flashAPIErrors, clearFlashMessages } from '../../../../../../shared/flash_messages';
import { HttpLogic } from '../../../../../../shared/http';
import { AppLogic } from '../../../../../app_logic';
import { CustomSource, SourceDataItem } from '../../../../../types';
import { CustomSource } from '../../../../../types';
export interface AddCustomSourceProps {
sourceData: SourceDataItem;
initialValue: string;
baseServiceType?: string;
initialValue?: string;
}
export enum AddCustomSourceSteps {
@ -34,7 +34,6 @@ interface AddCustomSourceValues {
currentStep: AddCustomSourceSteps;
customSourceNameValue: string;
newCustomSource: CustomSource;
sourceData: SourceDataItem;
}
/**
@ -67,7 +66,7 @@ export const AddCustomSourceLogic = kea<
},
],
customSourceNameValue: [
props.initialValue,
props.initialValue || '',
{
setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue,
},
@ -78,7 +77,6 @@ export const AddCustomSourceLogic = kea<
setNewCustomSource: (_, newCustomSource) => newCustomSource,
},
],
sourceData: [props.sourceData],
}),
listeners: ({ actions, values, props }) => ({
createContentSource: async () => {
@ -90,21 +88,12 @@ export const AddCustomSourceLogic = kea<
const { customSourceNameValue } = values;
const baseParams = {
const params = {
service_type: 'custom',
name: customSourceNameValue,
base_service_type: props.baseServiceType,
};
// pre-configured custom sources have a serviceType reflecting their target service
// we submit this as `base_service_type` to keep track of
const params =
props.sourceData.serviceType === 'custom'
? baseParams
: {
...baseParams,
base_service_type: props.sourceData.serviceType,
};
try {
const response = await HttpLogic.values.http.post<CustomSource>(route, {
body: JSON.stringify(params),

View file

@ -21,24 +21,24 @@ import { ConfigureCustom } from './configure_custom';
describe('ConfigureCustom', () => {
const setCustomSourceNameValue = jest.fn();
const createContentSource = jest.fn();
const sourceData = staticSourceData[1];
beforeEach(() => {
setMockActions({ setCustomSourceNameValue, createContentSource });
setMockValues({
customSourceNameValue: 'name',
buttonLoading: false,
sourceData: staticSourceData[1],
});
});
it('renders', () => {
const wrapper = shallow(<ConfigureCustom />);
const wrapper = shallow(<ConfigureCustom sourceData={sourceData} />);
expect(wrapper.find(EuiForm)).toHaveLength(1);
});
it('handles input change', () => {
const wrapper = shallow(<ConfigureCustom />);
const wrapper = shallow(<ConfigureCustom sourceData={sourceData} />);
const text = 'changed for the better';
const input = wrapper.find(EuiFieldText);
input.simulate('change', { target: { value: text } });
@ -47,7 +47,7 @@ describe('ConfigureCustom', () => {
});
it('handles form submission', () => {
const wrapper = shallow(<ConfigureCustom />);
const wrapper = shallow(<ConfigureCustom sourceData={sourceData} />);
const preventDefault = jest.fn();
wrapper.find('EuiForm').simulate('submit', { preventDefault });

View file

@ -21,11 +21,13 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { docLinks } from '../../../../../../shared/doc_links';
import connectionIllustration from '../../../../../assets/connection_illustration.svg';
import { SourceDataItem } from '../../../../../types';
import { SOURCE_NAME_LABEL } from '../../../constants';
import { AddSourceHeader } from '../add_source_header';
@ -33,9 +35,13 @@ import { CONFIG_CUSTOM_BUTTON, CONFIG_CUSTOM_LINK_TEXT, CONFIG_INTRO_ALT_TEXT }
import { AddCustomSourceLogic } from './add_custom_source_logic';
export const ConfigureCustom: React.FC = () => {
interface ConfigureCustomProps {
sourceData: SourceDataItem;
}
export const ConfigureCustom: React.FC<ConfigureCustomProps> = ({ sourceData }) => {
const { setCustomSourceNameValue, createContentSource } = useActions(AddCustomSourceLogic);
const { customSourceNameValue, buttonLoading, sourceData } = useValues(AddCustomSourceLogic);
const { customSourceNameValue, buttonLoading } = useValues(AddCustomSourceLogic);
const handleFormSubmit = (e: FormEvent) => {
e.preventDefault();

View file

@ -25,18 +25,21 @@ const mockValues = {
accessToken: 'token',
name: 'name',
},
sourceData: staticCustomSourceData,
};
const sourceData = staticCustomSourceData;
describe('SaveCustom', () => {
beforeAll(() => {
jest.clearAllMocks();
setMockValues(mockValues);
});
describe('default behavior', () => {
let wrapper: ShallowWrapper;
beforeAll(() => {
jest.clearAllMocks();
setMockValues(mockValues);
wrapper = shallow(<SaveCustom />);
wrapper = shallow(<SaveCustom sourceData={sourceData} />);
});
it('contains a button back to the sources list', () => {
@ -52,20 +55,14 @@ describe('SaveCustom', () => {
let wrapper: ShallowWrapper;
beforeAll(() => {
jest.clearAllMocks();
setMockValues({
...mockValues,
sourceData: {
...staticCustomSourceData,
serviceType: 'sharepoint-server',
configuration: {
...staticCustomSourceData.configuration,
githubRepository: 'elastic/sharepoint-server-connector',
},
},
});
wrapper = shallow(<SaveCustom />);
wrapper = shallow(
<SaveCustom
sourceData={{
...sourceData,
baseServiceType: 'share_point_server',
}}
/>
);
});
it('includes a link to provide feedback', () => {

View file

@ -21,12 +21,14 @@ import {
EuiCallOut,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButtonTo } from '../../../../../../shared/react_router_helpers';
import { AppLogic } from '../../../../../app_logic';
import { SOURCES_PATH, getSourcesPath } from '../../../../../routes';
import { SourceDataItem } from '../../../../../types';
import { CustomSourceDeployment } from '../../custom_source_deployment';
@ -35,10 +37,14 @@ import { SAVE_CUSTOM_BODY1 as READY_TO_ACCEPT_REQUESTS_LABEL } from '../constant
import { AddCustomSourceLogic } from './add_custom_source_logic';
export const SaveCustom: React.FC = () => {
const { newCustomSource, sourceData } = useValues(AddCustomSourceLogic);
interface SaveCustomProps {
sourceData: SourceDataItem;
}
export const SaveCustom: React.FC<SaveCustomProps> = ({ sourceData }) => {
const { newCustomSource } = useValues(AddCustomSourceLogic);
const { isOrganization } = useValues(AppLogic);
const { serviceType, name, categories = [] } = sourceData;
const { serviceType, baseServiceType, name, categories = [] } = sourceData;
return (
<>
@ -92,10 +98,10 @@ export const SaveCustom: React.FC = () => {
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<CustomSourceDeployment source={newCustomSource} sourceData={sourceData} />
<CustomSourceDeployment source={newCustomSource} baseServiceType={baseServiceType} />
</EuiFlexItem>
</EuiFlexGroup>
{serviceType !== 'custom' && (
{baseServiceType && (
<>
<EuiSpacer />
<EuiFlexGroup justifyContent="center">

View file

@ -7,6 +7,7 @@
import '../../../../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../../../__mocks__/react_router';
import { sourceConfigData } from '../../../../../__mocks__/content_sources.mock';
import React from 'react';
@ -19,24 +20,15 @@ import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../../components/layout';
import { staticSourceData } from '../../../source_data';
import { ExternalConnectorConfig } from './external_connector_config';
import { ExternalConnectorFormFields } from './external_connector_form_fields';
describe('ExternalConnectorConfig', () => {
const goBack = jest.fn();
const onDeleteConfig = jest.fn();
const setExternalConnectorApiKey = jest.fn();
const setExternalConnectorUrl = jest.fn();
const saveExternalConnectorConfig = jest.fn();
const props = {
sourceData: staticSourceData[0],
goBack,
onDeleteConfig,
};
const values = {
sourceConfigData,
buttonLoading: false,
@ -48,37 +40,47 @@ describe('ExternalConnectorConfig', () => {
};
beforeEach(() => {
jest.clearAllMocks();
setMockActions({
setExternalConnectorApiKey,
setExternalConnectorUrl,
saveExternalConnectorConfig,
});
setMockValues({ ...values });
mockUseParams.mockReturnValue({});
});
it('returns null if there is no matching source data for the service type', () => {
mockUseParams.mockReturnValue({ baseServiceType: 'doesnt_exist' });
const wrapper = shallow(<ExternalConnectorConfig />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('renders', () => {
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
const wrapper = shallow(<ExternalConnectorConfig />);
expect(wrapper.find(EuiSteps)).toHaveLength(1);
expect(wrapper.find(EuiSteps).dive().find(ExternalConnectorFormFields)).toHaveLength(1);
});
it('renders organizstion layout', () => {
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
const wrapper = shallow(<ExternalConnectorConfig />);
expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(1);
});
it('should show correct layout for personal dashboard', () => {
setMockValues({ ...values, isOrganization: false });
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
const wrapper = shallow(<ExternalConnectorConfig />);
expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(0);
expect(wrapper.find(PersonalDashboardLayout)).toHaveLength(1);
});
it('handles form submission', () => {
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
const wrapper = shallow(<ExternalConnectorConfig />);
const preventDefault = jest.fn();
wrapper.find('form').simulate('submit', { preventDefault });

View file

@ -7,11 +7,12 @@
import React, { FormEvent } from 'react';
import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
@ -26,56 +27,41 @@ import {
PersonalDashboardLayout,
WorkplaceSearchPageTemplate,
} from '../../../../../components/layout';
import { NAV, REMOVE_BUTTON } from '../../../../../constants';
import { SourceDataItem } from '../../../../../types';
import { staticExternalSourceData } from '../../../source_data';
import { NAV } from '../../../../../constants';
import { getSourceData } from '../../../source_data';
import { AddSourceHeader } from '../add_source_header';
import { ConfigDocsLinks } from '../config_docs_links';
import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON } from '../constants';
import { OAUTH_SAVE_CONFIG_BUTTON } from '../constants';
import { ExternalConnectorDocumentation } from './external_connector_documentation';
import { ExternalConnectorFormFields } from './external_connector_form_fields';
import { ExternalConnectorLogic } from './external_connector_logic';
interface SaveConfigProps {
sourceData: SourceDataItem;
goBack?: () => void;
onDeleteConfig?: () => void;
}
export const ExternalConnectorConfig: React.FC<SaveConfigProps> = ({
sourceData,
goBack,
onDeleteConfig,
}) => {
const serviceType = 'external';
export const ExternalConnectorConfig: React.FC = () => {
const { baseServiceType } = useParams<{ baseServiceType?: string }>();
const sourceData = getSourceData('external', baseServiceType);
const { saveExternalConnectorConfig } = useActions(ExternalConnectorLogic);
const {
formDisabled,
buttonLoading,
externalConnectorUrl,
externalConnectorApiKey,
sourceConfigData,
urlValid,
} = useValues(ExternalConnectorLogic);
const { formDisabled, buttonLoading, externalConnectorUrl, externalConnectorApiKey, urlValid } =
useValues(ExternalConnectorLogic);
const handleFormSubmission = (e: FormEvent) => {
e.preventDefault();
saveExternalConnectorConfig({ url: externalConnectorUrl, apiKey: externalConnectorApiKey });
};
const { name, categories } = sourceConfigData;
const {
configuration: { applicationLinkTitle, applicationPortalUrl },
} = sourceData;
const { isOrganization } = useValues(AppLogic);
if (!sourceData) {
return null;
}
const {
configuration: { documentationUrl },
} = staticExternalSourceData;
name,
categories = [],
configuration: { applicationLinkTitle, applicationPortalUrl, documentationUrl },
} = sourceData;
const saveButton = (
<EuiButton color="primary" fill isLoading={buttonLoading} disabled={formDisabled} type="submit">
@ -83,22 +69,10 @@ export const ExternalConnectorConfig: React.FC<SaveConfigProps> = ({
</EuiButton>
);
const deleteButton = (
<EuiButton color="danger" fill disabled={buttonLoading} onClick={onDeleteConfig}>
{REMOVE_BUTTON}
</EuiButton>
);
const backButton = <EuiButtonEmpty onClick={goBack}>{OAUTH_BACK_BUTTON}</EuiButtonEmpty>;
const formActions = (
<EuiFormRow>
<EuiFlexGroup justifyContent="flexStart" gutterSize="m" responsive={false}>
<EuiFlexItem grow={false}>{saveButton}</EuiFlexItem>
<EuiFlexItem grow={false}>
{goBack && backButton}
{onDeleteConfig && deleteButton}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
@ -132,11 +106,17 @@ export const ExternalConnectorConfig: React.FC<SaveConfigProps> = ({
},
];
const header = <AddSourceHeader name={name} serviceType={serviceType} categories={categories} />;
const header = (
<AddSourceHeader
name={name}
serviceType={baseServiceType || 'external'}
categories={categories}
/>
);
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
return (
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name || '...']} isLoading={false}>
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name]} isLoading={false}>
{header}
<EuiSpacer size="l" />
<ExternalConnectorDocumentation name={name} documentationUrl={documentationUrl} />

View file

@ -36,10 +36,6 @@ describe('ExternalConnectorLogic', () => {
formDisabled: true,
externalConnectorUrl: '',
externalConnectorApiKey: '',
sourceConfigData: {
name: '',
categories: [],
},
urlValid: true,
showInsecureUrlCallout: false,
insecureUrl: true,
@ -52,7 +48,6 @@ describe('ExternalConnectorLogic', () => {
formDisabled: false,
insecureUrl: false,
dataLoading: false,
sourceConfigData,
};
beforeEach(() => {
@ -87,7 +82,6 @@ describe('ExternalConnectorLogic', () => {
it('saves the source config', () => {
expect(ExternalConnectorLogic.values).toEqual({
...DEFAULT_VALUES_SUCCESS,
sourceConfigData,
});
});
@ -104,7 +98,6 @@ describe('ExternalConnectorLogic', () => {
...DEFAULT_VALUES_SUCCESS,
externalConnectorUrl: '',
insecureUrl: true,
sourceConfigData: newSourceConfigData,
});
});
it('sets undefined api key to empty string', () => {
@ -119,7 +112,6 @@ describe('ExternalConnectorLogic', () => {
expect(ExternalConnectorLogic.values).toEqual({
...DEFAULT_VALUES_SUCCESS,
externalConnectorApiKey: '',
sourceConfigData: newSourceConfigData,
});
});
});

View file

@ -48,7 +48,6 @@ export interface ExternalConnectorValues {
externalConnectorApiKey: string;
externalConnectorUrl: string;
urlValid: boolean;
sourceConfigData: SourceConfigData | Pick<SourceConfigData, 'name' | 'categories'>;
insecureUrl: boolean;
showInsecureUrlCallout: boolean;
}
@ -107,12 +106,6 @@ export const ExternalConnectorLogic = kea<
setShowInsecureUrlCallout: (_, showCallout) => showCallout,
},
],
sourceConfigData: [
{ name: '', categories: [] },
{
fetchExternalSourceSuccess: (_, sourceConfigData) => sourceConfigData,
},
],
urlValid: [
true,
{

View file

@ -11,6 +11,7 @@ import {
setMockActions,
setMockValues,
} from '../../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../../__mocks__/react_router';
import { sourceConfigData } from '../../../../__mocks__/content_sources.mock';
import React from 'react';
@ -22,13 +23,9 @@ import {
PersonalDashboardLayout,
} from '../../../../components/layout';
import { staticSourceData } from '../../source_data';
import { AddSource } from './add_source';
import { AddSourceSteps } from './add_source_logic';
import { ConfigCompleted } from './config_completed';
import { ConfigurationChoice } from './configuration_choice';
import { ConfigurationIntro } from './configuration_intro';
import { ConfigureOauth } from './configure_oauth';
import { ConnectInstance } from './connect_instance';
import { Reauthenticate } from './reauthenticate';
@ -36,7 +33,7 @@ import { SaveConfig } from './save_config';
describe('AddSourceList', () => {
const { navigateToUrl } = mockKibanaValues;
const initializeAddSource = jest.fn();
const getSourceConfigData = jest.fn();
const setAddSourceStep = jest.fn();
const saveSourceConfig = jest.fn((_, setConfigCompletedStep) => {
setConfigCompletedStep();
@ -47,7 +44,7 @@ describe('AddSourceList', () => {
const resetSourcesState = jest.fn();
const mockValues = {
addSourceCurrentStep: AddSourceSteps.ConfigIntroStep,
addSourceCurrentStep: null,
sourceConfigData,
dataLoading: false,
newCustomSource: {},
@ -56,68 +53,29 @@ describe('AddSourceList', () => {
};
beforeEach(() => {
jest.clearAllMocks();
setMockActions({
initializeAddSource,
getSourceConfigData,
setAddSourceStep,
saveSourceConfig,
createContentSource,
resetSourcesState,
});
setMockValues(mockValues);
});
it('renders default state', () => {
const wrapper = shallow(<AddSource sourceData={staticSourceData[0]} />);
wrapper.find(ConfigurationIntro).prop('advanceStep')();
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
expect(initializeAddSource).toHaveBeenCalled();
});
it('renders default state correctly when there are multiple connector options', () => {
const wrapper = shallow(
<AddSource
sourceData={{
...staticSourceData[0],
externalConnectorAvailable: true,
customConnectorAvailable: true,
internalConnectorAvailable: true,
}}
/>
);
wrapper.find(ConfigurationIntro).prop('advanceStep')();
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ChoiceStep);
});
it('renders default state correctly when there are multiple connector options but external connector is configured', () => {
setMockValues({ ...mockValues, externalConfigured: true });
const wrapper = shallow(
<AddSource
sourceData={{
...staticSourceData[0],
externalConnectorAvailable: true,
customConnectorAvailable: true,
internalConnectorAvailable: true,
}}
/>
);
wrapper.find(ConfigurationIntro).prop('advanceStep')();
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
mockUseParams.mockReturnValue({ serviceType: 'confluence_cloud' });
});
describe('layout', () => {
it('renders the default workplace search layout when on an organization view', () => {
setMockValues({ ...mockValues, isOrganization: true });
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
});
it('renders the personal dashboard layout when not in an organization', () => {
setMockValues({ ...mockValues, isOrganization: false });
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
expect(wrapper.type()).toEqual(PersonalDashboardLayout);
});
@ -125,7 +83,7 @@ describe('AddSourceList', () => {
it('renders a breadcrumb fallback while data is loading', () => {
setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} });
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']);
});
@ -135,26 +93,24 @@ describe('AddSourceList', () => {
...mockValues,
addSourceCurrentStep: AddSourceSteps.ConfigCompletedStep,
});
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
expect(wrapper.find(ConfigCompleted).prop('showFeedbackLink')).toEqual(false);
wrapper.find(ConfigCompleted).prop('advanceStep')();
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep);
});
it('renders Config Completed step with feedback for external connectors', () => {
mockUseParams.mockReturnValue({ serviceType: 'external' });
setMockValues({
...mockValues,
sourceConfigData: { ...sourceConfigData, serviceType: 'external' },
addSourceCurrentStep: AddSourceSteps.ConfigCompletedStep,
});
const wrapper = shallow(
<AddSource sourceData={{ ...staticSourceData[1], serviceType: 'external' }} />
);
const wrapper = shallow(<AddSource />);
expect(wrapper.find(ConfigCompleted).prop('showFeedbackLink')).toEqual(true);
wrapper.find(ConfigCompleted).prop('advanceStep')();
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep);
});
@ -163,13 +119,13 @@ describe('AddSourceList', () => {
...mockValues,
addSourceCurrentStep: AddSourceSteps.SaveConfigStep,
});
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
const saveConfig = wrapper.find(SaveConfig);
saveConfig.prop('advanceStep')();
saveConfig.prop('goBackStep')!();
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ConfigIntroStep);
expect(saveSourceConfig).toHaveBeenCalled();
saveConfig.prop('goBackStep')!();
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/intro');
});
it('renders Connect Instance step', () => {
@ -178,10 +134,11 @@ describe('AddSourceList', () => {
sourceConfigData,
addSourceCurrentStep: AddSourceSteps.ConnectInstanceStep,
});
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} connect />);
const wrapper = shallow(<AddSource />);
wrapper.find(ConnectInstance).prop('onFormCreated')('foo');
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
expect(navigateToUrl).toHaveBeenCalledWith('/sources');
});
it('renders Configure Oauth step', () => {
@ -189,11 +146,11 @@ describe('AddSourceList', () => {
...mockValues,
addSourceCurrentStep: AddSourceSteps.ConfigureOauthStep,
});
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
wrapper.find(ConfigureOauth).prop('onFormCreated')('foo');
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
expect(navigateToUrl).toHaveBeenCalledWith('/sources');
});
it('renders Reauthenticate step', () => {
@ -201,23 +158,8 @@ describe('AddSourceList', () => {
...mockValues,
addSourceCurrentStep: AddSourceSteps.ReauthenticateStep,
});
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const wrapper = shallow(<AddSource />);
expect(wrapper.find(Reauthenticate)).toHaveLength(1);
});
it('renders Config Choice step', () => {
setMockValues({
...mockValues,
addSourceCurrentStep: AddSourceSteps.ChoiceStep,
});
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
const advance = wrapper.find(ConfigurationChoice).prop('goToInternalStep');
expect(advance).toBeDefined();
if (advance) {
advance();
}
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
});
});

View file

@ -7,29 +7,28 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
import { i18n } from '@kbn/i18n';
import { flashSuccessToast } from '../../../../../shared/flash_messages';
import { KibanaLogic } from '../../../../../shared/kibana';
import { LicensingLogic } from '../../../../../shared/licensing';
import { AppLogic } from '../../../../app_logic';
import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../components/layout';
import { NAV } from '../../../../constants';
import { SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes';
import { SOURCES_PATH, getSourcesPath, getAddPath, ADD_SOURCE_PATH } from '../../../../routes';
import { hasMultipleConnectorOptions } from '../../../../utils';
import { SourcesLogic } from '../../sources_logic';
import { getSourceData } from '../../source_data';
import { AddSourceHeader } from './add_source_header';
import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic';
import { AddSourceLogic, AddSourceSteps } from './add_source_logic';
import { ConfigCompleted } from './config_completed';
import { ConfigurationChoice } from './configuration_choice';
import { ConfigurationIntro } from './configuration_intro';
import { ConfigureOauth } from './configure_oauth';
import { ConnectInstance } from './connect_instance';
import { Reauthenticate } from './reauthenticate';
@ -37,27 +36,42 @@ import { SaveConfig } from './save_config';
import './add_source.scss';
export const AddSource: React.FC<AddSourceProps> = (props) => {
const { initializeAddSource, setAddSourceStep, saveSourceConfig, resetSourceState } =
useActions(AddSourceLogic);
const { addSourceCurrentStep, sourceConfigData, dataLoading } = useValues(AddSourceLogic);
const { name, categories, needsPermissions, accountContextOnly, privateSourcesEnabled } =
sourceConfigData;
const { serviceType, configuration, features, objTypes } = props.sourceData;
const addPath = getAddPath(serviceType);
export const AddSource: React.FC = () => {
const { serviceType, initialStep } = useParams<{ serviceType: string; initialStep?: string }>();
const addSourceLogic = AddSourceLogic({ serviceType, initialStep });
const { getSourceConfigData, setAddSourceStep, saveSourceConfig, resetSourceState } =
useActions(addSourceLogic);
const { addSourceCurrentStep, sourceConfigData, dataLoading } = useValues(addSourceLogic);
const { isOrganization } = useValues(AppLogic);
const { externalConfigured } = useValues(SourcesLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { navigateToUrl } = useValues(KibanaLogic);
useEffect(() => {
initializeAddSource(props);
getSourceConfigData();
return resetSourceState;
}, []);
}, [serviceType]);
const goToConfigurationIntro = () => setAddSourceStep(AddSourceSteps.ConfigIntroStep);
const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep);
const sourceData = getSourceData(serviceType);
if (!sourceData) {
return null;
}
const { configuration, features, objTypes } = sourceData;
const { name, categories, needsPermissions, accountContextOnly, privateSourcesEnabled } =
sourceConfigData;
if (!hasPlatinumLicense && accountContextOnly) {
navigateToUrl(getSourcesPath(ADD_SOURCE_PATH, isOrganization));
}
const goToConfigurationIntro = () =>
KibanaLogic.values.navigateToUrl(
`${getSourcesPath(getAddPath(serviceType), isOrganization)}/intro`
);
const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep);
const goToConfigCompleted = () => saveSourceConfig(false, setConfigCompletedStep);
const goToChoice = () => setAddSourceStep(AddSourceSteps.ChoiceStep);
const FORM_SOURCE_ADDED_SUCCESS_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.formSourceAddedSuccessMessage',
{
@ -66,11 +80,7 @@ export const AddSource: React.FC<AddSourceProps> = (props) => {
}
);
const goToConnectInstance = () => {
setAddSourceStep(AddSourceSteps.ConnectInstanceStep);
KibanaLogic.values.navigateToUrl(`${getSourcesPath(addPath, isOrganization)}/connect`);
};
const goToConnectInstance = () => setAddSourceStep(AddSourceSteps.ConnectInstanceStep);
const goToFormSourceCreated = () => {
KibanaLogic.values.navigateToUrl(`${getSourcesPath(SOURCES_PATH, isOrganization)}`);
flashSuccessToast(FORM_SOURCE_ADDED_SUCCESS_MESSAGE);
@ -81,18 +91,6 @@ export const AddSource: React.FC<AddSourceProps> = (props) => {
return (
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name || '...']} isLoading={dataLoading}>
{addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && (
<ConfigurationIntro
name={name}
// TODO: Remove this once we can support multiple external connectors
advanceStep={
hasMultipleConnectorOptions(props.sourceData) && !externalConfigured
? goToChoice
: goToSaveConfig
}
header={header}
/>
)}
{addSourceCurrentStep === AddSourceSteps.SaveConfigStep && (
<SaveConfig
name={name}
@ -130,9 +128,6 @@ export const AddSource: React.FC<AddSourceProps> = (props) => {
{addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && (
<Reauthenticate name={name} header={header} />
)}
{addSourceCurrentStep === AddSourceSteps.ChoiceStep && (
<ConfigurationChoice sourceData={props.sourceData} goToInternalStep={goToSaveConfig} />
)}
</Layout>
);
};

View file

@ -0,0 +1,82 @@
/*
* 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 '../../../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, mockKibanaValues } from '../../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../../__mocks__/react_router';
import React from 'react';
import { shallow } from 'enzyme';
import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../components/layout';
import { getSourceData } from '../../source_data';
import { AddSourceChoice } from './add_source_choice';
import { ConfigurationChoice } from './configuration_choice';
describe('AddSourceChoice', () => {
const { navigateToUrl } = mockKibanaValues;
const mockValues = {
isOrganization: true,
hasPlatinumLicense: true,
};
beforeEach(() => {
jest.clearAllMocks();
mockUseParams.mockReturnValue({ serviceType: 'share_point' });
});
it('returns null if there is no matching source data for the service type', () => {
mockUseParams.mockReturnValue({ serviceType: 'doesnt_exist' });
setMockValues(mockValues);
const wrapper = shallow(<AddSourceChoice />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('redirects to root add source path if user does not have a platinum license and the service is account context only', () => {
mockUseParams.mockReturnValue({ serviceType: 'slack' });
setMockValues({ ...mockValues, hasPlatinumLicense: false });
shallow(<AddSourceChoice />);
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add');
});
describe('layout', () => {
it('renders the default workplace search layout when on an organization view', () => {
setMockValues({ ...mockValues, isOrganization: true });
const wrapper = shallow(<AddSourceChoice />);
expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
});
it('renders the personal dashboard layout when not in an organization', () => {
setMockValues({ ...mockValues, isOrganization: false });
const wrapper = shallow(<AddSourceChoice />);
expect(wrapper.type()).toEqual(PersonalDashboardLayout);
});
});
it('renders Config Choice step', () => {
setMockValues(mockValues);
const wrapper = shallow(<AddSourceChoice />);
expect(wrapper.find(ConfigurationChoice).prop('sourceData')).toEqual(
getSourceData('share_point')
);
});
});

View file

@ -0,0 +1,57 @@
/*
* 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 { useParams } from 'react-router-dom';
import { useValues } from 'kea';
import { KibanaLogic } from '../../../../../shared/kibana';
import { LicensingLogic } from '../../../../../shared/licensing';
import { AppLogic } from '../../../../app_logic';
import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../components/layout';
import { NAV } from '../../../../constants';
import { getSourcesPath, ADD_SOURCE_PATH } from '../../../../routes';
import { getSourceData } from '../../source_data';
import { ConfigurationChoice } from './configuration_choice';
import './add_source.scss';
export const AddSourceChoice: React.FC = () => {
const { serviceType } = useParams<{ serviceType: string }>();
const sourceData = getSourceData(serviceType);
const { isOrganization } = useValues(AppLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { navigateToUrl } = useValues(KibanaLogic);
if (!sourceData) {
return null;
}
const { name, accountContextOnly } = sourceData;
if (!hasPlatinumLicense && accountContextOnly) {
navigateToUrl(getSourcesPath(ADD_SOURCE_PATH, isOrganization));
}
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
return (
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name]}>
<ConfigurationChoice sourceData={sourceData} />
</Layout>
);
};

View file

@ -0,0 +1,79 @@
/*
* 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 '../../../../../__mocks__/shallow_useeffect.mock';
import { setMockValues } from '../../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../../__mocks__/react_router';
import React from 'react';
import { shallow } from 'enzyme';
import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../components/layout';
import { AddSourceIntro } from './add_source_intro';
import { ConfigurationIntro } from './configuration_intro';
describe('AddSourceList', () => {
const mockValues = {
isOrganization: true,
};
beforeEach(() => {
jest.clearAllMocks();
mockUseParams.mockReturnValue({ serviceType: 'share_point' });
});
it('returns null if there is no matching source data for the service type', () => {
mockUseParams.mockReturnValue({ serviceType: 'doesnt_exist' });
setMockValues(mockValues);
const wrapper = shallow(<AddSourceIntro />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('sends the user to a choice view when there are multiple connector options', () => {
setMockValues(mockValues);
const wrapper = shallow(<AddSourceIntro />);
expect(wrapper.find(ConfigurationIntro).prop('advanceStepTo')).toEqual(
'/sources/add/share_point/choice'
);
});
it('sends the user to the add source view by default', () => {
mockUseParams.mockReturnValue({ serviceType: 'slack' });
setMockValues(mockValues);
const wrapper = shallow(<AddSourceIntro />);
expect(wrapper.find(ConfigurationIntro).prop('advanceStepTo')).toEqual('/sources/add/slack/');
});
describe('layout', () => {
it('renders the default workplace search layout when on an organization view', () => {
setMockValues({ ...mockValues, isOrganization: true });
const wrapper = shallow(<AddSourceIntro />);
expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
});
it('renders the personal dashboard layout when not in an organization', () => {
setMockValues({ ...mockValues, isOrganization: false });
const wrapper = shallow(<AddSourceIntro />);
expect(wrapper.type()).toEqual(PersonalDashboardLayout);
});
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 { useParams } from 'react-router-dom';
import { useValues } from 'kea';
import { KibanaLogic } from '../../../../../shared/kibana';
import { LicensingLogic } from '../../../../../shared/licensing';
import { AppLogic } from '../../../../app_logic';
import {
WorkplaceSearchPageTemplate,
PersonalDashboardLayout,
} from '../../../../components/layout';
import { NAV } from '../../../../constants';
import { getSourcesPath, ADD_SOURCE_PATH, getAddPath } from '../../../../routes';
import { getSourceData, hasMultipleConnectorOptions } from '../../source_data';
import { AddSourceHeader } from './add_source_header';
import { ConfigurationIntro } from './configuration_intro';
import './add_source.scss';
export const AddSourceIntro: React.FC = () => {
const { serviceType } = useParams<{ serviceType: string }>();
const sourceData = getSourceData(serviceType);
const { isOrganization } = useValues(AppLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { navigateToUrl } = useValues(KibanaLogic);
if (!sourceData) {
return null;
}
const { name, categories = [], accountContextOnly } = sourceData;
if (!hasPlatinumLicense && accountContextOnly) {
navigateToUrl(getSourcesPath(ADD_SOURCE_PATH, isOrganization));
}
const header = <AddSourceHeader name={name} serviceType={serviceType} categories={categories} />;
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
const to =
`${getSourcesPath(getAddPath(serviceType), isOrganization)}/` +
(hasMultipleConnectorOptions(serviceType) ? 'choice' : '');
return (
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name]}>
<ConfigurationIntro name={name} advanceStepTo={to} header={header} />
</Layout>
);
};

View file

@ -15,7 +15,6 @@ import { sourceConfigData } from '../../../../__mocks__/content_sources.mock';
import { nextTick } from '@kbn/test-jest-helpers';
import { docLinks } from '../../../../../shared/doc_links';
import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers';
jest.mock('../../../../app_logic', () => ({
@ -23,10 +22,9 @@ jest.mock('../../../../app_logic', () => ({
}));
import { AppLogic } from '../../../../app_logic';
import { SOURCE_NAMES, SOURCE_OBJ_TYPES } from '../../../../constants';
import { SOURCES_PATH, PRIVATE_SOURCES_PATH, getSourcesPath } from '../../../../routes';
import { FeatureIds } from '../../../../types';
import { PERSONAL_DASHBOARD_SOURCE_ERROR } from '../../constants';
import { staticSourceData } from '../../source_data';
import { SourcesLogic } from '../../sources_logic';
import { ExternalConnectorLogic } from './add_external_connector/external_connector_logic';
@ -37,7 +35,6 @@ import {
SourceConnectData,
OrganizationsMap,
AddSourceValues,
AddSourceProps,
} from './add_source_logic';
describe('AddSourceLogic', () => {
@ -47,8 +44,7 @@ describe('AddSourceLogic', () => {
const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
const DEFAULT_VALUES: AddSourceValues = {
addSourceCurrentStep: AddSourceSteps.ConfigIntroStep,
addSourceProps: {} as AddSourceProps,
addSourceCurrentStep: null,
dataLoading: true,
sectionLoading: true,
buttonLoading: false,
@ -62,11 +58,11 @@ describe('AddSourceLogic', () => {
sourceConfigData: {} as SourceConfigData,
sourceConnectData: {} as SourceConnectData,
oauthConfigCompleted: false,
currentServiceType: '',
githubOrganizations: [],
selectedGithubOrganizationsMap: {} as OrganizationsMap,
selectedGithubOrganizations: [],
preContentSourceId: '',
sourceData: staticSourceData[0],
};
const sourceConnectData = {
@ -79,40 +75,13 @@ describe('AddSourceLogic', () => {
serviceType: 'github',
githubOrganizations: ['foo', 'bar'],
};
const DEFAULT_SERVICE_TYPE = {
name: SOURCE_NAMES.BOX,
iconName: SOURCE_NAMES.BOX,
serviceType: 'box',
configuration: {
isPublicKey: false,
hasOauthRedirect: true,
needsBaseUrl: false,
documentationUrl: docLinks.workplaceSearchBox,
applicationPortalUrl: 'https://app.box.com/developers/console',
},
objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES],
features: {
basicOrgContext: [
FeatureIds.SyncFrequency,
FeatureIds.SyncedItems,
FeatureIds.GlobalAccessPermissions,
],
basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions],
platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems],
platinumPrivateContext: [
FeatureIds.Private,
FeatureIds.SyncFrequency,
FeatureIds.SyncedItems,
],
},
accountContextOnly: false,
};
const DEFAULT_SERVICE_TYPE = 'box';
beforeEach(() => {
jest.clearAllMocks();
ExternalConnectorLogic.mount();
SourcesLogic.mount();
mount();
mount({}, { serviceType: 'box' });
});
it('has expected default values', () => {
@ -215,7 +184,6 @@ describe('AddSourceLogic', () => {
oauthConfigCompleted: true,
dataLoading: false,
sectionLoading: false,
currentServiceType: config.serviceType,
githubOrganizations: config.githubOrganizations,
});
});
@ -286,140 +254,90 @@ describe('AddSourceLogic', () => {
});
describe('listeners', () => {
it('initializeAddSource', () => {
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE };
const getSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'getSourceConfigData');
const setAddSourcePropsSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceProps');
AddSourceLogic.actions.initializeAddSource(addSourceProps);
expect(setAddSourcePropsSpy).toHaveBeenCalledWith({ addSourceProps });
expect(getSourceConfigDataSpy).toHaveBeenCalledWith('box', addSourceProps);
});
describe('setFirstStep', () => {
it('sets intro as first step', () => {
it('sets save config as first step if unconfigured', () => {
mount(
{
sourceConfigData: {
...sourceConfigData,
configured: false,
},
},
{ serviceType: DEFAULT_SERVICE_TYPE }
);
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE };
AddSourceLogic.actions.setFirstStep(addSourceProps);
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigIntroStep);
AddSourceLogic.actions.setFirstStep();
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
});
it('sets connect as first step', () => {
mount({ sourceConfigData }, { serviceType: DEFAULT_SERVICE_TYPE, initialStep: 'connect' });
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, connect: true };
AddSourceLogic.actions.setFirstStep(addSourceProps);
AddSourceLogic.actions.setFirstStep();
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep);
});
it('sets configure as first step', () => {
mount(
{ sourceConfigData },
{ serviceType: DEFAULT_SERVICE_TYPE, initialStep: 'configure' }
);
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, configure: true };
AddSourceLogic.actions.setFirstStep(addSourceProps);
AddSourceLogic.actions.setFirstStep();
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigureOauthStep);
});
it('sets reAuthenticate as first step', () => {
it('sets reauthenticate as first step', () => {
mount(
{ sourceConfigData },
{ serviceType: DEFAULT_SERVICE_TYPE, initialStep: 'reauthenticate' }
);
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, reAuthenticate: true };
AddSourceLogic.actions.setFirstStep(addSourceProps);
AddSourceLogic.actions.setFirstStep();
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReauthenticateStep);
});
it('sets SaveConfig as first step for external connectors', () => {
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = {
sourceData: {
...DEFAULT_SERVICE_TYPE,
serviceType: 'external',
},
};
AddSourceLogic.actions.setFirstStep(addSourceProps);
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
});
it('sets SaveConfigStep for when external connector is available and configured', () => {
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = {
sourceData: {
...DEFAULT_SERVICE_TYPE,
externalConnectorAvailable: true,
},
};
AddSourceLogic.actions.setSourceConfigData({
...sourceConfigData,
serviceType: 'external',
configured: false,
});
SourcesLogic.mount();
SourcesLogic.actions.onInitializeSources({
contentSources: [],
serviceTypes: [
{
serviceType: 'external',
it('sets connect step if configured', () => {
mount(
{
sourceConfigData: {
...sourceConfigData,
configured: true,
},
],
} as any);
AddSourceLogic.actions.setFirstStep(addSourceProps);
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
});
it('sets Connect step when configured and external connector is available and configured', () => {
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = {
sourceData: {
...DEFAULT_SERVICE_TYPE,
externalConnectorAvailable: true,
configured: true,
},
};
AddSourceLogic.actions.setSourceConfigData({
...sourceConfigData,
serviceType: 'external',
configured: true,
});
SourcesLogic.mount();
SourcesLogic.actions.onInitializeSources({
contentSources: [],
serviceTypes: [
{
serviceType: 'external',
configured: true,
},
],
} as any);
AddSourceLogic.actions.setFirstStep(addSourceProps);
{ serviceType: DEFAULT_SERVICE_TYPE }
);
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
AddSourceLogic.actions.setFirstStep();
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep);
});
it('sets Connect step when external and fully configured', () => {
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
const addSourceProps = {
sourceData: {
...DEFAULT_SERVICE_TYPE,
serviceType: 'external',
},
};
AddSourceLogic.actions.setSourceConfigData({
...sourceConfigData,
configured: true,
serviceType: 'external',
configuredFields: { clientId: 'a', clientSecret: 'b' },
});
SourcesLogic.mount();
SourcesLogic.actions.onInitializeSources({
contentSources: [],
serviceTypes: [
{
it('sets connect step if external connector has client id and secret', () => {
mount(
{
sourceConfigData: {
...sourceConfigData,
serviceType: 'external',
configured: true,
configuredFields: {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
},
},
],
} as any);
AddSourceLogic.actions.setFirstStep(addSourceProps);
},
{ serviceType: DEFAULT_SERVICE_TYPE }
);
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
AddSourceLogic.actions.setFirstStep();
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep);
});
@ -541,30 +459,33 @@ describe('AddSourceLogic', () => {
const setSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'setSourceConfigData');
http.get.mockReturnValue(Promise.resolve(sourceConfigData));
AddSourceLogic.actions.getSourceConfigData('github');
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/org/settings/connectors/github'
);
AddSourceLogic.actions.getSourceConfigData();
await nextTick();
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/org/settings/connectors/box'
);
expect(setSourceConfigDataSpy).toHaveBeenCalledWith(sourceConfigData);
});
it('calls API and sets values and calls setFirstStep if AddSourceProps is provided', async () => {
const setSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'setSourceConfigData');
const setFirstStepSpy = jest.spyOn(AddSourceLogic.actions, 'setFirstStep');
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE };
http.get.mockReturnValue(Promise.resolve(sourceConfigData));
AddSourceLogic.actions.getSourceConfigData('github', addSourceProps);
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/org/settings/connectors/github'
);
AddSourceLogic.actions.getSourceConfigData();
await nextTick();
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/org/settings/connectors/box'
);
expect(setSourceConfigDataSpy).toHaveBeenCalledWith(sourceConfigData);
expect(setFirstStepSpy).toHaveBeenCalledWith(addSourceProps);
expect(setFirstStepSpy).toHaveBeenCalled();
});
itShowsServerErrorAsFlashMessage(http.get, () => {
AddSourceLogic.actions.getSourceConfigData('github');
AddSourceLogic.actions.getSourceConfigData();
});
});
@ -579,7 +500,7 @@ describe('AddSourceLogic', () => {
);
http.get.mockReturnValue(Promise.resolve(sourceConnectData));
AddSourceLogic.actions.getSourceConnectData('github', successCallback);
AddSourceLogic.actions.getSourceConnectData(successCallback);
const query = {
index_permissions: false,
@ -588,7 +509,7 @@ describe('AddSourceLogic', () => {
expect(clearFlashMessages).toHaveBeenCalled();
expect(AddSourceLogic.values.buttonLoading).toEqual(true);
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/org/sources/github/prepare',
'/internal/workplace_search/org/sources/box/prepare',
{
query,
}
@ -602,7 +523,7 @@ describe('AddSourceLogic', () => {
it('passes query params', () => {
AddSourceLogic.actions.setSourceSubdomainValue('subdomain');
AddSourceLogic.actions.setSourceIndexPermissionsValue(true);
AddSourceLogic.actions.getSourceConnectData('github', successCallback);
AddSourceLogic.actions.getSourceConnectData(successCallback);
const query = {
index_permissions: true,
@ -610,7 +531,7 @@ describe('AddSourceLogic', () => {
};
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/org/sources/github/prepare',
'/internal/workplace_search/org/sources/box/prepare',
{
query,
}
@ -618,7 +539,7 @@ describe('AddSourceLogic', () => {
});
itShowsServerErrorAsFlashMessage(http.get, () => {
AddSourceLogic.actions.getSourceConnectData('github', successCallback);
AddSourceLogic.actions.getSourceConnectData(successCallback);
});
});
@ -833,7 +754,7 @@ describe('AddSourceLogic', () => {
const successCallback = jest.fn();
const errorCallback = jest.fn();
const serviceType = 'zendesk';
const serviceType = 'box';
const login = 'login';
const password = 'password';
const indexPermissions = false;
@ -859,7 +780,7 @@ describe('AddSourceLogic', () => {
const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading');
http.post.mockReturnValue(Promise.resolve());
AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback);
AddSourceLogic.actions.createContentSource(successCallback, errorCallback);
expect(clearFlashMessages).toHaveBeenCalled();
expect(AddSourceLogic.values.buttonLoading).toEqual(true);
@ -875,7 +796,7 @@ describe('AddSourceLogic', () => {
const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading');
http.post.mockReturnValue(Promise.reject('this is an error'));
AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback);
AddSourceLogic.actions.createContentSource(successCallback, errorCallback);
await nextTick();
expect(setButtonNotLoadingSpy).toHaveBeenCalled();
@ -891,10 +812,10 @@ describe('AddSourceLogic', () => {
});
it('getSourceConnectData', () => {
AddSourceLogic.actions.getSourceConnectData('github', jest.fn());
AddSourceLogic.actions.getSourceConnectData(jest.fn());
expect(http.get).toHaveBeenCalledWith(
'/internal/workplace_search/account/sources/github/prepare',
'/internal/workplace_search/account/sources/box/prepare',
{ query: {} }
);
});
@ -915,10 +836,10 @@ describe('AddSourceLogic', () => {
});
it('createContentSource', () => {
AddSourceLogic.actions.createContentSource('github', jest.fn());
AddSourceLogic.actions.createContentSource(jest.fn());
expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/account/create_source', {
body: JSON.stringify({ service_type: 'github' }),
body: JSON.stringify({ service_type: 'box' }),
});
});
});

View file

@ -23,6 +23,7 @@ import { AppLogic } from '../../../../app_logic';
import { SOURCES_PATH, PRIVATE_SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes';
import { SourceDataItem } from '../../../../types';
import { PERSONAL_DASHBOARD_SOURCE_ERROR } from '../../constants';
import { getSourceData } from '../../source_data';
import { SourcesLogic } from '../../sources_logic';
import {
@ -31,20 +32,16 @@ import {
} from './add_external_connector/external_connector_logic';
export interface AddSourceProps {
sourceData: SourceDataItem;
connect?: boolean;
configure?: boolean;
reAuthenticate?: boolean;
serviceType: string;
initialStep?: string;
}
export enum AddSourceSteps {
ConfigIntroStep = 'Config Intro',
SaveConfigStep = 'Save Config',
ConfigCompletedStep = 'Config Completed',
ConnectInstanceStep = 'Connect Instance',
ConfigureOauthStep = 'Configure Oauth',
ReauthenticateStep = 'Reauthenticate',
ChoiceStep = 'Choice',
}
export interface OauthParams {
@ -57,10 +54,6 @@ export interface OauthParams {
}
export interface AddSourceActions {
initializeAddSource: (addSourceProps: AddSourceProps) => { addSourceProps: AddSourceProps };
setAddSourceProps: ({ addSourceProps }: { addSourceProps: AddSourceProps }) => {
addSourceProps: AddSourceProps;
};
setAddSourceStep(addSourceCurrentStep: AddSourceSteps): AddSourceSteps;
setSourceConfigData(sourceConfigData: SourceConfigData): SourceConfigData;
setSourceConnectData(sourceConnectData: SourceConnectData): SourceConnectData;
@ -76,10 +69,9 @@ export interface AddSourceActions {
setSelectedGithubOrganizations(option: string): string;
resetSourceState(): void;
createContentSource(
serviceType: string,
successCallback: () => void,
errorCallback?: () => void
): { serviceType: string; successCallback(): void; errorCallback?(): void };
): { successCallback(): void; errorCallback?(): void };
saveSourceConfig(
isUpdating: boolean,
successCallback?: () => void
@ -89,24 +81,22 @@ export interface AddSourceActions {
params: OauthParams,
isOrganization: boolean
): { search: Search; params: OauthParams; isOrganization: boolean };
getSourceConfigData(
serviceType: string,
addSourceProps?: AddSourceProps
): { serviceType: string; addSourceProps: AddSourceProps | undefined };
getSourceConnectData(
serviceType: string,
successCallback: (oauthUrl: string) => void
): { serviceType: string; successCallback(oauthUrl: string): void };
getSourceConfigData(): void;
getSourceConnectData(successCallback: (oauthUrl: string) => void): {
successCallback(oauthUrl: string): void;
};
getSourceReConnectData(sourceId: string): { sourceId: string };
getPreContentSourceConfigData(): void;
setButtonNotLoading(): void;
setFirstStep(addSourceProps: AddSourceProps): { addSourceProps: AddSourceProps };
setFirstStep(): void;
}
export interface SourceConfigData {
serviceType: string;
baseServiceType?: string;
name: string;
configured: boolean;
externalConnectorServiceDescribed?: boolean;
categories: string[];
needsPermissions?: boolean;
privateSourcesEnabled: boolean;
@ -133,8 +123,7 @@ export interface OrganizationsMap {
}
export interface AddSourceValues {
addSourceProps: AddSourceProps;
addSourceCurrentStep: AddSourceSteps;
addSourceCurrentStep: AddSourceSteps | null;
dataLoading: boolean;
sectionLoading: boolean;
buttonLoading: boolean;
@ -147,12 +136,12 @@ export interface AddSourceValues {
indexPermissionsValue: boolean;
sourceConfigData: SourceConfigData;
sourceConnectData: SourceConnectData;
currentServiceType: string;
githubOrganizations: string[];
selectedGithubOrganizationsMap: OrganizationsMap;
selectedGithubOrganizations: string[];
preContentSourceId: string;
oauthConfigCompleted: boolean;
sourceData: SourceDataItem | null;
}
interface PreContentSourceResponse {
@ -161,471 +150,436 @@ interface PreContentSourceResponse {
githubOrganizations: string[];
}
export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceActions>>({
path: ['enterprise_search', 'workplace_search', 'add_source_logic'],
actions: {
initializeAddSource: (addSourceProps: AddSourceProps) => ({ addSourceProps }),
setAddSourceProps: ({ addSourceProps }: { addSourceProps: AddSourceProps }) => ({
addSourceProps,
}),
setAddSourceStep: (addSourceCurrentStep: AddSourceSteps) => addSourceCurrentStep,
setSourceConfigData: (sourceConfigData: SourceConfigData) => sourceConfigData,
setSourceConnectData: (sourceConnectData: SourceConnectData) => sourceConnectData,
setClientIdValue: (clientIdValue: string) => clientIdValue,
setClientSecretValue: (clientSecretValue: string) => clientSecretValue,
setBaseUrlValue: (baseUrlValue: string) => baseUrlValue,
setSourceLoginValue: (loginValue: string) => loginValue,
setSourcePasswordValue: (passwordValue: string) => passwordValue,
setSourceSubdomainValue: (subdomainValue: string) => subdomainValue,
setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue,
setPreContentSourceConfigData: (data: PreContentSourceResponse) => data,
setPreContentSourceId: (preContentSourceId: string) => preContentSourceId,
setSelectedGithubOrganizations: (option: string) => option,
getSourceConfigData: (serviceType: string, addSourceProps?: AddSourceProps) => ({
serviceType,
addSourceProps,
}),
getSourceConnectData: (serviceType: string, successCallback: (oauthUrl: string) => string) => ({
serviceType,
successCallback,
}),
getSourceReConnectData: (sourceId: string) => ({ sourceId }),
getPreContentSourceConfigData: () => true,
saveSourceConfig: (isUpdating: boolean, successCallback?: () => void) => ({
isUpdating,
successCallback,
}),
saveSourceParams: (search: Search, params: OauthParams, isOrganization: boolean) => ({
search,
params,
isOrganization,
}),
createContentSource: (
serviceType: string,
successCallback: () => void,
errorCallback?: () => void
) => ({ serviceType, successCallback, errorCallback }),
resetSourceState: () => true,
setButtonNotLoading: () => false,
setFirstStep: (addSourceProps) => ({ addSourceProps }),
},
reducers: {
addSourceProps: [
{} as AddSourceProps,
{
setAddSourceProps: (_, { addSourceProps }) => addSourceProps,
},
],
addSourceCurrentStep: [
AddSourceSteps.ConfigIntroStep,
{
setAddSourceStep: (_, addSourceCurrentStep) => addSourceCurrentStep,
},
],
sourceConfigData: [
{} as SourceConfigData,
{
setSourceConfigData: (_, sourceConfigData) => sourceConfigData,
},
],
sourceConnectData: [
{} as SourceConnectData,
{
setSourceConnectData: (_, sourceConnectData) => sourceConnectData,
},
],
dataLoading: [
true,
{
setSourceConfigData: () => false,
resetSourceState: () => false,
setPreContentSourceConfigData: () => false,
getSourceConfigData: () => true,
},
],
buttonLoading: [
false,
{
setButtonNotLoading: () => false,
setSourceConnectData: () => false,
setSourceConfigData: () => false,
resetSourceState: () => false,
saveSourceConfig: () => true,
getSourceConnectData: () => true,
createContentSource: () => true,
},
],
sectionLoading: [
true,
{
getPreContentSourceConfigData: () => true,
setPreContentSourceConfigData: () => false,
},
],
clientIdValue: [
'',
{
setClientIdValue: (_, clientIdValue) => clientIdValue,
setSourceConfigData: (_, { configuredFields: { clientId } }) => clientId || '',
resetSourceState: () => '',
},
],
clientSecretValue: [
'',
{
setClientSecretValue: (_, clientSecretValue) => clientSecretValue,
setSourceConfigData: (_, { configuredFields: { clientSecret } }) => clientSecret || '',
resetSourceState: () => '',
},
],
baseUrlValue: [
'',
{
setBaseUrlValue: (_, baseUrlValue) => baseUrlValue,
setSourceConfigData: (_, { configuredFields: { baseUrl } }) => baseUrl || '',
resetSourceState: () => '',
},
],
loginValue: [
'',
{
setSourceLoginValue: (_, loginValue) => loginValue,
resetSourceState: () => '',
},
],
passwordValue: [
'',
{
setSourcePasswordValue: (_, passwordValue) => passwordValue,
resetSourceState: () => '',
},
],
subdomainValue: [
'',
{
setSourceSubdomainValue: (_, subdomainValue) => subdomainValue,
resetSourceState: () => '',
},
],
indexPermissionsValue: [
false,
{
setSourceIndexPermissionsValue: (_, indexPermissionsValue) => indexPermissionsValue,
resetSourceState: () => false,
},
],
currentServiceType: [
'',
{
setPreContentSourceConfigData: (_, { serviceType }) => serviceType,
resetSourceState: () => '',
},
],
githubOrganizations: [
[],
{
setPreContentSourceConfigData: (_, { githubOrganizations }) => githubOrganizations,
resetSourceState: () => [],
},
],
selectedGithubOrganizationsMap: [
{} as OrganizationsMap,
{
setSelectedGithubOrganizations: (state, option) => ({
...state,
...{ [option]: !state[option] },
}),
resetSourceState: () => ({}),
},
],
preContentSourceId: [
'',
{
setPreContentSourceId: (_, preContentSourceId) => preContentSourceId,
setPreContentSourceConfigData: () => '',
resetSourceState: () => '',
},
],
oauthConfigCompleted: [
false,
{
setPreContentSourceConfigData: () => true,
},
],
},
selectors: ({ selectors }) => ({
selectedGithubOrganizations: [
() => [selectors.selectedGithubOrganizationsMap],
(orgsMap) => keys(pickBy(orgsMap)),
],
}),
listeners: ({ actions, values }) => ({
initializeAddSource: ({ addSourceProps }) => {
const { serviceType } = addSourceProps.sourceData;
actions.setAddSourceProps({ addSourceProps });
actions.getSourceConfigData(serviceType, addSourceProps);
export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceActions, AddSourceProps>>(
{
path: ['enterprise_search', 'workplace_search', 'add_source_logic'],
actions: {
setAddSourceStep: (addSourceCurrentStep: AddSourceSteps) => addSourceCurrentStep,
setSourceConfigData: (sourceConfigData: SourceConfigData) => sourceConfigData,
setSourceConnectData: (sourceConnectData: SourceConnectData) => sourceConnectData,
setClientIdValue: (clientIdValue: string) => clientIdValue,
setClientSecretValue: (clientSecretValue: string) => clientSecretValue,
setBaseUrlValue: (baseUrlValue: string) => baseUrlValue,
setSourceLoginValue: (loginValue: string) => loginValue,
setSourcePasswordValue: (passwordValue: string) => passwordValue,
setSourceSubdomainValue: (subdomainValue: string) => subdomainValue,
setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue,
setPreContentSourceConfigData: (data: PreContentSourceResponse) => data,
setPreContentSourceId: (preContentSourceId: string) => preContentSourceId,
setSelectedGithubOrganizations: (option: string) => option,
getSourceConfigData: () => true,
getSourceConnectData: (successCallback: (oauthUrl: string) => string) => ({
successCallback,
}),
getSourceReConnectData: (sourceId: string) => ({ sourceId }),
getPreContentSourceConfigData: () => true,
saveSourceConfig: (isUpdating: boolean, successCallback?: () => void) => ({
isUpdating,
successCallback,
}),
saveSourceParams: (search: Search, params: OauthParams, isOrganization: boolean) => ({
search,
params,
isOrganization,
}),
createContentSource: (successCallback: () => void, errorCallback?: () => void) => ({
successCallback,
errorCallback,
}),
resetSourceState: () => true,
setButtonNotLoading: () => true,
setFirstStep: () => true,
},
getSourceConfigData: async ({ serviceType, addSourceProps }) => {
const route = `/internal/workplace_search/org/settings/connectors/${serviceType}`;
reducers: ({ props }) => ({
addSourceCurrentStep: [
null,
{
setAddSourceStep: (_, addSourceCurrentStep) => addSourceCurrentStep,
},
],
sourceConfigData: [
{} as SourceConfigData,
{
setSourceConfigData: (_, sourceConfigData) => sourceConfigData,
},
],
sourceConnectData: [
{} as SourceConnectData,
{
setSourceConnectData: (_, sourceConnectData) => sourceConnectData,
},
],
dataLoading: [
true,
{
setSourceConfigData: () => false,
resetSourceState: () => false,
setPreContentSourceConfigData: () => false,
getSourceConfigData: () => true,
},
],
buttonLoading: [
false,
{
setButtonNotLoading: () => false,
setSourceConnectData: () => false,
setSourceConfigData: () => false,
resetSourceState: () => false,
saveSourceConfig: () => true,
getSourceConnectData: () => true,
createContentSource: () => true,
},
],
sectionLoading: [
true,
{
getPreContentSourceConfigData: () => true,
setPreContentSourceConfigData: () => false,
},
],
clientIdValue: [
'',
{
setClientIdValue: (_, clientIdValue) => clientIdValue,
setSourceConfigData: (_, { configuredFields: { clientId } }) => clientId || '',
resetSourceState: () => '',
},
],
clientSecretValue: [
'',
{
setClientSecretValue: (_, clientSecretValue) => clientSecretValue,
setSourceConfigData: (_, { configuredFields: { clientSecret } }) => clientSecret || '',
resetSourceState: () => '',
},
],
baseUrlValue: [
'',
{
setBaseUrlValue: (_, baseUrlValue) => baseUrlValue,
setSourceConfigData: (_, { configuredFields: { baseUrl } }) => baseUrl || '',
resetSourceState: () => '',
},
],
loginValue: [
'',
{
setSourceLoginValue: (_, loginValue) => loginValue,
resetSourceState: () => '',
},
],
passwordValue: [
'',
{
setSourcePasswordValue: (_, passwordValue) => passwordValue,
resetSourceState: () => '',
},
],
subdomainValue: [
'',
{
setSourceSubdomainValue: (_, subdomainValue) => subdomainValue,
resetSourceState: () => '',
},
],
indexPermissionsValue: [
false,
{
setSourceIndexPermissionsValue: (_, indexPermissionsValue) => indexPermissionsValue,
resetSourceState: () => false,
},
],
githubOrganizations: [
[],
{
setPreContentSourceConfigData: (_, { githubOrganizations }) => githubOrganizations,
resetSourceState: () => [],
},
],
selectedGithubOrganizationsMap: [
{} as OrganizationsMap,
{
setSelectedGithubOrganizations: (state, option) => ({
...state,
...{ [option]: !state[option] },
}),
resetSourceState: () => ({}),
},
],
preContentSourceId: [
'',
{
setPreContentSourceId: (_, preContentSourceId) => preContentSourceId,
setPreContentSourceConfigData: () => '',
resetSourceState: () => '',
},
],
oauthConfigCompleted: [
false,
{
setPreContentSourceConfigData: () => true,
},
],
sourceData: [getSourceData(props.serviceType) || null, {}],
}),
selectors: ({ selectors }) => ({
selectedGithubOrganizations: [
() => [selectors.selectedGithubOrganizationsMap],
(orgsMap) => keys(pickBy(orgsMap)),
],
}),
listeners: ({ actions, values, props }) => ({
getSourceConfigData: async () => {
const { serviceType } = props;
// TODO: Once multi-config support for connectors is added, this request url will need to include an ID
const route = `/internal/workplace_search/org/settings/connectors/${serviceType}`;
try {
const response = await HttpLogic.values.http.get<SourceConfigData>(route);
actions.setSourceConfigData(response);
if (addSourceProps) {
actions.setFirstStep(addSourceProps);
try {
const response = await HttpLogic.values.http.get<SourceConfigData>(route);
actions.setSourceConfigData(response);
actions.setFirstStep();
} catch (e) {
flashAPIErrors(e);
}
} catch (e) {
flashAPIErrors(e);
}
},
getSourceConnectData: async ({ serviceType, successCallback }) => {
clearFlashMessages();
const { isOrganization } = AppLogic.values;
const { subdomainValue: subdomain, indexPermissionsValue: indexPermissions } = values;
},
getSourceConnectData: async ({ successCallback }) => {
const { serviceType } = props;
clearFlashMessages();
const { isOrganization } = AppLogic.values;
const { subdomainValue: subdomain, indexPermissionsValue: indexPermissions } = values;
const route = isOrganization
? `/internal/workplace_search/org/sources/${serviceType}/prepare`
: `/internal/workplace_search/account/sources/${serviceType}/prepare`;
const route = isOrganization
? `/internal/workplace_search/org/sources/${serviceType}/prepare`
: `/internal/workplace_search/account/sources/${serviceType}/prepare`;
const indexPermissionsQuery = isOrganization
? { index_permissions: indexPermissions }
: undefined;
const indexPermissionsQuery = isOrganization
? { index_permissions: indexPermissions }
: undefined;
const query = subdomain
? {
...indexPermissionsQuery,
subdomain,
const query = subdomain
? {
...indexPermissionsQuery,
subdomain,
}
: { ...indexPermissionsQuery };
try {
const response = await HttpLogic.values.http.get<SourceConnectData>(route, {
query,
});
actions.setSourceConnectData(response);
successCallback(response.oauthUrl);
} catch (e) {
flashAPIErrors(e);
} finally {
actions.setButtonNotLoading();
}
},
getSourceReConnectData: async ({ sourceId }) => {
const { isOrganization } = AppLogic.values;
const route = isOrganization
? `/internal/workplace_search/org/sources/${sourceId}/reauth_prepare`
: `/internal/workplace_search/account/sources/${sourceId}/reauth_prepare`;
try {
const response = await HttpLogic.values.http.get<SourceConnectData>(route);
actions.setSourceConnectData(response);
} catch (e) {
flashAPIErrors(e);
}
},
getPreContentSourceConfigData: async () => {
const { isOrganization } = AppLogic.values;
const { preContentSourceId } = values;
const route = isOrganization
? `/internal/workplace_search/org/pre_sources/${preContentSourceId}`
: `/internal/workplace_search/account/pre_sources/${preContentSourceId}`;
try {
const response = await HttpLogic.values.http.get<PreContentSourceResponse>(route);
actions.setPreContentSourceConfigData(response);
} catch (e) {
flashAPIErrors(e);
}
},
saveSourceConfig: async ({ isUpdating, successCallback }) => {
clearFlashMessages();
const {
sourceConfigData: { serviceType },
baseUrlValue,
clientIdValue,
clientSecretValue,
sourceConfigData,
} = values;
const { externalConnectorUrl, externalConnectorApiKey } = ExternalConnectorLogic.values;
if (
serviceType === 'external' &&
externalConnectorUrl &&
!isValidExternalUrl(externalConnectorUrl)
) {
ExternalConnectorLogic.actions.setUrlValidation(false);
actions.setButtonNotLoading();
return;
}
const route = isUpdating
? `/internal/workplace_search/org/settings/connectors/${serviceType}`
: '/internal/workplace_search/org/settings/connectors';
const http = isUpdating ? HttpLogic.values.http.put : HttpLogic.values.http.post;
const params = {
base_url: baseUrlValue || undefined,
client_id: clientIdValue || undefined,
client_secret: clientSecretValue || undefined,
service_type: serviceType,
private_key: sourceConfigData.configuredFields?.privateKey,
public_key: sourceConfigData.configuredFields?.publicKey,
consumer_key: sourceConfigData.configuredFields?.consumerKey,
external_connector_url: (serviceType === 'external' && externalConnectorUrl) || undefined,
external_connector_api_key:
(serviceType === 'external' && externalConnectorApiKey) || undefined,
};
try {
const response = await http<SourceConfigData>(route, {
body: JSON.stringify(params),
});
if (successCallback) successCallback();
if (isUpdating) {
flashSuccessToast(
i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceConfigUpdated',
{
defaultMessage: 'Successfully updated configuration.',
}
)
);
}
: { ...indexPermissionsQuery };
try {
const response = await HttpLogic.values.http.get<SourceConnectData>(route, {
query,
});
actions.setSourceConnectData(response);
successCallback(response.oauthUrl);
} catch (e) {
flashAPIErrors(e);
} finally {
actions.setButtonNotLoading();
}
},
getSourceReConnectData: async ({ sourceId }) => {
const { isOrganization } = AppLogic.values;
const route = isOrganization
? `/internal/workplace_search/org/sources/${sourceId}/reauth_prepare`
: `/internal/workplace_search/account/sources/${sourceId}/reauth_prepare`;
try {
const response = await HttpLogic.values.http.get<SourceConnectData>(route);
actions.setSourceConnectData(response);
} catch (e) {
flashAPIErrors(e);
}
},
getPreContentSourceConfigData: async () => {
const { isOrganization } = AppLogic.values;
const { preContentSourceId } = values;
const route = isOrganization
? `/internal/workplace_search/org/pre_sources/${preContentSourceId}`
: `/internal/workplace_search/account/pre_sources/${preContentSourceId}`;
try {
const response = await HttpLogic.values.http.get<PreContentSourceResponse>(route);
actions.setPreContentSourceConfigData(response);
} catch (e) {
flashAPIErrors(e);
}
},
saveSourceConfig: async ({ isUpdating, successCallback }) => {
clearFlashMessages();
const {
sourceConfigData: { serviceType },
baseUrlValue,
clientIdValue,
clientSecretValue,
sourceConfigData,
} = values;
const { externalConnectorUrl, externalConnectorApiKey } = ExternalConnectorLogic.values;
if (
serviceType === 'external' &&
externalConnectorUrl &&
!isValidExternalUrl(externalConnectorUrl)
) {
ExternalConnectorLogic.actions.setUrlValidation(false);
actions.setButtonNotLoading();
return;
}
const route = isUpdating
? `/internal/workplace_search/org/settings/connectors/${serviceType}`
: '/internal/workplace_search/org/settings/connectors';
const http = isUpdating ? HttpLogic.values.http.put : HttpLogic.values.http.post;
const params = {
base_url: baseUrlValue || undefined,
client_id: clientIdValue || undefined,
client_secret: clientSecretValue || undefined,
service_type: serviceType,
private_key: sourceConfigData.configuredFields?.privateKey,
public_key: sourceConfigData.configuredFields?.publicKey,
consumer_key: sourceConfigData.configuredFields?.consumerKey,
external_connector_url: (serviceType === 'external' && externalConnectorUrl) || undefined,
external_connector_api_key:
(serviceType === 'external' && externalConnectorApiKey) || undefined,
};
try {
const response = await http<SourceConfigData>(route, {
body: JSON.stringify(params),
});
if (successCallback) successCallback();
if (isUpdating) {
flashSuccessToast(
i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceConfigUpdated',
{
defaultMessage: 'Successfully updated configuration.',
}
)
);
actions.setSourceConfigData(response);
} catch (e) {
flashAPIErrors(e);
} finally {
actions.setButtonNotLoading();
}
actions.setSourceConfigData(response);
} catch (e) {
flashAPIErrors(e);
} finally {
actions.setButtonNotLoading();
}
},
saveSourceParams: async ({ search, params, isOrganization }) => {
const { http } = HttpLogic.values;
const { navigateToUrl } = KibanaLogic.values;
const { setAddedSource } = SourcesLogic.actions;
const query = { ...params };
const route = '/internal/workplace_search/sources/create';
},
saveSourceParams: async ({ search, params, isOrganization }) => {
const { http } = HttpLogic.values;
const { navigateToUrl } = KibanaLogic.values;
const { setAddedSource } = SourcesLogic.actions;
const query = { ...params };
const route = '/internal/workplace_search/sources/create';
/**
/**
There is an extreme edge case where the user is trying to connect Github as source from ent-search,
after configuring it in Kibana. When this happens, Github redirects the user from ent-search to Kibana
with special error properties in the query params. In this case we need to redirect the user to the
app home page and display the error message, and not persist the other query params to the server.
*/
if (params.error_description) {
navigateToUrl(isOrganization ? '/' : PRIVATE_SOURCES_PATH);
setErrorMessage(
isOrganization
? params.error_description
: PERSONAL_DASHBOARD_SOURCE_ERROR(params.error_description)
);
return;
}
try {
const response = await http.get<{
serviceName: string;
indexPermissions: boolean;
serviceType: string;
preContentSourceId: string;
hasConfigureStep: boolean;
}>(route, { query });
const { serviceName, indexPermissions, serviceType, preContentSourceId, hasConfigureStep } =
response;
// GitHub requires an intermediate configuration step, where we collect the repos to index.
if (hasConfigureStep && !values.oauthConfigCompleted) {
actions.setPreContentSourceId(preContentSourceId);
navigateToUrl(
getSourcesPath(`${getAddPath('github')}/configure${search}`, isOrganization)
if (params.error_description) {
navigateToUrl(isOrganization ? '/' : PRIVATE_SOURCES_PATH);
setErrorMessage(
isOrganization
? params.error_description
: PERSONAL_DASHBOARD_SOURCE_ERROR(params.error_description)
);
} else {
setAddedSource(serviceName, indexPermissions, serviceType);
navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization));
return;
}
} catch (e) {
navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization));
flashAPIErrors(e);
}
},
setFirstStep: ({ addSourceProps }) => {
const firstStep = getFirstStep(
addSourceProps,
values.sourceConfigData,
SourcesLogic.values.externalConfigured
);
actions.setAddSourceStep(firstStep);
},
createContentSource: async ({ serviceType, successCallback, errorCallback }) => {
clearFlashMessages();
const { isOrganization } = AppLogic.values;
const route = isOrganization
? '/internal/workplace_search/org/create_source'
: '/internal/workplace_search/account/create_source';
const {
selectedGithubOrganizations: githubOrganizations,
loginValue,
passwordValue,
indexPermissionsValue,
} = values;
try {
const response = await http.get<{
serviceName: string;
indexPermissions: boolean;
serviceType: string;
preContentSourceId: string;
hasConfigureStep: boolean;
}>(route, { query });
const {
serviceName,
indexPermissions,
serviceType,
preContentSourceId,
hasConfigureStep,
} = response;
const params = {
service_type: serviceType,
login: loginValue || undefined,
password: passwordValue || undefined,
organizations: githubOrganizations.length > 0 ? githubOrganizations : undefined,
index_permissions: indexPermissionsValue || undefined,
} as {
[key: string]: string | string[] | undefined;
};
// GitHub requires an intermediate configuration step, where we collect the repos to index.
if (hasConfigureStep && !values.oauthConfigCompleted) {
actions.setPreContentSourceId(preContentSourceId);
navigateToUrl(
getSourcesPath(`${getAddPath('github')}/configure${search}`, isOrganization)
);
} else {
setAddedSource(serviceName, indexPermissions, serviceType);
navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization));
}
} catch (e) {
navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization));
flashAPIErrors(e);
}
},
setFirstStep: () => {
const firstStep = getFirstStep(values.sourceConfigData, props.initialStep);
actions.setAddSourceStep(firstStep);
},
createContentSource: async ({ successCallback, errorCallback }) => {
const { serviceType } = props;
clearFlashMessages();
const { isOrganization } = AppLogic.values;
const route = isOrganization
? '/internal/workplace_search/org/create_source'
: '/internal/workplace_search/account/create_source';
// Remove undefined values from params
Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
const {
selectedGithubOrganizations: githubOrganizations,
loginValue,
passwordValue,
indexPermissionsValue,
} = values;
try {
await HttpLogic.values.http.post(route, {
body: JSON.stringify({ ...params }),
});
successCallback();
} catch (e) {
flashAPIErrors(e);
if (errorCallback) errorCallback();
} finally {
actions.setButtonNotLoading();
}
},
}),
});
const params = {
service_type: serviceType,
login: loginValue || undefined,
password: passwordValue || undefined,
organizations: githubOrganizations.length > 0 ? githubOrganizations : undefined,
index_permissions: indexPermissionsValue || undefined,
} as {
[key: string]: string | string[] | undefined;
};
const getFirstStep = (
props: AddSourceProps,
sourceConfigData: SourceConfigData,
externalConfigured: boolean
): AddSourceSteps => {
const {
connect,
configure,
reAuthenticate,
sourceData: { serviceType, externalConnectorAvailable },
} = props;
// We can land on this page from a choice page for multiple types of connectors
// If that's the case we want to skip the intro and configuration, if the external & internal connector have already been configured
const { configuredFields, configured } = sourceConfigData;
if (externalConnectorAvailable && configured && externalConfigured)
return AddSourceSteps.ConnectInstanceStep;
if (externalConnectorAvailable && !configured && externalConfigured)
return AddSourceSteps.SaveConfigStep;
if (serviceType === 'external') {
// external connectors can be partially configured, so we need to check which fields are filled
if (configuredFields?.clientId && configuredFields?.clientSecret) {
return AddSourceSteps.ConnectInstanceStep;
}
// Unconfigured external connectors have already shown the intro step before the choice page, so we don't want to show it again
return AddSourceSteps.SaveConfigStep;
// Remove undefined values from params
Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
try {
await HttpLogic.values.http.post(route, {
body: JSON.stringify({ ...params }),
});
successCallback();
} catch (e) {
flashAPIErrors(e);
if (errorCallback) errorCallback();
} finally {
actions.setButtonNotLoading();
}
},
}),
}
if (connect) return AddSourceSteps.ConnectInstanceStep;
if (configure) return AddSourceSteps.ConfigureOauthStep;
if (reAuthenticate) return AddSourceSteps.ReauthenticateStep;
return AddSourceSteps.ConfigIntroStep;
);
const getFirstStep = (sourceConfigData: SourceConfigData, initialStep?: string): AddSourceSteps => {
const {
serviceType,
configured,
configuredFields: { clientId, clientSecret },
} = sourceConfigData;
if (initialStep === 'connect') return AddSourceSteps.ConnectInstanceStep;
if (initialStep === 'configure') return AddSourceSteps.ConfigureOauthStep;
if (initialStep === 'reauthenticate') return AddSourceSteps.ReauthenticateStep;
if (serviceType !== 'external' && configured) return AddSourceSteps.ConnectInstanceStep;
// TODO remove this once external/BYO connectors track `configured` properly
if (serviceType === 'external' && clientId && clientSecret)
return AddSourceSteps.ConnectInstanceStep;
return AddSourceSteps.SaveConfigStep;
};

View file

@ -26,7 +26,7 @@ describe('AvailableSourcesList', () => {
const wrapper = shallow(<AvailableSourcesList sources={mergedAvailableSources} />);
expect(wrapper.find(EuiTitle)).toHaveLength(1);
expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(24);
expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(25);
expect(wrapper.find('[data-test-subj="CustomAPISourceLink"]')).toHaveLength(1);
});

View file

@ -18,6 +18,7 @@ import {
EuiTitle,
EuiText,
EuiToolTip,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -43,8 +44,13 @@ interface AvailableSourcesListProps {
export const AvailableSourcesList: React.FC<AvailableSourcesListProps> = ({ sources }) => {
const { hasPlatinumLicense } = useValues(LicensingLogic);
const getSourceCard = ({ name, serviceType, accountContextOnly }: SourceDataItem) => {
const addPath = getAddPath(serviceType);
const getSourceCard = ({
accountContextOnly,
baseServiceType,
name,
serviceType,
}: SourceDataItem) => {
const addPath = getAddPath(serviceType, baseServiceType);
const disabled = !hasPlatinumLicense && accountContextOnly;
const connectButton = () => {
@ -61,15 +67,30 @@ export const AvailableSourcesList: React.FC<AvailableSourcesListProps> = ({ sour
}
)}
>
<EuiButtonEmptyTo disabled={disabled} to={getSourcesPath(addPath, true)}>
Connect
</EuiButtonEmptyTo>
<EuiButtonEmpty disabled>
{i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.availableSourceList.connectButtonLabel',
{
defaultMessage: 'Connect',
}
)}
</EuiButtonEmpty>
</EuiToolTip>
);
} else {
return (
<EuiButtonEmptyTo disabled={disabled} to={getSourcesPath(addPath, true)}>
Connect
<EuiButtonEmptyTo
to={
getSourcesPath(addPath, true) +
(serviceType === 'custom' || serviceType === 'external' ? '' : '/intro')
}
>
{i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.availableSourceList.connectButtonLabel',
{
defaultMessage: 'Connect',
}
)}
</EuiButtonEmptyTo>
);
}
@ -79,7 +100,7 @@ export const AvailableSourcesList: React.FC<AvailableSourcesListProps> = ({ sour
<>
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
<SourceIcon serviceType={serviceType} name={name} size="l" />
<SourceIcon serviceType={baseServiceType || serviceType} name={name} size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="m">{name}</EuiText>

View file

@ -5,20 +5,19 @@
* 2.0.
*/
import { mockKibanaValues, setMockValues } from '../../../../../__mocks__/kea_logic';
import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
import React from 'react';
import { mount } from 'enzyme';
import { EuiButton } from '@elastic/eui';
import { EuiButtonTo } from '../../../../../shared/react_router_helpers';
import { staticSourceData } from '../../source_data';
import { ConfigurationChoice } from './configuration_choice';
describe('ConfigurationChoice', () => {
const { navigateToUrl } = mockKibanaValues;
const props = {
sourceData: staticSourceData[0],
};
@ -28,31 +27,23 @@ describe('ConfigurationChoice', () => {
categories: [],
},
};
const mockActions = {
initializeSources: jest.fn(),
resetSourcesState: jest.fn(),
};
beforeEach(() => {
setMockValues(mockValues);
jest.clearAllMocks();
setMockValues(mockValues);
setMockActions(mockActions);
});
it('renders internal connector if available', () => {
const wrapper = mount(<ConfigurationChoice {...{ ...props }} />);
expect(wrapper.find('EuiCard')).toHaveLength(1);
expect(wrapper.find(EuiButton)).toHaveLength(1);
});
it('should navigate to internal connector on internal connector click', () => {
const wrapper = mount(<ConfigurationChoice {...props} />);
const button = wrapper.find(EuiButton);
button.simulate('click');
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/internal/');
});
it('should call prop function when provided on internal connector click', () => {
const advanceSpy = jest.fn();
const wrapper = mount(<ConfigurationChoice {...{ ...props, goToInternalStep: advanceSpy }} />);
const button = wrapper.find(EuiButton);
button.simulate('click');
expect(navigateToUrl).not.toHaveBeenCalled();
expect(advanceSpy).toHaveBeenCalled();
const internalConnectorCard = wrapper.find('[data-test-subj="InternalConnectorCard"]');
expect(internalConnectorCard).toHaveLength(1);
expect(internalConnectorCard.find(EuiButtonTo).prop('to')).toEqual('/sources/add/box/');
});
it('renders external connector if available', () => {
@ -62,32 +53,36 @@ describe('ConfigurationChoice', () => {
...props,
sourceData: {
...props.sourceData,
internalConnectorAvailable: false,
externalConnectorAvailable: true,
serviceType: 'share_point',
},
}}
/>
);
expect(wrapper.find('EuiCard')).toHaveLength(1);
expect(wrapper.find(EuiButton)).toHaveLength(1);
const externalConnectorCard = wrapper.find('[data-test-subj="ExternalConnectorCard"]');
expect(externalConnectorCard).toHaveLength(1);
expect(externalConnectorCard.find(EuiButtonTo).prop('to')).toEqual(
'/sources/add/share_point/external/connector_registration'
);
});
it('should navigate to external connector on external connector click', () => {
it('renders disabled message if external connector is available but user has already configured', () => {
setMockValues({ ...mockValues, externalConfigured: true });
const wrapper = mount(
<ConfigurationChoice
{...{
...props,
sourceData: {
...props.sourceData,
internalConnectorAvailable: false,
externalConnectorAvailable: true,
serviceType: 'share_point',
},
}}
/>
);
const button = wrapper.find(EuiButton);
button.simulate('click');
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/external/');
const externalConnectorCard = wrapper.find('[data-test-subj="ExternalConnectorCard"]');
expect(externalConnectorCard.prop('disabledMessage')).toBeDefined();
});
it('renders custom connector if available', () => {
@ -97,33 +92,16 @@ describe('ConfigurationChoice', () => {
...props,
sourceData: {
...props.sourceData,
internalConnectorAvailable: false,
externalConnectorAvailable: false,
customConnectorAvailable: true,
serviceType: 'share_point_server',
},
}}
/>
);
expect(wrapper.find('EuiCard')).toHaveLength(1);
expect(wrapper.find(EuiButton)).toHaveLength(1);
});
it('should navigate to custom connector on custom connector click', () => {
const wrapper = mount(
<ConfigurationChoice
{...{
...props,
sourceData: {
...props.sourceData,
internalConnectorAvailable: false,
externalConnectorAvailable: false,
customConnectorAvailable: true,
},
}}
/>
const customConnectorCard = wrapper.find('[data-test-subj="CustomConnectorCard"]');
expect(customConnectorCard).toHaveLength(1);
expect(customConnectorCard.find(EuiButtonTo).prop('to')).toEqual(
'/sources/add/share_point_server/custom'
);
const button = wrapper.find(EuiButton);
button.simulate('click');
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/custom/');
});
});

View file

@ -5,92 +5,85 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { useValues } from 'kea';
import { useActions, useValues } from 'kea';
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { EuiButton, EuiCard, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { KibanaLogic } from '../../../../../shared/kibana';
import { EuiButtonTo } from '../../../../../shared/react_router_helpers';
import { AppLogic } from '../../../../app_logic';
import { getAddPath, getSourcesPath } from '../../../../routes';
import { SourceDataItem } from '../../../../types';
import { AddSourceHeader } from './add_source_header';
import { AddSourceLogic } from './add_source_logic';
import { hasCustomConnectorOption, hasExternalConnectorOption } from '../../source_data';
interface ConfigurationChoiceProps {
sourceData: SourceDataItem;
goToInternalStep?: () => void;
}
import { SourcesLogic } from '../../sources_logic';
import { AddSourceHeader } from './add_source_header';
interface CardProps {
title: string;
description: string;
buttonText: string;
onClick: () => void;
to: string;
badgeLabel?: string;
disabledMessage?: string;
}
const ConnectorCard: React.FC<CardProps> = ({
title,
description,
buttonText,
to,
badgeLabel,
disabledMessage,
}: CardProps) => (
<EuiFlexItem grow>
<EuiCard
isDisabled={!!disabledMessage}
hasBorder
title={title}
description={disabledMessage || description}
betaBadgeProps={{ label: badgeLabel }}
footer={
<EuiButtonTo color="primary" to={to} isDisabled={!!disabledMessage}>
{buttonText}
</EuiButtonTo>
}
/>
</EuiFlexItem>
);
interface ConfigurationChoiceProps {
sourceData: SourceDataItem;
}
export const ConfigurationChoice: React.FC<ConfigurationChoiceProps> = ({
sourceData: {
name,
serviceType,
externalConnectorAvailable,
internalConnectorAvailable,
customConnectorAvailable,
},
goToInternalStep,
sourceData: { name, categories = [], serviceType },
}) => {
const { isOrganization } = useValues(AppLogic);
const { sourceConfigData } = useValues(AddSourceLogic);
const { categories } = sourceConfigData;
const goToInternal = goToInternalStep
? goToInternalStep
: () =>
KibanaLogic.values.navigateToUrl(
`${getSourcesPath(
`${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`,
isOrganization
)}/`
);
const goToExternal = () =>
KibanaLogic.values.navigateToUrl(
`${getSourcesPath(
`${getSourcesPath(getAddPath(serviceType), isOrganization)}/external`,
isOrganization
)}/`
);
const goToCustom = () =>
KibanaLogic.values.navigateToUrl(
`${getSourcesPath(
`${getSourcesPath(getAddPath(serviceType), isOrganization)}/custom`,
isOrganization
)}/`
);
const externalConnectorAvailable = hasExternalConnectorOption(serviceType);
const customConnectorAvailable = hasCustomConnectorOption(serviceType);
const ConnectorCard: React.FC<CardProps> = ({
title,
description,
buttonText,
onClick,
badgeLabel,
}: CardProps) => (
<EuiFlexItem grow>
<EuiCard
hasBorder
title={title}
description={description}
betaBadgeProps={{ label: badgeLabel }}
footer={
<EuiButton color="primary" onClick={onClick}>
{buttonText}
</EuiButton>
}
/>
</EuiFlexItem>
);
const { isOrganization } = useValues(AppLogic);
const { initializeSources, resetSourcesState } = useActions(SourcesLogic);
const { externalConfigured } = useValues(SourcesLogic);
useEffect(() => {
initializeSources();
return resetSourcesState;
}, []);
const internalTo = `${getSourcesPath(getAddPath(serviceType), isOrganization)}/`;
const externalTo = `${getSourcesPath(
getAddPath('external', serviceType),
isOrganization
)}/connector_registration`;
const customTo = `${getSourcesPath(getAddPath('custom', serviceType), isOrganization)}`;
const internalConnectorProps: CardProps = {
title: i18n.translate(
@ -118,7 +111,7 @@ export const ConfigurationChoice: React.FC<ConfigurationChoiceProps> = ({
defaultMessage: 'Recommended',
}
),
onClick: goToInternal,
to: internalTo,
};
const externalConnectorProps: CardProps = {
@ -141,7 +134,7 @@ export const ConfigurationChoice: React.FC<ConfigurationChoiceProps> = ({
defaultMessage: 'Instructions',
}
),
onClick: goToExternal,
to: externalTo,
badgeLabel: i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.betaLabel',
{
@ -169,7 +162,7 @@ export const ConfigurationChoice: React.FC<ConfigurationChoiceProps> = ({
defaultMessage: 'Instructions',
}
),
onClick: goToCustom,
to: customTo,
};
return (
@ -177,9 +170,26 @@ export const ConfigurationChoice: React.FC<ConfigurationChoiceProps> = ({
<AddSourceHeader name={name} serviceType={serviceType} categories={categories} />
<EuiSpacer size="l" />
<EuiFlexGroup justifyContent="flexStart" direction="row" responsive={false}>
{internalConnectorAvailable && <ConnectorCard {...internalConnectorProps} />}
{externalConnectorAvailable && <ConnectorCard {...externalConnectorProps} />}
{customConnectorAvailable && <ConnectorCard {...customConnectorProps} />}
<ConnectorCard {...internalConnectorProps} data-test-subj="InternalConnectorCard" />
{externalConnectorAvailable && (
<ConnectorCard
{...externalConnectorProps}
disabledMessage={
externalConfigured
? i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.alreadyConfiguredMessage',
{
defaultMessage: "You've already configured an external connector",
}
)
: undefined
}
data-test-subj="ExternalConnectorCard"
/>
)}
{customConnectorAvailable && (
<ConnectorCard {...customConnectorProps} data-test-subj="CustomConnectorCard" />
)}
</EuiFlexGroup>
</>
);

View file

@ -14,11 +14,10 @@ import { EuiText, EuiTitle } from '@elastic/eui';
import { ConfigurationIntro } from './configuration_intro';
describe('ConfigurationIntro', () => {
const advanceStep = jest.fn();
const props = {
header: <h1>Header</h1>,
name: 'foo',
advanceStep,
advanceStepTo: '',
};
it('renderscontext', () => {

View file

@ -9,7 +9,6 @@ import React from 'react';
import {
EuiBadge,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@ -18,9 +17,12 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButtonTo } from '../../../../../shared/react_router_helpers';
import connectionIllustration from '../../../../assets/connection_illustration.svg';
import {
@ -37,12 +39,12 @@ import {
interface ConfigurationIntroProps {
header: React.ReactNode;
name: string;
advanceStep(): void;
advanceStepTo: string;
}
export const ConfigurationIntro: React.FC<ConfigurationIntroProps> = ({
name,
advanceStep,
advanceStepTo,
header,
}) => (
<>
@ -144,11 +146,11 @@ export const ConfigurationIntro: React.FC<ConfigurationIntroProps> = ({
<EuiFlexItem>
<EuiSpacer size="l" />
<EuiFormRow>
<EuiButton
<EuiButtonTo
color="primary"
data-test-subj="ConfigureStepButton"
fill
onClick={advanceStep}
to={advanceStepTo}
>
{i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.configIntro.configure.button',
@ -157,7 +159,7 @@ export const ConfigurationIntro: React.FC<ConfigurationIntroProps> = ({
values: { name },
}
)}
</EuiButton>
</EuiButtonTo>
</EuiFormRow>
<EuiSpacer size="xl" />
</EuiFlexItem>

View file

@ -22,7 +22,7 @@ describe('ConfigureOauth', () => {
const onFormCreated = jest.fn();
const getPreContentSourceConfigData = jest.fn();
const setSelectedGithubOrganizations = jest.fn();
const createContentSource = jest.fn((_, formSubmitSuccess, handleFormSubmitError) => {
const createContentSource = jest.fn((formSubmitSuccess, handleFormSubmitError) => {
formSubmitSuccess();
handleFormSubmitError();
});

View file

@ -35,12 +35,8 @@ export const ConfigureOauth: React.FC<ConfigureOauthProps> = ({ name, onFormCrea
const { getPreContentSourceConfigData, setSelectedGithubOrganizations, createContentSource } =
useActions(AddSourceLogic);
const {
currentServiceType,
githubOrganizations,
selectedGithubOrganizationsMap,
sectionLoading,
} = useValues(AddSourceLogic);
const { githubOrganizations, selectedGithubOrganizationsMap, sectionLoading } =
useValues(AddSourceLogic);
const checkboxOptions = githubOrganizations.map((item) => ({ id: item, label: item }));
@ -54,7 +50,7 @@ export const ConfigureOauth: React.FC<ConfigureOauthProps> = ({ name, onFormCrea
const handleFormSubmit = (e: FormEvent) => {
setFormLoading(true);
e.preventDefault();
createContentSource(currentServiceType, formSubmitSuccess, handleFormSubmitError);
createContentSource(formSubmitSuccess, handleFormSubmitError);
};
const configfieldsForm = (

View file

@ -11,6 +11,8 @@ import React from 'react';
import { shallow } from 'enzyme';
import { EuiButtonEmpty } from '@elastic/eui';
import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers';
import { ConfiguredSourcesList } from './configured_sources_list';
@ -24,47 +26,19 @@ describe('ConfiguredSourcesList', () => {
it('renders', () => {
const wrapper = shallow(<ConfiguredSourcesList {...props} />);
expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(20);
expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(21);
expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(2);
expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(23);
expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(24);
});
it('does show connect button for a connected external source', () => {
const wrapper = shallow(
<ConfiguredSourcesList
{...{
sources: [{ ...mergedConfiguredSources[0], connected: true, serviceType: 'external' }],
isOrganization: true,
}}
/>
);
expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(1);
});
it('does show connect button for an unconnected external source', () => {
const wrapper = shallow(
<ConfiguredSourcesList
{...{
sources: [{ ...mergedConfiguredSources[0], connected: false, serviceType: 'external' }],
isOrganization: true,
}}
/>
);
const button = wrapper.find(EuiButtonEmptyTo);
expect(button).toHaveLength(1);
expect(button.prop('to')).toEqual('/sources/add/external/connect');
});
it('connect button for an unconnected source with multiple connector options routes to choice page', () => {
it('shows connect button for an source with multiple connector options that routes to choice page', () => {
const wrapper = shallow(
<ConfiguredSourcesList
{...{
sources: [
{
...mergedConfiguredSources[0],
connected: false,
serviceType: 'share_point',
externalConnectorAvailable: true,
},
],
isOrganization: true,
@ -73,28 +47,46 @@ describe('ConfiguredSourcesList', () => {
);
const button = wrapper.find(EuiButtonEmptyTo);
expect(button).toHaveLength(1);
expect(button.prop('to')).toEqual('/sources/add/share_point/');
expect(button.prop('to')).toEqual('/sources/add/share_point/choice');
});
it('connect button for a source with multiple connector options routes to connect page for private sources', () => {
it('shows connect button for a source without multiple connector options that routes to add page', () => {
const wrapper = shallow(
<ConfiguredSourcesList
{...{
sources: [
{
...mergedConfiguredSources[0],
connected: false,
serviceType: 'share_point',
externalConnectorAvailable: true,
serviceType: 'slack',
},
],
isOrganization: false,
isOrganization: true,
}}
/>
);
const button = wrapper.find(EuiButtonEmptyTo);
expect(button).toHaveLength(1);
expect(button.prop('to')).toEqual('/p/sources/add/share_point/connect');
expect(button.prop('to')).toEqual('/sources/add/slack/');
});
it('disabled when in organization mode and connector is account context only', () => {
const wrapper = shallow(
<ConfiguredSourcesList
{...{
sources: [
{
...mergedConfiguredSources[0],
serviceType: 'gmail',
accountContextOnly: true,
},
],
isOrganization: true,
}}
/>
);
const button = wrapper.find(EuiButtonEmpty);
expect(button).toHaveLength(1);
expect(button.prop('isDisabled')).toBe(true);
});
it('handles empty state', () => {

View file

@ -27,7 +27,8 @@ import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers';
import { SourceIcon } from '../../../../components/shared/source_icon';
import { getAddPath, getSourcesPath } from '../../../../routes';
import { SourceDataItem } from '../../../../types';
import { hasMultipleConnectorOptions } from '../../../../utils';
import { hasMultipleConnectorOptions } from '../../source_data';
import {
CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP,
@ -72,7 +73,8 @@ export const ConfiguredSourcesList: React.FC<ConfiguredSourcesProps> = ({
const visibleSources = (
<EuiFlexGrid columns={3} gutterSize="m" className="source-grid-configured">
{sources.map((sourceData, i) => {
const { connected, accountContextOnly, name, serviceType, isBeta } = sourceData;
const { connected, accountContextOnly, name, serviceType, isBeta, baseServiceType } =
sourceData;
return (
<React.Fragment key={i}>
<EuiFlexItem
@ -107,7 +109,11 @@ export const ConfiguredSourcesList: React.FC<ConfiguredSourcesProps> = ({
responsive={false}
>
<EuiFlexItem>
<SourceIcon serviceType={serviceType} name={name} size="xxl" />
<SourceIcon
serviceType={baseServiceType ?? serviceType}
name={name}
size="xxl"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">
@ -128,7 +134,7 @@ export const ConfiguredSourcesList: React.FC<ConfiguredSourcesProps> = ({
<EuiButtonEmptyTo
className="eui-fullWidth"
to={`${getSourcesPath(getAddPath(serviceType), isOrganization)}/${
hasMultipleConnectorOptions(sourceData) && isOrganization ? '' : 'connect'
hasMultipleConnectorOptions(serviceType) && isOrganization ? 'choice' : ''
}`}
>
{!connected

View file

@ -33,10 +33,10 @@ describe('ConnectInstance', () => {
const setSourcePasswordValue = jest.fn();
const setSourceSubdomainValue = jest.fn();
const setSourceIndexPermissionsValue = jest.fn();
const getSourceConnectData = jest.fn((_, redirectOauth) => {
const getSourceConnectData = jest.fn((redirectOauth) => {
redirectOauth();
});
const createContentSource = jest.fn((_, redirectFormCreated) => {
const createContentSource = jest.fn((redirectFormCreated) => {
redirectFormCreated();
});

View file

@ -46,7 +46,6 @@ export const ConnectInstance: React.FC<ConnectInstanceProps> = ({
features,
objTypes,
name,
serviceType,
needsPermissions,
onFormCreated,
header,
@ -74,8 +73,8 @@ export const ConnectInstance: React.FC<ConnectInstanceProps> = ({
const redirectOauth = (oauthUrl: string) => window.location.replace(oauthUrl);
const redirectFormCreated = () => onFormCreated(name);
const onOauthFormSubmit = () => getSourceConnectData(serviceType, redirectOauth);
const onCredentialsFormSubmit = () => createContentSource(serviceType, redirectFormCreated);
const onOauthFormSubmit = () => getSourceConnectData(redirectOauth);
const onCredentialsFormSubmit = () => createContentSource(redirectFormCreated);
const handleFormSubmit = (e: FormEvent) => {
e.preventDefault();

View file

@ -39,7 +39,7 @@ import {
SOURCE_FEATURES_GLOBAL_ACCESS_PERMISSIONS_FEATURE_DESCRIPTION,
} from './constants';
interface ConnectInstanceProps {
interface SourceFeatureProps {
features?: Features;
objTypes?: string[];
name: string;
@ -47,7 +47,7 @@ interface ConnectInstanceProps {
type IncludedFeatureIds = Exclude<FeatureIds, FeatureIds.DocumentLevelPermissions>;
export const SourceFeatures: React.FC<ConnectInstanceProps> = ({ features, objTypes, name }) => {
export const SourceFeatures: React.FC<SourceFeatureProps> = ({ features, objTypes, name }) => {
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { isOrganization } = useValues(AppLogic);

View file

@ -24,14 +24,6 @@ const customSource = {
name: 'name',
};
const preconfiguredSourceData = {
...staticCustomSourceData,
serviceType: 'sharepoint-server',
configuration: {
...staticCustomSourceData.configuration,
githubRepository: 'elastic/sharepoint-server-connector',
},
};
const mockValues = {
sourceData: staticCustomSourceData,
};
@ -44,9 +36,7 @@ describe('CustomSourceDeployment', () => {
jest.clearAllMocks();
setMockValues(mockValues);
wrapper = shallow(
<CustomSourceDeployment source={customSource} sourceData={staticCustomSourceData} />
);
wrapper = shallow(<CustomSourceDeployment source={customSource} />);
});
it('contains a source identifier', () => {
@ -69,7 +59,7 @@ describe('CustomSourceDeployment', () => {
});
wrapper = shallow(
<CustomSourceDeployment source={customSource} sourceData={preconfiguredSourceData} />
<CustomSourceDeployment source={customSource} baseServiceType={'share_point_server'} />
);
});
@ -86,9 +76,7 @@ describe('CustomSourceDeployment', () => {
jest.clearAllMocks();
setMockValues(mockValues);
const wrapper = shallow(
<CustomSourceDeployment small source={customSource} sourceData={staticCustomSourceData} />
);
const wrapper = shallow(<CustomSourceDeployment small source={customSource} />);
expect(wrapper.find(EuiPanel).prop('paddingSize')).toEqual('m');

View file

@ -14,17 +14,30 @@ import { EuiLinkTo } from '../../../../shared/react_router_helpers';
import { API_KEY_LABEL } from '../../../constants';
import { API_KEYS_PATH } from '../../../routes';
import { ContentSource, CustomSource, SourceDataItem } from '../../../types';
import { ContentSource, CustomSource } from '../../../types';
import { getSourceData } from '../source_data';
import { SourceIdentifier } from './source_identifier';
interface Props {
source: ContentSource | CustomSource;
sourceData: SourceDataItem;
baseServiceType?: string;
small?: boolean;
}
export const CustomSourceDeployment: React.FC<Props> = ({ source, sourceData, small = false }) => {
export const CustomSourceDeployment: React.FC<Props> = ({
source,
baseServiceType,
small = false,
}) => {
const { name, id } = source;
const sourceData = getSourceData('custom', baseServiceType);
if (!sourceData) {
return null;
}
const {
configuration: { documentationUrl, githubRepository },
} = sourceData;

View file

@ -16,8 +16,6 @@ import { EuiCallOut, EuiConfirmModal, EuiEmptyPrompt, EuiTable } from '@elastic/
import { ComponentLoader } from '../../../components/shared/component_loader';
import * as SourceData from '../source_data';
import { CustomSourceDeployment } from './custom_source_deployment';
import { Overview } from './overview';
@ -144,33 +142,6 @@ describe('Overview', () => {
expect(initializeSourceSynchronization).toHaveBeenCalled();
});
it('uses a base service type if one is provided', () => {
jest.spyOn(SourceData, 'getSourceData');
setMockValues({
...mockValues,
contentSource: {
...fullContentSources[0],
baseServiceType: 'share_point_server',
},
});
shallow(<Overview />);
expect(SourceData.getSourceData).toHaveBeenCalledWith('share_point_server');
});
it('defaults to the regular service tye', () => {
jest.spyOn(SourceData, 'getSourceData');
setMockValues({
...mockValues,
contentSource: fullContentSources[0],
});
shallow(<Overview />);
expect(SourceData.getSourceData).toHaveBeenCalledWith('custom');
});
describe('custom sources', () => {
it('includes deployment instructions', () => {
setMockValues({

View file

@ -81,7 +81,6 @@ import {
SOURCE_SYNC_CONFIRM_TITLE,
SOURCE_SYNC_CONFIRM_MESSAGE,
} from '../constants';
import { getSourceData } from '../source_data';
import { SourceLogic } from '../source_logic';
import { CustomSourceDeployment } from './custom_source_deployment';
@ -106,12 +105,10 @@ export const Overview: React.FC = () => {
isFederatedSource,
isIndexedSource,
name,
serviceType,
baseServiceType,
} = contentSource;
const serviceType = contentSource.baseServiceType || contentSource.serviceType;
const sourceData = getSourceData(serviceType);
const [isSyncing, setIsSyncing] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const closeModal = () => setIsModalVisible(false);
@ -431,7 +428,7 @@ export const Overview: React.FC = () => {
</h6>
</EuiTitle>
<EuiSpacer size="s" />
<CustomSourceDeployment source={contentSource} sourceData={sourceData} small />
<CustomSourceDeployment source={contentSource} baseServiceType={baseServiceType} small />
</>
);

View file

@ -88,7 +88,7 @@ export const SourceSettings: React.FC = () => {
const { isOrganization } = useValues(AppLogic);
useEffect(() => {
getSourceConfigData(serviceType);
getSourceConfigData();
}, []);
const isGithubApp =

View file

@ -17,9 +17,10 @@ import {
} from '../../constants';
import { FeatureIds, SourceDataItem } from '../../types';
export const staticExternalSourceData: SourceDataItem = {
// TODO remove Sharepoint-specific content after BYO connector support
export const staticGenericExternalSourceData: SourceDataItem = {
name: SOURCE_NAMES.SHAREPOINT,
iconName: SOURCE_NAMES.SHAREPOINT,
categories: [],
serviceType: 'external',
configuration: {
isPublicKey: false,
@ -40,16 +41,12 @@ export const staticExternalSourceData: SourceDataItem = {
platinumPrivateContext: [FeatureIds.Private, FeatureIds.SyncFrequency, FeatureIds.SyncedItems],
},
accountContextOnly: false,
internalConnectorAvailable: true,
externalConnectorAvailable: false,
customConnectorAvailable: false,
isBeta: true,
};
export const staticSourceData: SourceDataItem[] = [
{
name: SOURCE_NAMES.BOX,
iconName: SOURCE_NAMES.BOX,
serviceType: 'box',
configuration: {
isPublicKey: false,
@ -74,11 +71,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.CONFLUENCE,
iconName: SOURCE_NAMES.CONFLUENCE,
serviceType: 'confluence_cloud',
configuration: {
isPublicKey: false,
@ -108,11 +103,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.CONFLUENCE_SERVER,
iconName: SOURCE_NAMES.CONFLUENCE_SERVER,
serviceType: 'confluence_server',
configuration: {
isPublicKey: true,
@ -140,11 +133,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.DROPBOX,
iconName: SOURCE_NAMES.DROPBOX,
serviceType: 'dropbox',
configuration: {
isPublicKey: false,
@ -169,11 +160,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.GITHUB,
iconName: SOURCE_NAMES.GITHUB,
serviceType: 'github',
configuration: {
isPublicKey: false,
@ -205,11 +194,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.GITHUB_ENTERPRISE,
iconName: SOURCE_NAMES.GITHUB_ENTERPRISE,
serviceType: 'github_enterprise_server',
configuration: {
isPublicKey: false,
@ -247,11 +234,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.GMAIL,
iconName: SOURCE_NAMES.GMAIL,
serviceType: 'gmail',
configuration: {
isPublicKey: false,
@ -265,11 +250,9 @@ export const staticSourceData: SourceDataItem[] = [
platinumPrivateContext: [FeatureIds.Remote, FeatureIds.Private, FeatureIds.SearchableContent],
},
accountContextOnly: true,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.GOOGLE_DRIVE,
iconName: SOURCE_NAMES.GOOGLE_DRIVE,
serviceType: 'google_drive',
configuration: {
isPublicKey: false,
@ -298,11 +281,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.JIRA,
iconName: SOURCE_NAMES.JIRA,
serviceType: 'jira_cloud',
configuration: {
isPublicKey: false,
@ -334,11 +315,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.JIRA_SERVER,
iconName: SOURCE_NAMES.JIRA_SERVER,
serviceType: 'jira_server',
configuration: {
isPublicKey: true,
@ -369,13 +348,12 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.NETWORK_DRVE,
iconName: SOURCE_NAMES.NETWORK_DRVE,
categories: [SOURCE_CATEGORIES.STORAGE],
serviceType: 'network_drive', // this doesn't exist on the BE
serviceType: 'custom',
baseServiceType: 'network_drive',
configuration: {
isPublicKey: false,
hasOauthRedirect: false,
@ -385,12 +363,9 @@ export const staticSourceData: SourceDataItem[] = [
githubRepository: 'elastic/enterprise-search-network-drive-connector',
},
accountContextOnly: false,
internalConnectorAvailable: false,
customConnectorAvailable: true,
},
{
name: SOURCE_NAMES.ONEDRIVE,
iconName: SOURCE_NAMES.ONEDRIVE,
serviceType: 'one_drive',
configuration: {
isPublicKey: false,
@ -415,17 +390,16 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.OUTLOOK,
iconName: SOURCE_NAMES.OUTLOOK,
categories: [
SOURCE_CATEGORIES.COMMUNICATIONS,
SOURCE_CATEGORIES.PRODUCTIVITY,
SOURCE_CATEGORIES.MICROSOFT,
],
serviceType: 'outlook', // this doesn't exist on the BE
serviceType: 'custom',
baseServiceType: 'outlook',
configuration: {
isPublicKey: false,
hasOauthRedirect: false,
@ -435,12 +409,9 @@ export const staticSourceData: SourceDataItem[] = [
githubRepository: 'elastic/enterprise-search-outlook-connector',
},
accountContextOnly: false,
internalConnectorAvailable: false,
customConnectorAvailable: true,
},
{
name: SOURCE_NAMES.SALESFORCE,
iconName: SOURCE_NAMES.SALESFORCE,
serviceType: 'salesforce',
configuration: {
isPublicKey: false,
@ -472,11 +443,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.SALESFORCE_SANDBOX,
iconName: SOURCE_NAMES.SALESFORCE_SANDBOX,
serviceType: 'salesforce_sandbox',
configuration: {
isPublicKey: false,
@ -508,11 +477,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.SERVICENOW,
iconName: SOURCE_NAMES.SERVICENOW,
serviceType: 'service_now',
configuration: {
isPublicKey: false,
@ -541,11 +508,9 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.SHAREPOINT,
iconName: SOURCE_NAMES.SHAREPOINT,
serviceType: 'share_point',
configuration: {
isPublicKey: false,
@ -570,13 +535,39 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
externalConnectorAvailable: true,
},
staticExternalSourceData,
{
name: SOURCE_NAMES.SHAREPOINT,
categories: [],
serviceType: 'external',
baseServiceType: 'share_point',
configuration: {
isPublicKey: false,
hasOauthRedirect: true,
needsBaseUrl: false,
documentationUrl: docLinks.workplaceSearchExternalSharePointOnline,
applicationPortalUrl: 'https://portal.azure.com/',
},
objTypes: [SOURCE_OBJ_TYPES.ALL_STORED_FILES],
features: {
basicOrgContext: [
FeatureIds.SyncFrequency,
FeatureIds.SyncedItems,
FeatureIds.GlobalAccessPermissions,
],
basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions],
platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems],
platinumPrivateContext: [
FeatureIds.Private,
FeatureIds.SyncFrequency,
FeatureIds.SyncedItems,
],
},
accountContextOnly: false,
isBeta: true,
},
{
name: SOURCE_NAMES.SHAREPOINT_SERVER,
iconName: SOURCE_NAMES.SHAREPOINT_SERVER,
categories: [
SOURCE_CATEGORIES.FILE_SHARING,
SOURCE_CATEGORIES.STORAGE,
@ -584,7 +575,8 @@ export const staticSourceData: SourceDataItem[] = [
SOURCE_CATEGORIES.MICROSOFT,
SOURCE_CATEGORIES.OFFICE_365,
],
serviceType: 'share_point_server', // this doesn't exist on the BE
serviceType: 'custom',
baseServiceType: 'share_point_server',
configuration: {
isPublicKey: false,
hasOauthRedirect: false,
@ -594,12 +586,9 @@ export const staticSourceData: SourceDataItem[] = [
githubRepository: 'elastic/enterprise-search-sharepoint-server-connector',
},
accountContextOnly: false,
internalConnectorAvailable: false,
customConnectorAvailable: true,
},
{
name: SOURCE_NAMES.SLACK,
iconName: SOURCE_NAMES.SLACK,
serviceType: 'slack',
configuration: {
isPublicKey: false,
@ -617,17 +606,16 @@ export const staticSourceData: SourceDataItem[] = [
platinumPrivateContext: [FeatureIds.Remote, FeatureIds.Private, FeatureIds.SearchableContent],
},
accountContextOnly: true,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.TEAMS,
iconName: SOURCE_NAMES.TEAMS,
categories: [
SOURCE_CATEGORIES.COMMUNICATIONS,
SOURCE_CATEGORIES.PRODUCTIVITY,
SOURCE_CATEGORIES.MICROSOFT,
],
serviceType: 'teams', // this doesn't exist on the BE
serviceType: 'custom',
baseServiceType: 'teams',
configuration: {
isPublicKey: false,
hasOauthRedirect: false,
@ -637,12 +625,9 @@ export const staticSourceData: SourceDataItem[] = [
githubRepository: 'elastic/enterprise-search-teams-connector',
},
accountContextOnly: false,
internalConnectorAvailable: false,
customConnectorAvailable: true,
},
{
name: SOURCE_NAMES.ZENDESK,
iconName: SOURCE_NAMES.ZENDESK,
serviceType: 'zendesk',
configuration: {
isPublicKey: false,
@ -667,13 +652,12 @@ export const staticSourceData: SourceDataItem[] = [
],
},
accountContextOnly: false,
internalConnectorAvailable: true,
},
{
name: SOURCE_NAMES.ZOOM,
iconName: SOURCE_NAMES.ZOOM,
categories: [SOURCE_CATEGORIES.COMMUNICATIONS, SOURCE_CATEGORIES.PRODUCTIVITY],
serviceType: 'zoom', // this doesn't exist on the BE
serviceType: 'custom',
baseServiceType: 'zoom',
configuration: {
isPublicKey: false,
hasOauthRedirect: false,
@ -683,14 +667,12 @@ export const staticSourceData: SourceDataItem[] = [
githubRepository: 'elastic/enterprise-search-zoom-connector',
},
accountContextOnly: false,
internalConnectorAvailable: false,
customConnectorAvailable: true,
},
staticGenericExternalSourceData,
];
export const staticCustomSourceData: SourceDataItem = {
name: SOURCE_NAMES.CUSTOM,
iconName: SOURCE_NAMES.CUSTOM,
categories: ['API', 'Custom'],
serviceType: 'custom',
configuration: {
@ -701,12 +683,26 @@ export const staticCustomSourceData: SourceDataItem = {
applicationPortalUrl: '',
},
accountContextOnly: false,
customConnectorAvailable: true,
};
export const getSourceData = (serviceType: string): SourceDataItem => {
return (
staticSourceData.find((staticSource) => staticSource.serviceType === serviceType) ||
staticCustomSourceData
export const getSourceData = (
serviceType: string,
baseServiceType?: string
): SourceDataItem | undefined => {
if (serviceType === 'custom' && typeof baseServiceType === 'undefined') {
return staticCustomSourceData;
}
return staticSourceData.find(
(staticSource) =>
staticSource.serviceType === serviceType && staticSource.baseServiceType === baseServiceType
);
};
export const hasExternalConnectorOption = (serviceType: string): boolean =>
!!getSourceData('external', serviceType);
export const hasCustomConnectorOption = (serviceType: string): boolean =>
!!getSourceData('custom', serviceType);
export const hasMultipleConnectorOptions = (serviceType: string): boolean =>
hasExternalConnectorOption(serviceType) || hasCustomConnectorOption(serviceType);

View file

@ -23,7 +23,12 @@ import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers';
import { AppLogic } from '../../app_logic';
import { staticSourceData } from './source_data';
import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic';
import {
SourcesLogic,
fetchSourceStatuses,
POLLING_INTERVAL,
mergeServerAndStaticData,
} from './sources_logic';
describe('SourcesLogic', () => {
const { http } = mockHttpValues;
@ -37,8 +42,14 @@ describe('SourcesLogic', () => {
const defaultValues = {
contentSources: [],
privateContentSources: [],
sourceData: staticSourceData.map((data) => ({ ...data, connected: false })),
availableSources: staticSourceData.map((data) => ({ ...data, connected: false })),
sourceData: mergeServerAndStaticData([], staticSourceData, []).map((data) => ({
...data,
connected: false,
})),
availableSources: mergeServerAndStaticData([], staticSourceData, []).map((data) => ({
...data,
connected: false,
})),
configuredSources: [],
serviceTypes: [],
permissionsModal: null,
@ -322,7 +333,7 @@ describe('SourcesLogic', () => {
it('availableSources & configuredSources have correct length', () => {
SourcesLogic.actions.onInitializeSources(serverResponse);
expect(SourcesLogic.values.availableSources).toHaveLength(18);
expect(SourcesLogic.values.availableSources).toHaveLength(19);
expect(SourcesLogic.values.configuredSources).toHaveLength(5);
});
it('externalConfigured is set to true if external is configured', () => {

View file

@ -51,7 +51,7 @@ export interface IPermissionsModalProps {
additionalConfiguration: boolean;
}
type CombinedDataItem = SourceDataItem & { connected: boolean };
type CombinedDataItem = SourceDataItem & Partial<Connector> & { connected: boolean };
export interface ISourcesValues {
contentSources: ContentSourceDetails[];
@ -145,17 +145,17 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
selectors: ({ selectors }) => ({
availableSources: [
() => [selectors.sourceData],
(sourceData: SourceDataItem[]) =>
(sourceData: CombinedDataItem[]) =>
sortByName(sourceData.filter(({ configured }) => !configured)),
],
configuredSources: [
() => [selectors.sourceData],
(sourceData: SourceDataItem[]) =>
(sourceData: CombinedDataItem[]) =>
sortByName(sourceData.filter(({ configured }) => configured)),
],
externalConfigured: [
() => [selectors.configuredSources],
(configuredSources: SourceDataItem[]) =>
(configuredSources: CombinedDataItem[]) =>
!!configuredSources.find((item) => item.serviceType === 'external'),
],
sourceData: [
@ -312,9 +312,12 @@ export const mergeServerAndStaticData = (
contentSources: ContentSourceDetails[]
): CombinedDataItem[] => {
const unsortedData = staticData.map((staticItem) => {
const serverItem = serverData.find(({ serviceType }) => serviceType === staticItem.serviceType);
const serverItem = staticItem.baseServiceType
? undefined // static items with base service types will never have matching external connectors, BE doesn't pass us a baseServiceType
: serverData.find(({ serviceType }) => serviceType === staticItem.serviceType);
const connectedSource = contentSources.find(
({ serviceType }) => serviceType === staticItem.serviceType
({ baseServiceType, serviceType }) =>
serviceType === staticItem.serviceType && baseServiceType === staticItem.baseServiceType
);
return {
...staticItem,

View file

@ -10,11 +10,11 @@ import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import { shallow } from 'enzyme';
import { ADD_SOURCE_PATH, PRIVATE_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes';
import { ADD_SOURCE_PATH, PRIVATE_SOURCES_PATH, getSourcesPath } from '../../routes';
import { SourcesRouter } from './sources_router';
@ -34,19 +34,13 @@ describe('SourcesRouter', () => {
});
it('renders sources routes', () => {
const TOTAL_ROUTES = 103;
const wrapper = shallow(<SourcesRouter />);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(TOTAL_ROUTES);
});
it('redirects when nonplatinum license and accountOnly context', () => {
setMockValues({ ...mockValues, hasPlatinumLicense: false });
const wrapper = shallow(<SourcesRouter />);
expect(wrapper.find(Redirect).last().prop('from')).toEqual(ADD_SOURCE_PATH);
expect(wrapper.find(Redirect).last().prop('to')).toEqual(SOURCES_PATH);
expect(wrapper.find('[data-test-subj="ConnectorIntroRoute"]')).toHaveLength(1);
expect(wrapper.find('[data-test-subj="ConnectorChoiceRoute"]')).toHaveLength(1);
expect(wrapper.find('[data-test-subj="ExternalConnectorConfigRoute"]')).toHaveLength(1);
expect(wrapper.find('[data-test-subj="AddCustomSourceRoute"]')).toHaveLength(2);
expect(wrapper.find('[data-test-subj="AddSourceRoute"]')).toHaveLength(1);
});
it('redirects when cannot create sources', () => {

View file

@ -11,7 +11,6 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { Location } from 'history';
import { useActions, useValues } from 'kea';
import { LicensingLogic } from '../../../shared/licensing';
import { AppLogic } from '../../app_logic';
import {
GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE,
@ -24,17 +23,15 @@ import {
SOURCES_PATH,
getSourcesPath,
getAddPath,
ADD_CUSTOM_PATH,
} from '../../routes';
import { hasMultipleConnectorOptions } from '../../utils';
import { AddSource, AddSourceList, GitHubViaApp } from './components/add_source';
import { AddCustomSource } from './components/add_source/add_custom_source';
import { ExternalConnectorConfig } from './components/add_source/add_external_connector';
import { ConfigurationChoice } from './components/add_source/configuration_choice';
import { AddSourceChoice } from './components/add_source/add_source_choice';
import { AddSourceIntro } from './components/add_source/add_source_intro';
import { OrganizationSources } from './organization_sources';
import { PrivateSources } from './private_sources';
import { staticCustomSourceData, staticSourceData as sources } from './source_data';
import { SourceRouter } from './source_router';
import { SourcesLogic } from './sources_logic';
@ -42,7 +39,6 @@ import './sources.scss';
export const SourcesRouter: React.FC = () => {
const { pathname } = useLocation() as Location;
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { resetSourcesState } = useActions(SourcesLogic);
const {
account: { canCreatePrivateSources },
@ -82,119 +78,51 @@ export const SourcesRouter: React.FC = () => {
<Route exact path={getAddPath(GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE)}>
<GitHubViaApp isGithubEnterpriseServer />
</Route>
{sources.map((sourceData, i) => {
const { serviceType, externalConnectorAvailable, internalConnectorAvailable } = sourceData;
const path = `${getSourcesPath(getAddPath(serviceType), isOrganization)}`;
const defaultOption = internalConnectorAvailable
? 'internal'
: externalConnectorAvailable
? 'external'
: 'custom';
const showChoice = defaultOption !== 'internal' && hasMultipleConnectorOptions(sourceData);
return (
<Route key={i} exact path={path}>
{showChoice ? (
<ConfigurationChoice sourceData={sourceData} />
) : (
<Redirect exact from={path} to={`${path}/${defaultOption}`} />
)}
</Route>
);
})}
<Route exact path={getSourcesPath(ADD_CUSTOM_PATH, isOrganization)}>
<AddCustomSource sourceData={staticCustomSourceData} />
<Route
exact
path={`${getSourcesPath(getAddPath(':serviceType'), isOrganization)}/intro`}
data-test-subj="ConnectorIntroRoute"
>
<AddSourceIntro />
</Route>
<Route
exact
path={`${getSourcesPath(getAddPath(':serviceType'), isOrganization)}/choice`}
data-test-subj="ConnectorChoiceRoute"
>
<AddSourceChoice />
</Route>
<Route
exact
path={`${getSourcesPath(
getAddPath('external', ':baseServiceType'),
isOrganization
)}/connector_registration`}
data-test-subj="ExternalConnectorConfigRoute"
>
<ExternalConnectorConfig />
</Route>
<Route
exact
path={`${getSourcesPath(getAddPath('custom'), isOrganization)}/`}
data-test-subj="AddCustomSourceRoute"
>
<AddCustomSource />
</Route>
<Route
exact
path={`${getSourcesPath(getAddPath('custom', ':baseServiceType'), isOrganization)}/`}
data-test-subj="AddCustomSourceRoute"
>
<AddCustomSource />
</Route>
<Route
exact
path={`${getSourcesPath(getAddPath(':serviceType'), isOrganization)}/:initialStep?`}
data-test-subj="AddSourceRoute"
>
<AddSource />
</Route>
{sources
.filter((sourceData) => sourceData.internalConnectorAvailable)
.map((sourceData, i) => {
const { serviceType, accountContextOnly } = sourceData;
return (
<Route
key={i}
exact
path={`${getSourcesPath(getAddPath(serviceType), isOrganization)}/internal`}
>
{!hasPlatinumLicense && accountContextOnly ? (
<Redirect exact from={ADD_SOURCE_PATH} to={SOURCES_PATH} />
) : (
<AddSource sourceData={sourceData} />
)}
</Route>
);
})}
{sources
.filter((sourceData) => sourceData.externalConnectorAvailable)
.map((sourceData, i) => {
const { serviceType, accountContextOnly } = sourceData;
return (
<Route
key={i}
exact
path={`${getSourcesPath(getAddPath(serviceType), isOrganization)}/external`}
>
{!hasPlatinumLicense && accountContextOnly ? (
<Redirect exact from={ADD_SOURCE_PATH} to={SOURCES_PATH} />
) : (
<ExternalConnectorConfig sourceData={sourceData} />
)}
</Route>
);
})}
{sources
.filter((sourceData) => sourceData.customConnectorAvailable)
.map((sourceData, i) => {
const { serviceType, accountContextOnly } = sourceData;
return (
<Route
key={i}
exact
path={`${getSourcesPath(getAddPath(serviceType), isOrganization)}/custom`}
>
{!hasPlatinumLicense && accountContextOnly ? (
<Redirect exact from={ADD_SOURCE_PATH} to={SOURCES_PATH} />
) : (
<AddCustomSource sourceData={sourceData} initialValue={sourceData.name} />
)}
</Route>
);
})}
{sources.map((sourceData, i) => (
<Route
key={i}
exact
path={`${getSourcesPath(getAddPath(sourceData.serviceType), isOrganization)}/connect`}
>
<AddSource connect sourceData={sourceData} />
</Route>
))}
{sources.map((sourceData, i) => (
<Route
key={i}
exact
path={`${getSourcesPath(
getAddPath(sourceData.serviceType),
isOrganization
)}/reauthenticate`}
>
<AddSource reAuthenticate sourceData={sourceData} />
</Route>
))}
{sources.map((sourceData, i) => {
if (sourceData.configuration.needsConfiguration)
return (
<Route
key={i}
exact
path={`${getSourcesPath(
getAddPath(sourceData.serviceType),
isOrganization
)}/configure`}
>
<AddSource configure sourceData={sourceData} />
</Route>
);
})}
{canCreatePrivateSources ? (
<Route exact path={getSourcesPath(ADD_SOURCE_PATH, false)}>
<AddSourceList />

View file

@ -8,6 +8,7 @@
import '../../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../__mocks__/react_router';
import { sourceConfigData } from '../../../__mocks__/content_sources.mock';
import React from 'react';
@ -18,8 +19,6 @@ import { EuiCallOut, EuiConfirmModal } from '@elastic/eui';
import { SaveConfig } from '../../content_sources/components/add_source/save_config';
import { staticSourceData } from '../../content_sources/source_data';
import { SourceConfig } from './source_config';
describe('SourceConfig', () => {
@ -30,10 +29,11 @@ describe('SourceConfig', () => {
beforeEach(() => {
setMockValues({ sourceConfigData, dataLoading: false });
setMockActions({ deleteSourceConfig, getSourceConfigData, saveSourceConfig });
mockUseParams.mockReturnValue({ serviceType: 'share_point' });
});
it('renders', () => {
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
const wrapper = shallow(<SourceConfig />);
const saveConfig = wrapper.find(SaveConfig);
// Trigger modal visibility
@ -43,15 +43,23 @@ describe('SourceConfig', () => {
expect(wrapper.find(EuiCallOut)).toHaveLength(0);
});
it('returns null if there is no matching source data for the service type', () => {
mockUseParams.mockReturnValue({ serviceType: 'doesnt_exist' });
const wrapper = shallow(<SourceConfig />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('renders a breadcrumb fallback while data is loading', () => {
setMockValues({ dataLoading: true, sourceConfigData: {} });
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
const wrapper = shallow(<SourceConfig />);
expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']);
});
it('handles delete click', () => {
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
const wrapper = shallow(<SourceConfig />);
const saveConfig = wrapper.find(SaveConfig);
// Trigger modal visibility
@ -63,7 +71,7 @@ describe('SourceConfig', () => {
});
it('saves source config', () => {
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
const wrapper = shallow(<SourceConfig />);
const saveConfig = wrapper.find(SaveConfig);
// Trigger modal visibility
@ -75,7 +83,7 @@ describe('SourceConfig', () => {
});
it('cancels and closes modal', () => {
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
const wrapper = shallow(<SourceConfig />);
const saveConfig = wrapper.find(SaveConfig);
// Trigger modal visibility
@ -87,9 +95,8 @@ describe('SourceConfig', () => {
});
it('shows feedback link for external sources', () => {
const wrapper = shallow(
<SourceConfig sourceData={{ ...staticSourceData[1], serviceType: 'external' }} />
);
mockUseParams.mockReturnValue({ serviceType: 'external' });
const wrapper = shallow(<SourceConfig />);
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
});

View file

@ -7,6 +7,8 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
import {
@ -21,29 +23,34 @@ import { i18n } from '@kbn/i18n';
import { WorkplaceSearchPageTemplate } from '../../../components/layout';
import { NAV, REMOVE_BUTTON, CANCEL_BUTTON } from '../../../constants';
import { SourceDataItem } from '../../../types';
import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header';
import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic';
import { SaveConfig } from '../../content_sources/components/add_source/save_config';
import { getSourceData } from '../../content_sources/source_data';
import { SettingsLogic } from '../settings_logic';
interface SourceConfigProps {
sourceData: SourceDataItem;
}
export const SourceConfig: React.FC<SourceConfigProps> = ({ sourceData }) => {
export const SourceConfig: React.FC = () => {
const { serviceType } = useParams<{ serviceType: string }>();
const [confirmModalVisible, setConfirmModalVisibility] = useState(false);
const { configuration, serviceType } = sourceData;
const addSourceLogic = AddSourceLogic({ serviceType });
const { deleteSourceConfig } = useActions(SettingsLogic);
const { saveSourceConfig, getSourceConfigData } = useActions(AddSourceLogic);
const { saveSourceConfig, getSourceConfigData, resetSourceState } = useActions(addSourceLogic);
const {
sourceConfigData: { name, categories },
dataLoading,
} = useValues(AddSourceLogic);
} = useValues(addSourceLogic);
const sourceData = getSourceData(serviceType);
useEffect(() => {
getSourceConfigData(serviceType);
}, []);
getSourceConfigData();
return resetSourceState;
}, [serviceType]);
if (!sourceData) {
return null;
}
const { configuration } = sourceData;
const hideConfirmModal = () => setConfirmModalVisibility(false);
const showConfirmModal = () => setConfirmModalVisibility(true);

View file

@ -10,12 +10,10 @@ import '../../../__mocks__/shallow_useeffect.mock';
import { setMockActions } from '../../../__mocks__/kea_logic';
import React from 'react';
import { Route, Redirect, Switch } from 'react-router-dom';
import { Redirect, Switch } from 'react-router-dom';
import { shallow } from 'enzyme';
import { staticSourceData } from '../content_sources/source_data';
import { Connectors } from './components/connectors';
import { Customize } from './components/customize';
import { OauthApplication } from './components/oauth_application';
@ -24,9 +22,6 @@ import { SettingsRouter } from './settings_router';
describe('SettingsRouter', () => {
const initializeSettings = jest.fn();
const NUM_SOURCES = staticSourceData.length;
// Should be 4 routes other than the sources listed: Connectors, Customize, & OauthApplication, & a redirect
const NUM_ROUTES = NUM_SOURCES + 4;
beforeEach(() => {
setMockActions({ initializeSettings });
@ -36,11 +31,10 @@ describe('SettingsRouter', () => {
const wrapper = shallow(<SettingsRouter />);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(NUM_ROUTES);
expect(wrapper.find(Redirect)).toHaveLength(1);
expect(wrapper.find(Connectors)).toHaveLength(1);
expect(wrapper.find(Customize)).toHaveLength(1);
expect(wrapper.find(OauthApplication)).toHaveLength(1);
expect(wrapper.find(SourceConfig)).toHaveLength(NUM_SOURCES);
expect(wrapper.find(SourceConfig)).toHaveLength(1);
});
});

View file

@ -16,7 +16,6 @@ import {
ORG_SETTINGS_OAUTH_APPLICATION_PATH,
getEditPath,
} from '../../routes';
import { staticSourceData } from '../content_sources/source_data';
import { Connectors } from './components/connectors';
import { Customize } from './components/customize';
@ -42,11 +41,9 @@ export const SettingsRouter: React.FC = () => {
<Route exact path={ORG_SETTINGS_OAUTH_APPLICATION_PATH}>
<OauthApplication />
</Route>
{staticSourceData.map((sourceData, i) => (
<Route key={i} exact path={getEditPath(sourceData.serviceType)}>
<SourceConfig sourceData={sourceData} />
</Route>
))}
<Route exact path={getEditPath(':serviceType')}>
<SourceConfig />
</Route>
<Route>
<Redirect to={ORG_SETTINGS_CUSTOMIZE_PATH} />
</Route>