mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Workplace Search] Enables external SharePoint Online and custom SharePoint Server connectors (#126172)
This adds the ability to add and manage an External SharePoint Online connector and a Custom SharePoint Server connector via Kibana. Co-authored-by: Byron Hulcher <byron.hulcher@elastic.co>
This commit is contained in:
parent
0bb021199e
commit
5023a03313
45 changed files with 1984 additions and 543 deletions
|
@ -8,6 +8,7 @@
|
|||
import { groups } from './groups.mock';
|
||||
|
||||
import { IndexingRule } from '../types';
|
||||
import { SourceConfigData } from '../views/content_sources/components/add_source/add_source_logic';
|
||||
import { staticSourceData } from '../views/content_sources/source_data';
|
||||
import { mergeServerAndStaticData } from '../views/content_sources/sources_logic';
|
||||
|
||||
|
@ -339,23 +340,23 @@ export const mergedConfiguredSources = mergeServerAndStaticData(
|
|||
contentSources
|
||||
);
|
||||
|
||||
export const sourceConfigData = {
|
||||
export const sourceConfigData: SourceConfigData = {
|
||||
serviceType: 'confluence_cloud',
|
||||
name: 'Confluence',
|
||||
configured: true,
|
||||
needsPermissions: true,
|
||||
accountContextOnly: false,
|
||||
supportedByLicense: true,
|
||||
privateSourcesEnabled: false,
|
||||
categories: ['wiki', 'atlassian', 'intranet'],
|
||||
configuredFields: {
|
||||
isOauth1: false,
|
||||
clientId: 'CyztADsSECRETCSAUCEh1a',
|
||||
clientSecret: 'GSjJxqSECRETCSAUCEksHk',
|
||||
baseUrl: 'https://mine.atlassian.net',
|
||||
privateKey: '-----BEGIN PRIVATE KEY-----\nkeykeykeykey==\n-----END PRIVATE KEY-----\n',
|
||||
publicKey: '-----BEGIN PUBLIC KEY-----\nkeykeykeykey\n-----END PUBLIC KEY-----\n',
|
||||
consumerKey: 'elastic_enterprise_search_123',
|
||||
apiKey: 'asdf1234',
|
||||
url: 'https://www.elastic.co',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import oneDrive from './onedrive.svg';
|
|||
import salesforce from './salesforce.svg';
|
||||
import serviceNow from './servicenow.svg';
|
||||
import sharePoint from './sharepoint.svg';
|
||||
import sharePointServer from './sharepoint_server.svg';
|
||||
import slack from './slack.svg';
|
||||
import zendesk from './zendesk.svg';
|
||||
|
||||
|
@ -29,6 +30,8 @@ export const images = {
|
|||
confluenceServer: confluence,
|
||||
custom,
|
||||
dropbox,
|
||||
// TODO: For now external sources are all SharePoint. When this is no longer the case, this needs to be dynamic.
|
||||
external: sharePoint,
|
||||
github,
|
||||
githubEnterpriseServer: github,
|
||||
githubViaApp: github,
|
||||
|
@ -44,6 +47,7 @@ export const images = {
|
|||
salesforceSandbox: salesforce,
|
||||
serviceNow,
|
||||
sharePoint,
|
||||
sharePointServer,
|
||||
slack,
|
||||
zendesk,
|
||||
} as { [key: string]: string };
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" height="100" viewBox="0 0 102 100" width="102" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="9.88794" x2="43.4615" y1="20.9988" y2="78.9304"><stop offset="0" stop-color="#058f92"/><stop offset=".5" stop-color="#038489"/><stop offset="1" stop-color="#026d71"/></linearGradient><path d="m52.1629 57.022c15.4125 0 27.9068-12.5409 27.9068-28.011 0-15.47-12.4943-28.011-27.9068-28.011s-27.9068 12.541-27.9068 28.011c0 15.4701 12.4943 28.011 27.9068 28.011z" fill="#036c70"/><path d="m75.4188 80.3617c14.1281 0 25.5812-11.4959 25.5812-25.6768 0-14.1808-11.4531-25.6767-25.5812-25.6767s-25.5812 11.4959-25.5812 25.6767c0 14.1809 11.4531 25.6768 25.5812 25.6768z" fill="#1a9ba1"/><path d="m55.6508 99.0371c10.9172 0 19.7673-8.8832 19.7673-19.8411 0-10.958-8.8501-19.8411-19.7673-19.8411-10.9171 0-19.7673 8.8831-19.7673 19.8411 0 10.9579 8.8502 19.8411 19.7673 19.8411z" fill="#37c6d0"/><g fill="#000"><path d="m56.8143 26.2805v49.8128c-.0116 1.7309-1.0566 3.2859-2.6512 3.9449-.5077.2156-1.0534.3267-1.6046.3268h-16.6511c-.0232-.3968-.0232-.7703-.0232-1.1671-.0078-.3896.0077-.7794.0465-1.1672.4256-7.4615 5.0052-14.0449 11.8372-17.0166v-4.3417c-15.2049-2.4185-25.5776-16.751-23.1681-32.0125.0167-.1057.034-.2114.0519-.3169.1158-.7872.2789-1.5667.4884-2.3342h27.4184c2.3467.009 4.2468 1.9162 4.2558 4.2717z" opacity=".1"/><path d="m50.2321 24.342h-25.5812c-2.5842 15.2341 7.6245 29.6865 22.8019 32.2803.4596.0785.921.1455 1.3839.2008-7.2092 3.4314-12.4627 13.1769-12.9092 21.2067-.0387.3877-.0542.7775-.0465 1.1671 0 .3968 0 .7703.0232 1.1671.042.7847.1431 1.565.3023 2.3343h14.0232c1.7245-.0116 3.2737-1.0605 3.9302-2.6611.2148-.5096.3255-1.0573.3256-1.6106v-49.8129c-.0088-2.3545-1.9076-4.2614-4.2534-4.2717z" opacity=".2"/><path d="m50.2332 24.342h-25.5812c-2.5837 15.2355 7.6266 29.6885 22.8054 32.2819.3105.053.6219.1008.9339.1432-6.9766 3.6788-12.0231 13.401-12.4603 21.2627h14.3022c2.343-.0178 4.2381-1.9199 4.2558-4.2717v-45.1444c-.009-2.3554-1.9091-4.2627-4.2558-4.2717z" opacity=".2"/><path d="m47.9068 24.342h-23.2556c-2.4396 14.3825 6.539 28.2329 20.6278 31.8205-5.3347 6.1179-8.6068 13.7714-9.3488 21.8673h11.9767c2.3467-.009 4.2468-1.9162 4.2558-4.2717v-45.1444c-.0013-2.3586-1.906-4.2703-4.2559-4.2717z" opacity=".2"/></g><path d="m5.26276 24.342h42.63684c2.3543 0 4.2628 1.9157 4.2628 4.2787v42.7961c0 2.3631-1.9085 4.2787-4.2628 4.2787h-42.63684c-2.35426 0-4.26276-1.9156-4.26276-4.2787v-42.7961c0-2.363 1.9085-4.2787 4.26276-4.2787z" fill="url(#a)"/><path d="m20.0404 49.4935c-.999-.6651-1.8334-1.5506-2.4395-2.5887-.5873-1.0853-.8801-2.3065-.8489-3.5411-.0523-1.6716.5097-3.3042 1.579-4.5868 1.1237-1.2842 2.5762-2.2351 4.2-2.7498 1.8507-.6114 3.789-.9126 5.7372-.8916 2.5619-.094 5.1203.2656 7.5581 1.0621v5.3688c-1.0592-.6441-2.2129-1.1168-3.4186-1.4006-1.3083-.322-2.6507-.4835-3.9977-.4809-1.4204-.0523-2.8318.2476-4.1093.8731-.9862.4269-1.6258 1.4009-1.6278 2.4789-.004.654.2463 1.2838.6976 1.7554.5332.556 1.1638 1.0087 1.8605 1.3352.7752.3875 1.938.9026 3.4884 1.5453.1707.0542.3371.1213.4976.2008 1.5259.5985 2.9979 1.3272 4.4 2.1778 1.0618.6571 1.9529 1.558 2.6 2.6284.6635 1.2134.985 2.5852.9302 3.9682.0758 1.7162-.4473 3.4052-1.479 4.7759-1.0284 1.2601-2.4032 2.1888-3.9535 2.6704-1.8235.5736-3.7265.851-5.6372.8216-1.7143.0078-3.426-.1328-5.1162-.4202-1.4272-.2344-2.8199-.6452-4.1465-1.2231v-5.6606c1.268.909 2.6847 1.5884 4.186 2.0075 1.4962.468 3.0516.718 4.6186.7423 1.4503.0922 2.898-.2162 4.186-.8917.9023-.511 1.451-1.479 1.4279-2.5186.0061-.7234-.279-1.4186-.7906-1.9281-.6363-.6269-1.3729-1.1422-2.1791-1.5243-.9302-.4669-2.3-1.0823-4.1093-1.8464-1.4393-.5808-2.8174-1.3041-4.1139-2.1592z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -192,6 +192,10 @@ export const SOURCE_NAMES = {
|
|||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint',
|
||||
{ defaultMessage: 'SharePoint Online' }
|
||||
),
|
||||
SHAREPOINT_SERVER: i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePointServer',
|
||||
{ defaultMessage: 'SharePoint Server' }
|
||||
),
|
||||
SLACK: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack', {
|
||||
defaultMessage: 'Slack',
|
||||
}),
|
||||
|
@ -357,6 +361,7 @@ export const GITHUB_VIA_APP_SERVICE_TYPE = 'github_via_app';
|
|||
export const GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE = 'github_enterprise_server_via_app';
|
||||
|
||||
export const CUSTOM_SERVICE_TYPE = 'custom';
|
||||
export const EXTERNAL_SERVICE_TYPE = 'external';
|
||||
|
||||
export const WORKPLACE_SEARCH_URL_PREFIX = '/app/enterprise_search/workplace_search';
|
||||
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
GITHUB_VIA_APP_SERVICE_TYPE,
|
||||
GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE,
|
||||
} from './constants';
|
||||
|
||||
export const SETUP_GUIDE_PATH = '/setup_guide';
|
||||
|
||||
export const NOT_FOUND_PATH = '/404';
|
||||
|
@ -40,25 +35,7 @@ export const PRIVATE_SOURCES_PATH = `${PERSONAL_PATH}${SOURCES_PATH}`;
|
|||
|
||||
export const SOURCE_ADDED_PATH = `${SOURCES_PATH}/added`;
|
||||
export const ADD_SOURCE_PATH = `${SOURCES_PATH}/add`;
|
||||
export const ADD_BOX_PATH = `${SOURCES_PATH}/add/box`;
|
||||
export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence_cloud`;
|
||||
export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence_server`;
|
||||
export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`;
|
||||
export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github_enterprise_server`;
|
||||
export const ADD_GITHUB_PATH = `${SOURCES_PATH}/add/github`;
|
||||
export const ADD_GITHUB_VIA_APP_PATH = `${SOURCES_PATH}/add/${GITHUB_VIA_APP_SERVICE_TYPE}`;
|
||||
export const ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH = `${SOURCES_PATH}/add/${GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE}`;
|
||||
export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`;
|
||||
export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`;
|
||||
export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`;
|
||||
export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira_server`;
|
||||
export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/one_drive`;
|
||||
export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`;
|
||||
export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce_sandbox`;
|
||||
export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`;
|
||||
export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/share_point`;
|
||||
export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`;
|
||||
export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`;
|
||||
export const ADD_EXTERNAL_PATH = `${SOURCES_PATH}/add/external`;
|
||||
export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`;
|
||||
|
||||
export const PERSONAL_SETTINGS_PATH = `${PERSONAL_PATH}/settings`;
|
||||
|
@ -83,24 +60,6 @@ export const ORG_SETTINGS_PATH = '/settings';
|
|||
export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`;
|
||||
export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`;
|
||||
export const ORG_SETTINGS_OAUTH_APPLICATION_PATH = `${ORG_SETTINGS_PATH}/oauth`;
|
||||
export const EDIT_BOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/box/edit`;
|
||||
export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence_cloud/edit`;
|
||||
export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence_server/edit`;
|
||||
export const EDIT_DROPBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/dropbox/edit`;
|
||||
export const EDIT_GITHUB_ENTERPRISE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github_enterprise_server/edit`;
|
||||
export const EDIT_GITHUB_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github/edit`;
|
||||
export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`;
|
||||
export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google_drive/edit`;
|
||||
export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_cloud/edit`;
|
||||
export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira_server/edit`;
|
||||
export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one_drive/edit`;
|
||||
export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`;
|
||||
export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce_sandbox/edit`;
|
||||
export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`;
|
||||
export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share_point/edit`;
|
||||
export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`;
|
||||
export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`;
|
||||
export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`;
|
||||
|
||||
export const getContentSourcePath = (
|
||||
path: string,
|
||||
|
@ -118,3 +77,6 @@ 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 getEditPath = (serviceType: string): string =>
|
||||
`${ORG_SETTINGS_CONNECTORS_PATH}/${serviceType}/edit`;
|
||||
|
|
|
@ -66,23 +66,27 @@ export interface Configuration {
|
|||
needsConfiguration?: boolean;
|
||||
hasOauthRedirect: boolean;
|
||||
baseUrlTitle?: string;
|
||||
helpText: string;
|
||||
helpText?: string;
|
||||
documentationUrl: string;
|
||||
applicationPortalUrl?: string;
|
||||
applicationLinkTitle?: string;
|
||||
githubRepository?: string;
|
||||
}
|
||||
|
||||
export interface SourceDataItem {
|
||||
name: string;
|
||||
iconName: string;
|
||||
categories?: string[];
|
||||
serviceType: string;
|
||||
configuration: Configuration;
|
||||
configured?: boolean;
|
||||
connected?: boolean;
|
||||
features?: Features;
|
||||
objTypes?: string[];
|
||||
addPath: string;
|
||||
editPath?: string; // undefined for GitHub apps, as they are configured on a source level, and don't use a connector where you can edit the configuration
|
||||
accountContextOnly: boolean;
|
||||
internalConnectorAvailable?: boolean;
|
||||
externalConnectorAvailable?: boolean;
|
||||
customConnectorAvailable?: boolean;
|
||||
}
|
||||
|
||||
export interface ContentSource {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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;
|
|
@ -11,3 +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';
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function isNotNullish<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { sourceConfigData } from '../../../../__mocks__/content_sources.mock';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
WorkplaceSearchPageTemplate,
|
||||
PersonalDashboardLayout,
|
||||
} from '../../../../components/layout';
|
||||
import { staticSourceData } from '../../source_data';
|
||||
|
||||
import { AddCustomSource } from './add_custom_source';
|
||||
import { AddCustomSourceSteps } from './add_custom_source_logic';
|
||||
import { ConfigureCustom } from './configure_custom';
|
||||
import { SaveCustom } from './save_custom';
|
||||
|
||||
describe('AddCustomSource', () => {
|
||||
const props = {
|
||||
sourceData: staticSourceData[0],
|
||||
initialValues: undefined,
|
||||
};
|
||||
|
||||
const values = {
|
||||
sourceConfigData,
|
||||
isOrganization: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockValues({ ...values });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<AddCustomSource {...props} />);
|
||||
|
||||
expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should show correct layout for personal dashboard', () => {
|
||||
setMockValues({ isOrganization: false });
|
||||
const wrapper = shallow(<AddCustomSource {...props} />);
|
||||
|
||||
expect(wrapper.find(WorkplaceSearchPageTemplate)).toHaveLength(0);
|
||||
expect(wrapper.find(PersonalDashboardLayout)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should show Configure Custom for custom configuration step', () => {
|
||||
setMockValues({ currentStep: AddCustomSourceSteps.ConfigureCustomStep });
|
||||
const wrapper = shallow(<AddCustomSource {...props} />);
|
||||
|
||||
expect(wrapper.find(ConfigureCustom)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should show Save Custom for save custom step', () => {
|
||||
setMockValues({ currentStep: AddCustomSourceSteps.SaveCustomStep });
|
||||
const wrapper = shallow(<AddCustomSource {...props} />);
|
||||
|
||||
expect(wrapper.find(SaveCustom)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
import {
|
||||
WorkplaceSearchPageTemplate,
|
||||
PersonalDashboardLayout,
|
||||
} from '../../../../components/layout';
|
||||
import { NAV } from '../../../../constants';
|
||||
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
|
||||
import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic';
|
||||
import { ConfigureCustom } from './configure_custom';
|
||||
import { SaveCustom } from './save_custom';
|
||||
|
||||
import './add_source.scss';
|
||||
|
||||
interface Props {
|
||||
sourceData: SourceDataItem;
|
||||
initialValue?: string;
|
||||
}
|
||||
export const AddCustomSource: React.FC<Props> = ({ sourceData, initialValue = '' }) => {
|
||||
const addCustomSourceLogic = AddCustomSourceLogic({ sourceData, initialValue });
|
||||
const { currentStep } = useValues(addCustomSourceLogic);
|
||||
const { isOrganization } = useValues(AppLogic);
|
||||
|
||||
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
|
||||
|
||||
return (
|
||||
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, sourceData.name || '...']}>
|
||||
{currentStep === AddCustomSourceSteps.ConfigureCustomStep && <ConfigureCustom />}
|
||||
{currentStep === AddCustomSourceSteps.SaveCustomStep && <SaveCustom />}
|
||||
</Layout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 {
|
||||
LogicMounter,
|
||||
mockFlashMessageHelpers,
|
||||
mockHttpValues,
|
||||
} from '../../../../../__mocks__/kea_logic';
|
||||
import { sourceConfigData } from '../../../../__mocks__/content_sources.mock';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { docLinks } from '../../../../../shared/doc_links';
|
||||
import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers';
|
||||
|
||||
jest.mock('../../../../app_logic', () => ({
|
||||
AppLogic: { values: { isOrganization: true } },
|
||||
}));
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
|
||||
import { SOURCE_NAMES } from '../../../../constants';
|
||||
import { CustomSource, SourceDataItem } 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,
|
||||
helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', {
|
||||
defaultMessage:
|
||||
'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.',
|
||||
}),
|
||||
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', () => {
|
||||
const { mount } = new LogicMounter(AddCustomSourceLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { clearFlashMessages } = mockFlashMessageHelpers;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mount({}, MOCK_PROPS);
|
||||
});
|
||||
|
||||
it('has expected default values', () => {
|
||||
expect(AddCustomSourceLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('setButtonNotLoading', () => {
|
||||
it('turns off the button loading flag', () => {
|
||||
AddCustomSourceLogic.actions.setButtonNotLoading();
|
||||
|
||||
expect(AddCustomSourceLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
buttonLoading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCustomSourceNameValue', () => {
|
||||
it('saves the name', () => {
|
||||
AddCustomSourceLogic.actions.setCustomSourceNameValue('name');
|
||||
|
||||
expect(AddCustomSourceLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
customSourceNameValue: 'name',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setNewCustomSource', () => {
|
||||
it('saves the custom source', () => {
|
||||
const newCustomSource = {
|
||||
accessToken: 'foo',
|
||||
key: 'bar',
|
||||
name: 'source',
|
||||
id: '123key',
|
||||
};
|
||||
|
||||
AddCustomSourceLogic.actions.setNewCustomSource(newCustomSource);
|
||||
|
||||
expect(AddCustomSourceLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
newCustomSource,
|
||||
currentStep: AddCustomSourceSteps.SaveCustomStep,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
beforeEach(() => {
|
||||
mount(
|
||||
{
|
||||
customSourceNameValue: MOCK_NAME,
|
||||
},
|
||||
MOCK_PROPS
|
||||
);
|
||||
});
|
||||
|
||||
describe('organization context', () => {
|
||||
describe('createContentSource', () => {
|
||||
it('calls API and sets values', async () => {
|
||||
const setButtonNotLoadingSpy = jest.spyOn(
|
||||
AddCustomSourceLogic.actions,
|
||||
'setButtonNotLoading'
|
||||
);
|
||||
const setNewCustomSourceSpy = jest.spyOn(
|
||||
AddCustomSourceLogic.actions,
|
||||
'setNewCustomSource'
|
||||
);
|
||||
http.post.mockReturnValue(Promise.resolve({ sourceConfigData }));
|
||||
|
||||
AddCustomSourceLogic.actions.createContentSource();
|
||||
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
expect(AddCustomSourceLogic.values.buttonLoading).toEqual(true);
|
||||
expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/org/create_source', {
|
||||
body: JSON.stringify({ service_type: 'custom', name: MOCK_NAME }),
|
||||
});
|
||||
await nextTick();
|
||||
expect(setNewCustomSourceSpy).toHaveBeenCalledWith({ sourceConfigData });
|
||||
expect(setButtonNotLoadingSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
itShowsServerErrorAsFlashMessage(http.post, () => {
|
||||
AddCustomSourceLogic.actions.createContentSource();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('account context routes', () => {
|
||||
beforeEach(() => {
|
||||
AppLogic.values.isOrganization = false;
|
||||
});
|
||||
|
||||
describe('createContentSource', () => {
|
||||
it('sends relevant fields to the API', () => {
|
||||
AddCustomSourceLogic.actions.createContentSource();
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/account/create_source',
|
||||
{
|
||||
body: JSON.stringify({ service_type: 'custom', name: MOCK_NAME }),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
itShowsServerErrorAsFlashMessage(http.post, () => {
|
||||
AddCustomSourceLogic.actions.createContentSource();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { 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';
|
||||
|
||||
export interface AddCustomSourceProps {
|
||||
sourceData: SourceDataItem;
|
||||
initialValue: string;
|
||||
}
|
||||
|
||||
export enum AddCustomSourceSteps {
|
||||
ConfigureCustomStep = 'Configure Custom',
|
||||
SaveCustomStep = 'Save Custom',
|
||||
}
|
||||
|
||||
export interface AddCustomSourceActions {
|
||||
createContentSource(): void;
|
||||
setButtonNotLoading(): void;
|
||||
setCustomSourceNameValue(customSourceNameValue: string): string;
|
||||
setNewCustomSource(data: CustomSource): CustomSource;
|
||||
}
|
||||
|
||||
interface AddCustomSourceValues {
|
||||
buttonLoading: boolean;
|
||||
currentStep: AddCustomSourceSteps;
|
||||
customSourceNameValue: string;
|
||||
newCustomSource: CustomSource;
|
||||
sourceData: SourceDataItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workplace Search needs to know the host for the redirect. As of yet, we do not
|
||||
* have access to this in Kibana. We parse it from the browser and pass it as a param.
|
||||
*/
|
||||
|
||||
export const AddCustomSourceLogic = kea<
|
||||
MakeLogicType<AddCustomSourceValues, AddCustomSourceActions, AddCustomSourceProps>
|
||||
>({
|
||||
path: ['enterprise_search', 'workplace_search', 'add_custom_source_logic'],
|
||||
actions: {
|
||||
createContentSource: true,
|
||||
setButtonNotLoading: true,
|
||||
setCustomSourceNameValue: (customSourceNameValue) => customSourceNameValue,
|
||||
setNewCustomSource: (data) => data,
|
||||
},
|
||||
reducers: ({ props }) => ({
|
||||
buttonLoading: [
|
||||
false,
|
||||
{
|
||||
setButtonNotLoading: () => false,
|
||||
createContentSource: () => true,
|
||||
},
|
||||
],
|
||||
currentStep: [
|
||||
AddCustomSourceSteps.ConfigureCustomStep,
|
||||
{
|
||||
setNewCustomSource: () => AddCustomSourceSteps.SaveCustomStep,
|
||||
},
|
||||
],
|
||||
customSourceNameValue: [
|
||||
props.initialValue,
|
||||
{
|
||||
setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue,
|
||||
},
|
||||
],
|
||||
newCustomSource: [
|
||||
{} as CustomSource,
|
||||
{
|
||||
setNewCustomSource: (_, newCustomSource) => newCustomSource,
|
||||
},
|
||||
],
|
||||
sourceData: [props.sourceData],
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
createContentSource: async () => {
|
||||
clearFlashMessages();
|
||||
const { isOrganization } = AppLogic.values;
|
||||
const route = isOrganization
|
||||
? '/internal/workplace_search/org/create_source'
|
||||
: '/internal/workplace_search/account/create_source';
|
||||
|
||||
const { customSourceNameValue } = values;
|
||||
|
||||
const params = {
|
||||
service_type: 'custom',
|
||||
name: customSourceNameValue,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await HttpLogic.values.http.post<CustomSource>(route, {
|
||||
body: JSON.stringify({ ...params }),
|
||||
});
|
||||
actions.setNewCustomSource(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
} finally {
|
||||
actions.setButtonNotLoading();
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -22,16 +22,16 @@ 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 { ConfigurationIntro } from './configuration_intro';
|
||||
import { ConfigureCustom } from './configure_custom';
|
||||
import { ConfigureOauth } from './configure_oauth';
|
||||
import { ConnectInstance } from './connect_instance';
|
||||
import { Reauthenticate } from './reauthenticate';
|
||||
import { SaveConfig } from './save_config';
|
||||
import { SaveCustom } from './save_custom';
|
||||
|
||||
describe('AddSourceList', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
|
@ -65,7 +65,7 @@ describe('AddSourceList', () => {
|
|||
});
|
||||
|
||||
it('renders default state', () => {
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[0]} />);
|
||||
wrapper.find(ConfigurationIntro).prop('advanceStep')();
|
||||
|
||||
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
|
||||
|
@ -74,14 +74,14 @@ describe('AddSourceList', () => {
|
|||
describe('layout', () => {
|
||||
it('renders the default workplace search layout when on an organization view', () => {
|
||||
setMockValues({ ...mockValues, isOrganization: true });
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
|
||||
});
|
||||
|
||||
it('renders the personal dashboard layout when not in an organization', () => {
|
||||
setMockValues({ ...mockValues, isOrganization: false });
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.type()).toEqual(PersonalDashboardLayout);
|
||||
});
|
||||
|
@ -89,7 +89,7 @@ describe('AddSourceList', () => {
|
|||
|
||||
it('renders a breadcrumb fallback while data is loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} });
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']);
|
||||
});
|
||||
|
@ -99,7 +99,7 @@ describe('AddSourceList', () => {
|
|||
...mockValues,
|
||||
addSourceCurrentStep: AddSourceSteps.ConfigCompletedStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
wrapper.find(ConfigCompleted).prop('advanceStep')();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
|
||||
|
@ -111,7 +111,7 @@ describe('AddSourceList', () => {
|
|||
...mockValues,
|
||||
addSourceCurrentStep: AddSourceSteps.SaveConfigStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
const saveConfig = wrapper.find(SaveConfig);
|
||||
saveConfig.prop('advanceStep')();
|
||||
saveConfig.prop('goBackStep')!();
|
||||
|
@ -126,51 +126,30 @@ describe('AddSourceList', () => {
|
|||
sourceConfigData,
|
||||
addSourceCurrentStep: AddSourceSteps.ConnectInstanceStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} connect />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} connect />);
|
||||
wrapper.find(ConnectInstance).prop('onFormCreated')('foo');
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
|
||||
});
|
||||
|
||||
it('renders Configure Custom step', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
addSourceCurrentStep: AddSourceSteps.ConfigureCustomStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
wrapper.find(ConfigureCustom).prop('advanceStep')();
|
||||
|
||||
expect(createContentSource).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders Configure Oauth step', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
addSourceCurrentStep: AddSourceSteps.ConfigureOauthStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
|
||||
wrapper.find(ConfigureOauth).prop('onFormCreated')('foo');
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect');
|
||||
});
|
||||
|
||||
it('renders Save Custom step', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
addSourceCurrentStep: AddSourceSteps.SaveCustomStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
|
||||
expect(wrapper.find(SaveCustom)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders Reauthenticate step', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
addSourceCurrentStep: AddSourceSteps.ReauthenticateStep,
|
||||
});
|
||||
const wrapper = shallow(<AddSource sourceIndex={1} />);
|
||||
const wrapper = shallow(<AddSource sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.find(Reauthenticate)).toHaveLength(1);
|
||||
});
|
||||
|
|
|
@ -18,49 +18,28 @@ import {
|
|||
WorkplaceSearchPageTemplate,
|
||||
PersonalDashboardLayout,
|
||||
} from '../../../../components/layout';
|
||||
import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants';
|
||||
import { SOURCES_PATH, getSourcesPath } from '../../../../routes';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
import { staticSourceData } from '../../source_data';
|
||||
import { NAV } from '../../../../constants';
|
||||
import { SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes';
|
||||
|
||||
import { AddSourceHeader } from './add_source_header';
|
||||
import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic';
|
||||
import { ConfigCompleted } from './config_completed';
|
||||
import { ConfigurationIntro } from './configuration_intro';
|
||||
import { ConfigureCustom } from './configure_custom';
|
||||
import { ConfigureOauth } from './configure_oauth';
|
||||
import { ConnectInstance } from './connect_instance';
|
||||
import { Reauthenticate } from './reauthenticate';
|
||||
import { SaveConfig } from './save_config';
|
||||
import { SaveCustom } from './save_custom';
|
||||
|
||||
import './add_source.scss';
|
||||
|
||||
export const AddSource: React.FC<AddSourceProps> = (props) => {
|
||||
const {
|
||||
initializeAddSource,
|
||||
setAddSourceStep,
|
||||
saveSourceConfig,
|
||||
createContentSource,
|
||||
resetSourceState,
|
||||
} = useActions(AddSourceLogic);
|
||||
const {
|
||||
addSourceCurrentStep,
|
||||
sourceConfigData: {
|
||||
name,
|
||||
categories,
|
||||
needsPermissions,
|
||||
accountContextOnly,
|
||||
privateSourcesEnabled,
|
||||
},
|
||||
dataLoading,
|
||||
newCustomSource,
|
||||
} = useValues(AddSourceLogic);
|
||||
|
||||
const { serviceType, configuration, features, objTypes, addPath } = staticSourceData[
|
||||
props.sourceIndex
|
||||
] as SourceDataItem;
|
||||
|
||||
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);
|
||||
const { isOrganization } = useValues(AppLogic);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -85,9 +64,6 @@ export const AddSource: React.FC<AddSourceProps> = (props) => {
|
|||
KibanaLogic.values.navigateToUrl(`${getSourcesPath(addPath, isOrganization)}/connect`);
|
||||
};
|
||||
|
||||
const saveCustomSuccess = () => setAddSourceStep(AddSourceSteps.SaveCustomStep);
|
||||
const goToSaveCustom = () => createContentSource(CUSTOM_SERVICE_TYPE, saveCustomSuccess);
|
||||
|
||||
const goToFormSourceCreated = () => {
|
||||
KibanaLogic.values.navigateToUrl(`${getSourcesPath(SOURCES_PATH, isOrganization)}`);
|
||||
flashSuccessToast(FORM_SOURCE_ADDED_SUCCESS_MESSAGE);
|
||||
|
@ -131,24 +107,9 @@ export const AddSource: React.FC<AddSourceProps> = (props) => {
|
|||
header={header}
|
||||
/>
|
||||
)}
|
||||
{addSourceCurrentStep === AddSourceSteps.ConfigureCustomStep && (
|
||||
<ConfigureCustom
|
||||
helpText={configuration.helpText}
|
||||
advanceStep={goToSaveCustom}
|
||||
header={header}
|
||||
/>
|
||||
)}
|
||||
{addSourceCurrentStep === AddSourceSteps.ConfigureOauthStep && (
|
||||
<ConfigureOauth name={name} onFormCreated={goToFormSourceCreated} header={header} />
|
||||
)}
|
||||
{addSourceCurrentStep === AddSourceSteps.SaveCustomStep && (
|
||||
<SaveCustom
|
||||
documentationUrl={configuration.documentationUrl}
|
||||
newCustomSource={newCustomSource}
|
||||
isOrganization={isOrganization}
|
||||
header={header}
|
||||
/>
|
||||
)}
|
||||
{addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && (
|
||||
<Reauthenticate name={name} header={header} />
|
||||
)}
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '../../../../components/layout';
|
||||
import { ContentSection } from '../../../../components/shared/content_section';
|
||||
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
|
||||
import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants';
|
||||
import { NAV, CUSTOM_SERVICE_TYPE, EXTERNAL_SERVICE_TYPE } from '../../../../constants';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
import { SourcesLogic } from '../../sources_logic';
|
||||
|
||||
|
@ -90,12 +90,12 @@ export const AddSourceList: React.FC = () => {
|
|||
const filterConfiguredSources = (source: SourceDataItem) =>
|
||||
filterSources(source, configuredSources);
|
||||
|
||||
const visibleAvailableSources = availableSources.filter(
|
||||
filterAvailableSources
|
||||
) as SourceDataItem[];
|
||||
const visibleConfiguredSources = configuredSources.filter(
|
||||
filterConfiguredSources
|
||||
) as SourceDataItem[];
|
||||
const visibleAvailableSources = availableSources
|
||||
.filter(filterAvailableSources)
|
||||
.filter((source) => source.serviceType !== EXTERNAL_SERVICE_TYPE);
|
||||
// The API returns available external sources as a separate entry, but we don't want to present them as options to add
|
||||
|
||||
const visibleConfiguredSources = configuredSources.filter(filterConfiguredSources);
|
||||
|
||||
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ 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,13 +23,9 @@ jest.mock('../../../../app_logic', () => ({
|
|||
}));
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
|
||||
import {
|
||||
ADD_GITHUB_PATH,
|
||||
SOURCES_PATH,
|
||||
PRIVATE_SOURCES_PATH,
|
||||
getSourcesPath,
|
||||
} from '../../../../routes';
|
||||
import { CustomSource } from '../../../../types';
|
||||
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 { SourcesLogic } from '../../sources_logic';
|
||||
|
||||
|
@ -38,6 +35,8 @@ import {
|
|||
SourceConfigData,
|
||||
SourceConnectData,
|
||||
OrganizationsMap,
|
||||
AddSourceValues,
|
||||
AddSourceProps,
|
||||
} from './add_source_logic';
|
||||
|
||||
describe('AddSourceLogic', () => {
|
||||
|
@ -46,13 +45,12 @@ describe('AddSourceLogic', () => {
|
|||
const { navigateToUrl } = mockKibanaValues;
|
||||
const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
const DEFAULT_VALUES: AddSourceValues = {
|
||||
addSourceCurrentStep: AddSourceSteps.ConfigIntroStep,
|
||||
addSourceProps: {},
|
||||
addSourceProps: {} as AddSourceProps,
|
||||
dataLoading: true,
|
||||
sectionLoading: true,
|
||||
buttonLoading: false,
|
||||
customSourceNameValue: '',
|
||||
clientIdValue: '',
|
||||
clientSecretValue: '',
|
||||
baseUrlValue: '',
|
||||
|
@ -62,7 +60,6 @@ describe('AddSourceLogic', () => {
|
|||
indexPermissionsValue: false,
|
||||
sourceConfigData: {} as SourceConfigData,
|
||||
sourceConnectData: {} as SourceConnectData,
|
||||
newCustomSource: {} as CustomSource,
|
||||
oauthConfigCompleted: false,
|
||||
currentServiceType: '',
|
||||
githubOrganizations: [],
|
||||
|
@ -81,8 +78,34 @@ describe('AddSourceLogic', () => {
|
|||
serviceType: 'github',
|
||||
githubOrganizations: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
const CUSTOM_SERVICE_TYPE_INDEX = 17;
|
||||
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,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -145,15 +168,6 @@ describe('AddSourceLogic', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('setCustomSourceNameValue', () => {
|
||||
AddSourceLogic.actions.setCustomSourceNameValue('name');
|
||||
|
||||
expect(AddSourceLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
customSourceNameValue: 'name',
|
||||
});
|
||||
});
|
||||
|
||||
it('setSourceLoginValue', () => {
|
||||
AddSourceLogic.actions.setSourceLoginValue('login');
|
||||
|
||||
|
@ -190,22 +204,6 @@ describe('AddSourceLogic', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('setCustomSourceData', () => {
|
||||
const newCustomSource = {
|
||||
accessToken: 'foo',
|
||||
key: 'bar',
|
||||
name: 'source',
|
||||
id: '123key',
|
||||
};
|
||||
|
||||
AddSourceLogic.actions.setCustomSourceData(newCustomSource);
|
||||
|
||||
expect(AddSourceLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
newCustomSource,
|
||||
});
|
||||
});
|
||||
|
||||
it('setPreContentSourceConfigData', () => {
|
||||
AddSourceLogic.actions.setPreContentSourceConfigData(config);
|
||||
|
||||
|
@ -260,13 +258,14 @@ describe('AddSourceLogic', () => {
|
|||
});
|
||||
|
||||
it('handles fallback states', () => {
|
||||
const { publicKey, privateKey, consumerKey } = sourceConfigData.configuredFields;
|
||||
const sourceConfigDataMock = {
|
||||
const { publicKey, privateKey, consumerKey, apiKey } = sourceConfigData.configuredFields;
|
||||
const sourceConfigDataMock: SourceConfigData = {
|
||||
...sourceConfigData,
|
||||
configuredFields: {
|
||||
publicKey,
|
||||
privateKey,
|
||||
consumerKey,
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
AddSourceLogic.actions.setSourceConfigData(sourceConfigDataMock);
|
||||
|
@ -284,7 +283,7 @@ describe('AddSourceLogic', () => {
|
|||
|
||||
describe('listeners', () => {
|
||||
it('initializeAddSource', () => {
|
||||
const addSourceProps = { sourceIndex: 1 };
|
||||
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE };
|
||||
const getSourceConfigDataSpy = jest.spyOn(AddSourceLogic.actions, 'getSourceConfigData');
|
||||
const setAddSourcePropsSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceProps');
|
||||
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
|
||||
|
@ -293,21 +292,13 @@ describe('AddSourceLogic', () => {
|
|||
|
||||
expect(setAddSourcePropsSpy).toHaveBeenCalledWith({ addSourceProps });
|
||||
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigIntroStep);
|
||||
expect(getSourceConfigDataSpy).toHaveBeenCalledWith('confluence_cloud');
|
||||
expect(getSourceConfigDataSpy).toHaveBeenCalledWith('box');
|
||||
});
|
||||
|
||||
describe('getFirstStep', () => {
|
||||
it('sets custom as first step', () => {
|
||||
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
|
||||
const addSourceProps = { sourceIndex: CUSTOM_SERVICE_TYPE_INDEX };
|
||||
AddSourceLogic.actions.initializeAddSource(addSourceProps);
|
||||
|
||||
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigureCustomStep);
|
||||
});
|
||||
|
||||
it('sets connect as first step', () => {
|
||||
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
|
||||
const addSourceProps = { sourceIndex: 1, connect: true };
|
||||
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, connect: true };
|
||||
AddSourceLogic.actions.initializeAddSource(addSourceProps);
|
||||
|
||||
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep);
|
||||
|
@ -315,7 +306,7 @@ describe('AddSourceLogic', () => {
|
|||
|
||||
it('sets configure as first step', () => {
|
||||
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
|
||||
const addSourceProps = { sourceIndex: 1, configure: true };
|
||||
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, configure: true };
|
||||
AddSourceLogic.actions.initializeAddSource(addSourceProps);
|
||||
|
||||
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ConfigureOauthStep);
|
||||
|
@ -323,7 +314,7 @@ describe('AddSourceLogic', () => {
|
|||
|
||||
it('sets reAuthenticate as first step', () => {
|
||||
const setAddSourceStepSpy = jest.spyOn(AddSourceLogic.actions, 'setAddSourceStep');
|
||||
const addSourceProps = { sourceIndex: 1, reAuthenticate: true };
|
||||
const addSourceProps = { sourceData: DEFAULT_SERVICE_TYPE, reAuthenticate: true };
|
||||
AddSourceLogic.actions.initializeAddSource(addSourceProps);
|
||||
|
||||
expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReauthenticateStep);
|
||||
|
@ -401,7 +392,7 @@ describe('AddSourceLogic', () => {
|
|||
await nextTick();
|
||||
|
||||
expect(setPreContentSourceIdSpy).toHaveBeenCalledWith(preContentSourceId);
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(`${ADD_GITHUB_PATH}/configure${queryString}`);
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(`/sources/add/github/configure${queryString}`);
|
||||
});
|
||||
|
||||
describe('Github error edge case', () => {
|
||||
|
@ -635,7 +626,6 @@ describe('AddSourceLogic', () => {
|
|||
const errorCallback = jest.fn();
|
||||
|
||||
const serviceType = 'zendesk';
|
||||
const name = 'name';
|
||||
const login = 'login';
|
||||
const password = 'password';
|
||||
const indexPermissions = false;
|
||||
|
@ -643,7 +633,6 @@ describe('AddSourceLogic', () => {
|
|||
let params: any;
|
||||
|
||||
beforeEach(() => {
|
||||
AddSourceLogic.actions.setCustomSourceNameValue(name);
|
||||
AddSourceLogic.actions.setSourceLoginValue(login);
|
||||
AddSourceLogic.actions.setSourcePasswordValue(password);
|
||||
AddSourceLogic.actions.setPreContentSourceConfigData(config);
|
||||
|
@ -652,7 +641,6 @@ describe('AddSourceLogic', () => {
|
|||
|
||||
params = {
|
||||
service_type: serviceType,
|
||||
name,
|
||||
login,
|
||||
password,
|
||||
organizations: ['foo'],
|
||||
|
@ -661,8 +649,7 @@ describe('AddSourceLogic', () => {
|
|||
|
||||
it('calls API and sets values', async () => {
|
||||
const setButtonNotLoadingSpy = jest.spyOn(AddSourceLogic.actions, 'setButtonNotLoading');
|
||||
const setCustomSourceDataSpy = jest.spyOn(AddSourceLogic.actions, 'setCustomSourceData');
|
||||
http.post.mockReturnValue(Promise.resolve({ sourceConfigData }));
|
||||
http.post.mockReturnValue(Promise.resolve());
|
||||
|
||||
AddSourceLogic.actions.createContentSource(serviceType, successCallback, errorCallback);
|
||||
|
||||
|
@ -672,7 +659,6 @@ describe('AddSourceLogic', () => {
|
|||
body: JSON.stringify({ ...params }),
|
||||
});
|
||||
await nextTick();
|
||||
expect(setCustomSourceDataSpy).toHaveBeenCalledWith({ sourceConfigData });
|
||||
expect(successCallback).toHaveBeenCalled();
|
||||
expect(setButtonNotLoadingSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -21,20 +21,14 @@ import {
|
|||
import { HttpLogic } from '../../../../../shared/http';
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants';
|
||||
import {
|
||||
SOURCES_PATH,
|
||||
ADD_GITHUB_PATH,
|
||||
PRIVATE_SOURCES_PATH,
|
||||
getSourcesPath,
|
||||
} from '../../../../routes';
|
||||
import { CustomSource } from '../../../../types';
|
||||
import { WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants';
|
||||
import { SOURCES_PATH, PRIVATE_SOURCES_PATH, getSourcesPath, getAddPath } from '../../../../routes';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
import { PERSONAL_DASHBOARD_SOURCE_ERROR } from '../../constants';
|
||||
import { staticSourceData } from '../../source_data';
|
||||
import { SourcesLogic } from '../../sources_logic';
|
||||
|
||||
export interface AddSourceProps {
|
||||
sourceIndex: number;
|
||||
sourceData: SourceDataItem;
|
||||
connect?: boolean;
|
||||
configure?: boolean;
|
||||
reAuthenticate?: boolean;
|
||||
|
@ -45,9 +39,7 @@ export enum AddSourceSteps {
|
|||
SaveConfigStep = 'Save Config',
|
||||
ConfigCompletedStep = 'Config Completed',
|
||||
ConnectInstanceStep = 'Connect Instance',
|
||||
ConfigureCustomStep = 'Configure Custom',
|
||||
ConfigureOauthStep = 'Configure Oauth',
|
||||
SaveCustomStep = 'Save Custom',
|
||||
ReauthenticateStep = 'Reauthenticate',
|
||||
}
|
||||
|
||||
|
@ -71,12 +63,10 @@ export interface AddSourceActions {
|
|||
setClientIdValue(clientIdValue: string): string;
|
||||
setClientSecretValue(clientSecretValue: string): string;
|
||||
setBaseUrlValue(baseUrlValue: string): string;
|
||||
setCustomSourceNameValue(customSourceNameValue: string): string;
|
||||
setSourceLoginValue(loginValue: string): string;
|
||||
setSourcePasswordValue(passwordValue: string): string;
|
||||
setSourceSubdomainValue(subdomainValue: string): string;
|
||||
setSourceIndexPermissionsValue(indexPermissionsValue: boolean): boolean;
|
||||
setCustomSourceData(data: CustomSource): CustomSource;
|
||||
setPreContentSourceConfigData(data: PreContentSourceResponse): PreContentSourceResponse;
|
||||
setPreContentSourceId(preContentSourceId: string): string;
|
||||
setSelectedGithubOrganizations(option: string): string;
|
||||
|
@ -119,6 +109,8 @@ export interface SourceConfigData {
|
|||
baseUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
url?: string;
|
||||
apiKey?: string;
|
||||
};
|
||||
accountContextOnly?: boolean;
|
||||
}
|
||||
|
@ -132,13 +124,12 @@ export interface OrganizationsMap {
|
|||
[key: string]: string | boolean;
|
||||
}
|
||||
|
||||
interface AddSourceValues {
|
||||
export interface AddSourceValues {
|
||||
addSourceProps: AddSourceProps;
|
||||
addSourceCurrentStep: AddSourceSteps;
|
||||
dataLoading: boolean;
|
||||
sectionLoading: boolean;
|
||||
buttonLoading: boolean;
|
||||
customSourceNameValue: string;
|
||||
clientIdValue: string;
|
||||
clientSecretValue: string;
|
||||
baseUrlValue: string;
|
||||
|
@ -148,7 +139,6 @@ interface AddSourceValues {
|
|||
indexPermissionsValue: boolean;
|
||||
sourceConfigData: SourceConfigData;
|
||||
sourceConnectData: SourceConnectData;
|
||||
newCustomSource: CustomSource;
|
||||
currentServiceType: string;
|
||||
githubOrganizations: string[];
|
||||
selectedGithubOrganizationsMap: OrganizationsMap;
|
||||
|
@ -185,12 +175,10 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
setClientIdValue: (clientIdValue: string) => clientIdValue,
|
||||
setClientSecretValue: (clientSecretValue: string) => clientSecretValue,
|
||||
setBaseUrlValue: (baseUrlValue: string) => baseUrlValue,
|
||||
setCustomSourceNameValue: (customSourceNameValue: string) => customSourceNameValue,
|
||||
setSourceLoginValue: (loginValue: string) => loginValue,
|
||||
setSourcePasswordValue: (passwordValue: string) => passwordValue,
|
||||
setSourceSubdomainValue: (subdomainValue: string) => subdomainValue,
|
||||
setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue,
|
||||
setCustomSourceData: (data: CustomSource) => data,
|
||||
setPreContentSourceConfigData: (data: PreContentSourceResponse) => data,
|
||||
setPreContentSourceId: (preContentSourceId: string) => preContentSourceId,
|
||||
setSelectedGithubOrganizations: (option: string) => option,
|
||||
|
@ -322,20 +310,6 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
resetSourceState: () => false,
|
||||
},
|
||||
],
|
||||
customSourceNameValue: [
|
||||
'',
|
||||
{
|
||||
setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue,
|
||||
resetSourceState: () => '',
|
||||
},
|
||||
],
|
||||
newCustomSource: [
|
||||
{} as CustomSource,
|
||||
{
|
||||
setCustomSourceData: (_, newCustomSource) => newCustomSource,
|
||||
resetSourceState: () => ({} as CustomSource),
|
||||
},
|
||||
],
|
||||
currentServiceType: [
|
||||
'',
|
||||
{
|
||||
|
@ -383,7 +357,7 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
initializeAddSource: ({ addSourceProps }) => {
|
||||
const { serviceType } = staticSourceData[addSourceProps.sourceIndex];
|
||||
const { serviceType } = addSourceProps.sourceData;
|
||||
actions.setAddSourceProps({ addSourceProps });
|
||||
actions.setAddSourceStep(getFirstStep(addSourceProps));
|
||||
actions.getSourceConfigData(serviceType);
|
||||
|
@ -540,7 +514,9 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
// GitHub requires an intermediate configuration step, where we collect the repos to index.
|
||||
if (hasConfigureStep && !values.oauthConfigCompleted) {
|
||||
actions.setPreContentSourceId(preContentSourceId);
|
||||
navigateToUrl(getSourcesPath(`${ADD_GITHUB_PATH}/configure${search}`, isOrganization));
|
||||
navigateToUrl(
|
||||
getSourcesPath(`${getAddPath('github')}/configure${search}`, isOrganization)
|
||||
);
|
||||
} else {
|
||||
setAddedSource(serviceName, indexPermissions, serviceType);
|
||||
navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization));
|
||||
|
@ -559,7 +535,6 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
|
||||
const {
|
||||
selectedGithubOrganizations: githubOrganizations,
|
||||
customSourceNameValue,
|
||||
loginValue,
|
||||
passwordValue,
|
||||
indexPermissionsValue,
|
||||
|
@ -567,7 +542,6 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
|
||||
const params = {
|
||||
service_type: serviceType,
|
||||
name: customSourceNameValue || undefined,
|
||||
login: loginValue || undefined,
|
||||
password: passwordValue || undefined,
|
||||
organizations: githubOrganizations.length > 0 ? githubOrganizations : undefined,
|
||||
|
@ -580,10 +554,9 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
|
||||
|
||||
try {
|
||||
const response = await HttpLogic.values.http.post<CustomSource>(route, {
|
||||
await HttpLogic.values.http.post(route, {
|
||||
body: JSON.stringify({ ...params }),
|
||||
});
|
||||
actions.setCustomSourceData(response);
|
||||
successCallback();
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
|
@ -596,11 +569,7 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
|
|||
});
|
||||
|
||||
const getFirstStep = (props: AddSourceProps): AddSourceSteps => {
|
||||
const { sourceIndex, connect, configure, reAuthenticate } = props;
|
||||
const { serviceType } = staticSourceData[sourceIndex];
|
||||
const isCustom = serviceType === CUSTOM_SERVICE_TYPE;
|
||||
|
||||
if (isCustom) return AddSourceSteps.ConfigureCustomStep;
|
||||
const { connect, configure, reAuthenticate } = props;
|
||||
if (connect) return AddSourceSteps.ConnectInstanceStep;
|
||||
if (configure) return AddSourceSteps.ConfigureOauthStep;
|
||||
if (reAuthenticate) return AddSourceSteps.ReauthenticateStep;
|
||||
|
|
|
@ -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(11);
|
||||
expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(20);
|
||||
expect(wrapper.find('[data-test-subj="CustomAPISourceLink"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
@ -34,7 +34,7 @@ describe('AvailableSourcesList', () => {
|
|||
setMockValues({ hasPlatinumLicense: false });
|
||||
const wrapper = shallow(<AvailableSourcesList sources={mergedAvailableSources} />);
|
||||
|
||||
expect(wrapper.find(EuiToolTip)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiToolTip)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles empty state', () => {
|
||||
|
|
|
@ -24,9 +24,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import { LicensingLogic } from '../../../../../shared/licensing';
|
||||
import { EuiButtonEmptyTo, EuiLinkTo } from '../../../../../shared/react_router_helpers';
|
||||
import { SourceIcon } from '../../../../components/shared/source_icon';
|
||||
import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes';
|
||||
import { ADD_CUSTOM_PATH, getAddPath, getSourcesPath } from '../../../../routes';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
|
||||
import { staticCustomSourceData } from '../../source_data';
|
||||
|
||||
import {
|
||||
AVAILABLE_SOURCE_EMPTY_STATE,
|
||||
AVAILABLE_SOURCE_TITLE,
|
||||
|
@ -41,7 +43,8 @@ interface AvailableSourcesListProps {
|
|||
export const AvailableSourcesList: React.FC<AvailableSourcesListProps> = ({ sources }) => {
|
||||
const { hasPlatinumLicense } = useValues(LicensingLogic);
|
||||
|
||||
const getSourceCard = ({ name, serviceType, addPath, accountContextOnly }: SourceDataItem) => {
|
||||
const getSourceCard = ({ name, serviceType, accountContextOnly }: SourceDataItem) => {
|
||||
const addPath = getAddPath(serviceType);
|
||||
const disabled = !hasPlatinumLicense && accountContextOnly;
|
||||
|
||||
const connectButton = () => {
|
||||
|
@ -105,6 +108,15 @@ export const AvailableSourcesList: React.FC<AvailableSourcesListProps> = ({ sour
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
<EuiFlexItem grow={false} data-test-subj="AvailableSourceListItem">
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="stretch"
|
||||
data-test-subj="AvailableSourceCard"
|
||||
>
|
||||
<EuiFlexItem>{getSourceCard(staticCustomSourceData)}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 { mockKibanaValues, setMockValues } from '../../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiText, EuiButton } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
PersonalDashboardLayout,
|
||||
WorkplaceSearchPageTemplate,
|
||||
} from '../../../../components/layout';
|
||||
import { staticSourceData } from '../../source_data';
|
||||
|
||||
import { ConfigurationChoice } from './configuration_choice';
|
||||
|
||||
describe('ConfigurationChoice', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const props = {
|
||||
sourceData: staticSourceData[0],
|
||||
};
|
||||
const mockValues = {
|
||||
isOrganization: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockValues(mockValues);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('layout', () => {
|
||||
it('renders the default workplace search layout when on an organization view', () => {
|
||||
setMockValues({ ...mockValues, isOrganization: true });
|
||||
const wrapper = shallow(<ConfigurationChoice sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
|
||||
});
|
||||
|
||||
it('renders the personal dashboard layout when not in an organization', () => {
|
||||
setMockValues({ ...mockValues, isOrganization: false });
|
||||
const wrapper = shallow(<ConfigurationChoice sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.type()).toEqual(PersonalDashboardLayout);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders internal connector if available', () => {
|
||||
const wrapper = shallow(<ConfigurationChoice {...{ ...props }} />);
|
||||
|
||||
expect(wrapper.find('EuiPanel')).toHaveLength(1);
|
||||
expect(wrapper.find(EuiText)).toHaveLength(3);
|
||||
expect(wrapper.find(EuiButton)).toHaveLength(1);
|
||||
});
|
||||
it('should navigate to internal connector on internal connector click', () => {
|
||||
const wrapper = shallow(<ConfigurationChoice {...props} />);
|
||||
const button = wrapper.find(EuiButton);
|
||||
button.simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/internal/');
|
||||
});
|
||||
|
||||
it('renders external connector if available', () => {
|
||||
const wrapper = shallow(
|
||||
<ConfigurationChoice
|
||||
{...{
|
||||
...props,
|
||||
sourceData: {
|
||||
...props.sourceData,
|
||||
internalConnectorAvailable: false,
|
||||
externalConnectorAvailable: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiPanel')).toHaveLength(1);
|
||||
expect(wrapper.find(EuiText)).toHaveLength(3);
|
||||
expect(wrapper.find(EuiButton)).toHaveLength(1);
|
||||
});
|
||||
it('should navigate to external connector on external connector click', () => {
|
||||
const wrapper = shallow(
|
||||
<ConfigurationChoice
|
||||
{...{
|
||||
...props,
|
||||
sourceData: {
|
||||
...props.sourceData,
|
||||
internalConnectorAvailable: false,
|
||||
externalConnectorAvailable: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const button = wrapper.find(EuiButton);
|
||||
button.simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/external/');
|
||||
});
|
||||
|
||||
it('renders custom connector if available', () => {
|
||||
const wrapper = shallow(
|
||||
<ConfigurationChoice
|
||||
{...{
|
||||
...props,
|
||||
sourceData: {
|
||||
...props.sourceData,
|
||||
internalConnectorAvailable: false,
|
||||
externalConnectorAvailable: false,
|
||||
customConnectorAvailable: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiPanel')).toHaveLength(1);
|
||||
expect(wrapper.find(EuiText)).toHaveLength(3);
|
||||
expect(wrapper.find(EuiButton)).toHaveLength(1);
|
||||
});
|
||||
it('should navigate to custom connector on internal connector click', () => {
|
||||
const wrapper = shallow(
|
||||
<ConfigurationChoice
|
||||
{...{
|
||||
...props,
|
||||
sourceData: {
|
||||
...props.sourceData,
|
||||
internalConnectorAvailable: false,
|
||||
externalConnectorAvailable: false,
|
||||
customConnectorAvailable: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const button = wrapper.find(EuiButton);
|
||||
button.simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/box/custom/');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
import {
|
||||
WorkplaceSearchPageTemplate,
|
||||
PersonalDashboardLayout,
|
||||
} from '../../../../components/layout';
|
||||
import { NAV } from '../../../../constants';
|
||||
import { getAddPath, getSourcesPath } from '../../../../routes';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
|
||||
import { AddSourceHeader } from './add_source_header';
|
||||
|
||||
interface ConfigurationIntroProps {
|
||||
sourceData: SourceDataItem;
|
||||
}
|
||||
|
||||
export const ConfigurationChoice: React.FC<ConfigurationIntroProps> = ({
|
||||
sourceData: {
|
||||
name,
|
||||
serviceType,
|
||||
externalConnectorAvailable,
|
||||
internalConnectorAvailable,
|
||||
customConnectorAvailable,
|
||||
},
|
||||
}) => {
|
||||
const { isOrganization } = useValues(AppLogic);
|
||||
const goToInternal = () =>
|
||||
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 Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
|
||||
|
||||
return (
|
||||
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE]} pageViewTelemetry="add_source_choice">
|
||||
<AddSourceHeader name={name} serviceType={serviceType} categories={[]} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexStart"
|
||||
alignItems="flexStart"
|
||||
direction="row"
|
||||
responsive={false}
|
||||
>
|
||||
{internalConnectorAvailable && (
|
||||
<EuiFlexItem grow>
|
||||
<EuiPanel color="plain" hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{name}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.internal.title',
|
||||
{
|
||||
defaultMessage: 'Default connector',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.internal.description',
|
||||
{
|
||||
defaultMessage: 'Use our out-of-the-box connector to get started quickly.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton color="primary" fill onClick={goToInternal}>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.internal.button',
|
||||
{
|
||||
defaultMessage: 'Connect',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{externalConnectorAvailable && (
|
||||
<EuiFlexItem grow>
|
||||
<EuiPanel color="plain" hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{name}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.title',
|
||||
{
|
||||
defaultMessage: 'Custom connector',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Set up a custom connector for more configurability and control.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton color="primary" fill onClick={goToExternal}>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.external.button',
|
||||
{
|
||||
defaultMessage: 'Instructions',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{customConnectorAvailable && (
|
||||
<EuiFlexItem grow>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{name}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.custom.title',
|
||||
{
|
||||
defaultMessage: 'Custom connector',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.custom.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Set up a custom connector for more configurability and control.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton color="primary" fill onClick={goToCustom}>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.configExternalChoice.custom.button',
|
||||
{
|
||||
defaultMessage: 'Instructions',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Layout>
|
||||
);
|
||||
};
|
|
@ -14,45 +14,45 @@ import { shallow } from 'enzyme';
|
|||
|
||||
import { EuiForm, EuiFieldText } from '@elastic/eui';
|
||||
|
||||
import { staticSourceData } from '../../source_data';
|
||||
|
||||
import { ConfigureCustom } from './configure_custom';
|
||||
|
||||
describe('ConfigureCustom', () => {
|
||||
const advanceStep = jest.fn();
|
||||
const setCustomSourceNameValue = jest.fn();
|
||||
|
||||
const props = {
|
||||
header: <h1>Header</h1>,
|
||||
helpText: 'I bet you could use a hand.',
|
||||
advanceStep,
|
||||
};
|
||||
const createContentSource = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
setMockActions({ setCustomSourceNameValue });
|
||||
setMockValues({ customSourceNameValue: 'name', buttonLoading: false });
|
||||
setMockActions({ setCustomSourceNameValue, createContentSource });
|
||||
setMockValues({
|
||||
customSourceNameValue: 'name',
|
||||
buttonLoading: false,
|
||||
sourceData: staticSourceData[1],
|
||||
});
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<ConfigureCustom {...props} />);
|
||||
const wrapper = shallow(<ConfigureCustom />);
|
||||
|
||||
expect(wrapper.find(EuiForm)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles input change', () => {
|
||||
const wrapper = shallow(<ConfigureCustom {...props} />);
|
||||
const TEXT = 'changed for the better';
|
||||
const wrapper = shallow(<ConfigureCustom />);
|
||||
const text = 'changed for the better';
|
||||
const input = wrapper.find(EuiFieldText);
|
||||
input.simulate('change', { target: { value: TEXT } });
|
||||
input.simulate('change', { target: { value: text } });
|
||||
|
||||
expect(setCustomSourceNameValue).toHaveBeenCalledWith(TEXT);
|
||||
expect(setCustomSourceNameValue).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it('handles form submission', () => {
|
||||
const wrapper = shallow(<ConfigureCustom {...props} />);
|
||||
const wrapper = shallow(<ConfigureCustom />);
|
||||
|
||||
const preventDefault = jest.fn();
|
||||
wrapper.find('form').simulate('submit', { preventDefault });
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(advanceStep).toHaveBeenCalled();
|
||||
expect(createContentSource).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,51 +24,64 @@ import { docLinks } from '../../../../../shared/doc_links';
|
|||
|
||||
import { SOURCE_NAME_LABEL } from '../../constants';
|
||||
|
||||
import { AddSourceLogic } from './add_source_logic';
|
||||
import { AddCustomSourceLogic } from './add_custom_source_logic';
|
||||
import { AddSourceHeader } from './add_source_header';
|
||||
import { CONFIG_CUSTOM_BUTTON, CONFIG_CUSTOM_LINK_TEXT } from './constants';
|
||||
|
||||
interface ConfigureCustomProps {
|
||||
header: React.ReactNode;
|
||||
helpText: string;
|
||||
advanceStep(): void;
|
||||
}
|
||||
|
||||
export const ConfigureCustom: React.FC<ConfigureCustomProps> = ({
|
||||
helpText,
|
||||
advanceStep,
|
||||
header,
|
||||
}) => {
|
||||
const { setCustomSourceNameValue } = useActions(AddSourceLogic);
|
||||
const { customSourceNameValue, buttonLoading } = useValues(AddSourceLogic);
|
||||
export const ConfigureCustom: React.FC = () => {
|
||||
const { setCustomSourceNameValue, createContentSource } = useActions(AddCustomSourceLogic);
|
||||
const { customSourceNameValue, buttonLoading, sourceData } = useValues(AddCustomSourceLogic);
|
||||
|
||||
const handleFormSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
advanceStep();
|
||||
createContentSource();
|
||||
};
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setCustomSourceNameValue(e.target.value);
|
||||
|
||||
const {
|
||||
serviceType,
|
||||
configuration: { documentationUrl, helpText },
|
||||
name,
|
||||
categories = [],
|
||||
} = sourceData;
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
<AddSourceHeader name={name} serviceType={serviceType} categories={categories} />
|
||||
<EuiSpacer />
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<EuiForm>
|
||||
<EuiText grow={false}>
|
||||
<p>{helpText}</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.configCustom.docs.link.description"
|
||||
defaultMessage="{link} to learn more about Custom API Sources."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={docLinks.workplaceSearchCustomSources} target="_blank">
|
||||
{CONFIG_CUSTOM_LINK_TEXT}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{serviceType === 'custom' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.configCustom.docs.link.description"
|
||||
defaultMessage="{link} to learn more about Custom API Sources."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={docLinks.workplaceSearchCustomSources} target="_blank">
|
||||
{CONFIG_CUSTOM_LINK_TEXT}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.configCustom.deploymentDocs.link.description"
|
||||
defaultMessage="{link} to learn more about deploying a {name} source."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink target="_blank" href={documentationUrl}>
|
||||
{CONFIG_CUSTOM_LINK_TEXT}
|
||||
</EuiLink>
|
||||
),
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xxl" />
|
||||
|
@ -90,7 +103,17 @@ export const ConfigureCustom: React.FC<ConfigureCustomProps> = ({
|
|||
isLoading={buttonLoading}
|
||||
data-test-subj="CreateCustomButton"
|
||||
>
|
||||
{CONFIG_CUSTOM_BUTTON}
|
||||
{serviceType === 'custom' ? (
|
||||
CONFIG_CUSTOM_BUTTON
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.configCustom.createNamedSourceButtonLabel"
|
||||
defaultMessage="Create {name} source"
|
||||
values={{
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
|
|
|
@ -22,9 +22,9 @@ describe('ConfiguredSourcesList', () => {
|
|||
it('renders', () => {
|
||||
const wrapper = shallow(<ConfiguredSourcesList {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(5);
|
||||
expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(6);
|
||||
expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(16);
|
||||
expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(2);
|
||||
expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(19);
|
||||
});
|
||||
|
||||
it('handles empty state', () => {
|
||||
|
|
|
@ -22,8 +22,9 @@ import {
|
|||
|
||||
import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers';
|
||||
import { SourceIcon } from '../../../../components/shared/source_icon';
|
||||
import { getSourcesPath } from '../../../../routes';
|
||||
import { getAddPath, getSourcesPath } from '../../../../routes';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
import { hasMultipleConnectorOptions } from '../../../../utils';
|
||||
|
||||
import {
|
||||
CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP,
|
||||
|
@ -68,54 +69,62 @@ export const ConfiguredSourcesList: React.FC<ConfiguredSourcesProps> = ({
|
|||
|
||||
const visibleSources = (
|
||||
<EuiFlexGrid columns={3} gutterSize="m" className="source-grid-configured">
|
||||
{sources.map(({ name, serviceType, addPath, connected, accountContextOnly }, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<EuiFlexItem
|
||||
grow
|
||||
className="organizational-content-source-item"
|
||||
data-test-subj="ConfiguredSourcesListItem"
|
||||
>
|
||||
<EuiSplitPanel.Outer color="plain" hasShadow={false} hasBorder>
|
||||
<EuiSplitPanel.Inner>
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<SourceIcon serviceType={serviceType} name={name} size="xxl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>
|
||||
{name}
|
||||
{!connected && !accountContextOnly && isOrganization && unConnectedTooltip}
|
||||
{accountContextOnly && isOrganization && accountOnlyTooltip}
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner color="subdued" paddingSize="none">
|
||||
{((!isOrganization || (isOrganization && !accountContextOnly)) && (
|
||||
<EuiButtonEmptyTo
|
||||
className="eui-fullWidth"
|
||||
to={`${getSourcesPath(addPath, isOrganization)}/connect`}
|
||||
{sources.map((sourceData, i) => {
|
||||
const { connected, accountContextOnly, name, serviceType } = sourceData;
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<EuiFlexItem
|
||||
grow
|
||||
className="organizational-content-source-item"
|
||||
data-test-subj="ConfiguredSourcesListItem"
|
||||
>
|
||||
<EuiSplitPanel.Outer color="plain" hasShadow={false} hasBorder>
|
||||
<EuiSplitPanel.Inner>
|
||||
<EuiFlexGroup
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
{CONFIGURED_SOURCES_CONNECT_BUTTON}
|
||||
</EuiButtonEmptyTo>
|
||||
)) || (
|
||||
<EuiButtonEmpty className="eui-fullWidth" isDisabled>
|
||||
{ADD_SOURCE_ORG_SOURCES_TITLE}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<EuiFlexItem>
|
||||
<SourceIcon serviceType={serviceType} name={name} size="xxl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>
|
||||
{name}
|
||||
{!connected &&
|
||||
!accountContextOnly &&
|
||||
isOrganization &&
|
||||
unConnectedTooltip}
|
||||
{accountContextOnly && isOrganization && accountOnlyTooltip}
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner color="subdued" paddingSize="none">
|
||||
{((!isOrganization || (isOrganization && !accountContextOnly)) && (
|
||||
<EuiButtonEmptyTo
|
||||
className="eui-fullWidth"
|
||||
to={`${getSourcesPath(getAddPath(serviceType), isOrganization)}/${
|
||||
hasMultipleConnectorOptions(sourceData) ? '' : 'connect'
|
||||
}`}
|
||||
>
|
||||
{CONFIGURED_SOURCES_CONNECT_BUTTON}
|
||||
</EuiButtonEmptyTo>
|
||||
)) || (
|
||||
<EuiButtonEmpty className="eui-fullWidth" isDisabled>
|
||||
{ADD_SOURCE_ORG_SOURCES_TITLE}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('ConnectInstance', () => {
|
|||
|
||||
const credentialsSourceData = staticSourceData[13];
|
||||
const oauthSourceData = staticSourceData[0];
|
||||
const subdomainSourceData = staticSourceData[16];
|
||||
const subdomainSourceData = staticSourceData[18];
|
||||
|
||||
const props = {
|
||||
...credentialsSourceData,
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
|
||||
import { sourceConfigData } from '../../../../__mocks__/content_sources.mock';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiSteps } from '@elastic/eui';
|
||||
|
||||
import { staticSourceData } from '../../source_data';
|
||||
|
||||
import { ExternalConnectorConfig } from './external_connector_config';
|
||||
|
||||
describe('ExternalConnectorConfig', () => {
|
||||
const goBack = jest.fn();
|
||||
const onDeleteConfig = jest.fn();
|
||||
const setExternalConnectorApiKey = jest.fn();
|
||||
const setExternalConnectorUrl = jest.fn();
|
||||
const saveExternalConnectorConfig = jest.fn();
|
||||
const fetchExternalSource = jest.fn();
|
||||
|
||||
const props = {
|
||||
sourceData: staticSourceData[0],
|
||||
goBack,
|
||||
onDeleteConfig,
|
||||
};
|
||||
|
||||
const values = {
|
||||
sourceConfigData,
|
||||
buttonLoading: false,
|
||||
clientIdValue: 'foo',
|
||||
clientSecretValue: 'bar',
|
||||
baseUrlValue: 'http://foo.baz',
|
||||
hasPlatinumLicense: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockActions({
|
||||
setExternalConnectorApiKey,
|
||||
setExternalConnectorUrl,
|
||||
saveExternalConnectorConfig,
|
||||
fetchExternalSource,
|
||||
});
|
||||
setMockValues({ ...values });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
|
||||
|
||||
expect(wrapper.find(EuiSteps)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles form submission', () => {
|
||||
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
|
||||
|
||||
const preventDefault = jest.fn();
|
||||
wrapper.find('form').simulate('submit', { preventDefault });
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(saveExternalConnectorConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('external connector configuration', () => {
|
||||
it('handles url change', () => {
|
||||
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
|
||||
const steps = wrapper.find(EuiSteps);
|
||||
const input = steps.dive().find('[name="external-connector-url"]');
|
||||
input.simulate('change', { target: { value: 'url' } });
|
||||
|
||||
expect(setExternalConnectorUrl).toHaveBeenCalledWith('url');
|
||||
});
|
||||
|
||||
it('handles Client secret change', () => {
|
||||
const wrapper = shallow(<ExternalConnectorConfig {...props} />);
|
||||
const steps = wrapper.find(EuiSteps);
|
||||
const input = steps.dive().find('[name="external-connector-api-key"]');
|
||||
input.simulate('change', { target: { value: 'api-key' } });
|
||||
|
||||
expect(setExternalConnectorApiKey).toHaveBeenCalledWith('api-key');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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, { FormEvent, useEffect } from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSteps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
import {
|
||||
PersonalDashboardLayout,
|
||||
WorkplaceSearchPageTemplate,
|
||||
} from '../../../../components/layout';
|
||||
import { NAV, REMOVE_BUTTON } from '../../../../constants';
|
||||
import { SourceDataItem } from '../../../../types';
|
||||
|
||||
import { AddSourceHeader } from './add_source_header';
|
||||
import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON } from './constants';
|
||||
import { ExternalConnectorLogic } from './external_connector_logic';
|
||||
|
||||
interface SaveConfigProps {
|
||||
sourceData: SourceDataItem;
|
||||
goBack?: () => void;
|
||||
onDeleteConfig?: () => void;
|
||||
}
|
||||
|
||||
export const ExternalConnectorConfig: React.FC<SaveConfigProps> = ({ goBack, onDeleteConfig }) => {
|
||||
const serviceType = 'external';
|
||||
const {
|
||||
fetchExternalSource,
|
||||
setExternalConnectorApiKey,
|
||||
setExternalConnectorUrl,
|
||||
saveExternalConnectorConfig,
|
||||
} = useActions(ExternalConnectorLogic);
|
||||
|
||||
const { buttonLoading, externalConnectorUrl, externalConnectorApiKey, sourceConfigData } =
|
||||
useValues(ExternalConnectorLogic);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExternalSource();
|
||||
}, []);
|
||||
|
||||
const handleFormSubmission = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
saveExternalConnectorConfig({ url: externalConnectorUrl, apiKey: externalConnectorApiKey });
|
||||
};
|
||||
|
||||
const { name, categories } = sourceConfigData;
|
||||
const { isOrganization } = useValues(AppLogic);
|
||||
|
||||
const saveButton = (
|
||||
<EuiButton color="primary" fill isLoading={buttonLoading} type="submit">
|
||||
{OAUTH_SAVE_CONFIG_BUTTON}
|
||||
</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>
|
||||
);
|
||||
|
||||
const connectorForm = (
|
||||
<EuiFlexGroup justifyContent="flexStart" direction="column" responsive={false}>
|
||||
{/* TODO: get a docs link in here for the external connector
|
||||
<ConfigDocsLinks
|
||||
name={name}
|
||||
documentationUrl={documentationUrl}
|
||||
applicationPortalUrl={applicationPortalUrl}
|
||||
applicationLinkTitle={applicationLinkTitle}
|
||||
/> */}
|
||||
<EuiSpacer />
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.addSource.externalConnectorConfig.urlLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={externalConnectorUrl}
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
onChange={(e) => setExternalConnectorUrl(e.target.value)}
|
||||
name="external-connector-url"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.addSource.externalConnectorConfig.apiKeyLabel',
|
||||
{
|
||||
defaultMessage: 'API key',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={externalConnectorApiKey}
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
onChange={(e) => setExternalConnectorApiKey(e.target.value)}
|
||||
name="external-connector-api-key"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
{formActions}
|
||||
</EuiForm>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const configSteps = [
|
||||
{
|
||||
title: i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.contentSource.addSource.externalConnectorConfig.stepTitle',
|
||||
{
|
||||
defaultMessage: 'Provide the appropriate configuration information',
|
||||
}
|
||||
),
|
||||
children: connectorForm,
|
||||
},
|
||||
];
|
||||
|
||||
const header = <AddSourceHeader name={name} serviceType={serviceType} categories={categories} />;
|
||||
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
|
||||
|
||||
return (
|
||||
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name || '...']} isLoading={false}>
|
||||
{header}
|
||||
<EuiSpacer size="l" />
|
||||
<form onSubmit={handleFormSubmission}>
|
||||
<EuiSteps steps={configSteps} />
|
||||
</form>
|
||||
</Layout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { LogicMounter, mockHttpValues, mockKibanaValues } from '../../../../../__mocks__/kea_logic';
|
||||
import { sourceConfigData } from '../../../../__mocks__/content_sources.mock';
|
||||
|
||||
import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers';
|
||||
|
||||
jest.mock('../../../../app_logic', () => ({
|
||||
AppLogic: { values: { isOrganization: true } },
|
||||
}));
|
||||
|
||||
import { ExternalConnectorLogic, ExternalConnectorValues } from './external_connector_logic';
|
||||
|
||||
describe('ExternalConnectorLogic', () => {
|
||||
const { mount } = new LogicMounter(ExternalConnectorLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
|
||||
const DEFAULT_VALUES: ExternalConnectorValues = {
|
||||
dataLoading: true,
|
||||
buttonLoading: false,
|
||||
externalConnectorUrl: '',
|
||||
externalConnectorApiKey: '',
|
||||
sourceConfigData: {
|
||||
name: '',
|
||||
categories: [],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mount();
|
||||
});
|
||||
|
||||
it('has expected default values', () => {
|
||||
expect(ExternalConnectorLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('fetchExternalSourceSuccess', () => {
|
||||
beforeEach(() => {
|
||||
ExternalConnectorLogic.actions.fetchExternalSourceSuccess(sourceConfigData);
|
||||
});
|
||||
|
||||
it('turns off the data loading flag', () => {
|
||||
expect(ExternalConnectorLogic.values.dataLoading).toEqual(false);
|
||||
});
|
||||
|
||||
it('saves the external url', () => {
|
||||
expect(ExternalConnectorLogic.values.externalConnectorUrl).toEqual(
|
||||
sourceConfigData.configuredFields.url
|
||||
);
|
||||
});
|
||||
|
||||
it('saves the source config', () => {
|
||||
expect(ExternalConnectorLogic.values.sourceConfigData).toEqual(sourceConfigData);
|
||||
});
|
||||
|
||||
it('sets undefined url to empty string', () => {
|
||||
ExternalConnectorLogic.actions.fetchExternalSourceSuccess({
|
||||
...sourceConfigData,
|
||||
configuredFields: { ...sourceConfigData.configuredFields, url: undefined },
|
||||
});
|
||||
expect(ExternalConnectorLogic.values.externalConnectorUrl).toEqual('');
|
||||
});
|
||||
it('sets undefined api key to empty string', () => {
|
||||
ExternalConnectorLogic.actions.fetchExternalSourceSuccess({
|
||||
...sourceConfigData,
|
||||
configuredFields: { ...sourceConfigData.configuredFields, apiKey: undefined },
|
||||
});
|
||||
expect(ExternalConnectorLogic.values.externalConnectorApiKey).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveExternalConnectorConfigSuccess', () => {
|
||||
it('turns off the button loading flag', () => {
|
||||
mount({
|
||||
buttonLoading: true,
|
||||
});
|
||||
|
||||
ExternalConnectorLogic.actions.saveExternalConnectorConfigSuccess('external');
|
||||
|
||||
expect(ExternalConnectorLogic.values.buttonLoading).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setExternalConnectorApiKey', () => {
|
||||
it('updates the api key', () => {
|
||||
ExternalConnectorLogic.actions.setExternalConnectorApiKey('abcd1234');
|
||||
|
||||
expect(ExternalConnectorLogic.values.externalConnectorApiKey).toEqual('abcd1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setExternalConnectorUrl', () => {
|
||||
it('updates the url', () => {
|
||||
ExternalConnectorLogic.actions.setExternalConnectorUrl('https://www.elastic.co');
|
||||
|
||||
expect(ExternalConnectorLogic.values.externalConnectorUrl).toEqual(
|
||||
'https://www.elastic.co'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('fetchExternalSource', () => {
|
||||
it('retrieves config info on the "external" connector', () => {
|
||||
const promise = Promise.resolve();
|
||||
http.get.mockReturnValue(promise);
|
||||
ExternalConnectorLogic.actions.fetchExternalSource();
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/settings/connectors/external'
|
||||
);
|
||||
});
|
||||
|
||||
itShowsServerErrorAsFlashMessage(http.get, () => {
|
||||
mount();
|
||||
ExternalConnectorLogic.actions.fetchExternalSource();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveExternalConnectorConfig', () => {
|
||||
it('saves the external connector config', () => {
|
||||
const saveExternalConnectorConfigSuccess = jest.spyOn(
|
||||
ExternalConnectorLogic.actions,
|
||||
'saveExternalConnectorConfigSuccess'
|
||||
);
|
||||
ExternalConnectorLogic.actions.saveExternalConnectorConfig({
|
||||
url: 'url',
|
||||
apiKey: 'apiKey',
|
||||
});
|
||||
expect(saveExternalConnectorConfigSuccess).toHaveBeenCalled();
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/external');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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 { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
flashAPIErrors,
|
||||
flashSuccessToast,
|
||||
clearFlashMessages,
|
||||
} from '../../../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../../../shared/http';
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
|
||||
import { getAddPath, getSourcesPath } from '../../../../routes';
|
||||
|
||||
import { SourceConfigData } from './add_source_logic';
|
||||
|
||||
export interface ExternalConnectorActions {
|
||||
fetchExternalSource: () => true;
|
||||
fetchExternalSourceSuccess(sourceConfigData: SourceConfigData): SourceConfigData;
|
||||
saveExternalConnectorConfigSuccess(externalConnectorId: string): string;
|
||||
setExternalConnectorApiKey(externalConnectorApiKey: string): string;
|
||||
saveExternalConnectorConfig(config: ExternalConnectorConfig): ExternalConnectorConfig;
|
||||
setExternalConnectorUrl(externalConnectorUrl: string): string;
|
||||
resetSourceState: () => true;
|
||||
}
|
||||
|
||||
export interface ExternalConnectorConfig {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface ExternalConnectorValues {
|
||||
buttonLoading: boolean;
|
||||
dataLoading: boolean;
|
||||
externalConnectorApiKey: string;
|
||||
externalConnectorUrl: string;
|
||||
sourceConfigData: SourceConfigData | Pick<SourceConfigData, 'name' | 'categories'>;
|
||||
}
|
||||
|
||||
export const ExternalConnectorLogic = kea<
|
||||
MakeLogicType<ExternalConnectorValues, ExternalConnectorActions>
|
||||
>({
|
||||
path: ['enterprise_search', 'workplace_search', 'external_connector_logic'],
|
||||
actions: {
|
||||
fetchExternalSource: true,
|
||||
fetchExternalSourceSuccess: (sourceConfigData) => sourceConfigData,
|
||||
saveExternalConnectorConfigSuccess: (externalConnectorId) => externalConnectorId,
|
||||
saveExternalConnectorConfig: (config) => config,
|
||||
setExternalConnectorApiKey: (externalConnectorApiKey: string) => externalConnectorApiKey,
|
||||
setExternalConnectorUrl: (externalConnectorUrl: string) => externalConnectorUrl,
|
||||
},
|
||||
reducers: {
|
||||
dataLoading: [
|
||||
true,
|
||||
{
|
||||
fetchExternalSourceSuccess: () => false,
|
||||
},
|
||||
],
|
||||
buttonLoading: [
|
||||
false,
|
||||
{
|
||||
saveExternalConnectorConfigSuccess: () => false,
|
||||
saveExternalConnectorConfig: () => true,
|
||||
},
|
||||
],
|
||||
externalConnectorUrl: [
|
||||
'',
|
||||
{
|
||||
fetchExternalSourceSuccess: (_, { configuredFields: { url } }) => url || '',
|
||||
setExternalConnectorUrl: (_, url) => url,
|
||||
},
|
||||
],
|
||||
externalConnectorApiKey: [
|
||||
'',
|
||||
{
|
||||
fetchExternalSourceSuccess: (_, { configuredFields: { apiKey } }) => apiKey || '',
|
||||
setExternalConnectorApiKey: (_, apiKey) => apiKey,
|
||||
},
|
||||
],
|
||||
sourceConfigData: [
|
||||
{ name: '', categories: [] },
|
||||
{
|
||||
fetchExternalSourceSuccess: (_, sourceConfigData) => sourceConfigData,
|
||||
},
|
||||
],
|
||||
},
|
||||
listeners: ({ actions }) => ({
|
||||
fetchExternalSource: async () => {
|
||||
const route = '/internal/workplace_search/org/settings/connectors/external';
|
||||
|
||||
try {
|
||||
const response = await HttpLogic.values.http.get<SourceConfigData>(route);
|
||||
actions.fetchExternalSourceSuccess(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
saveExternalConnectorConfig: async () => {
|
||||
clearFlashMessages();
|
||||
// const route = '/internal/workplace_search/org/settings/connectors';
|
||||
// const http = HttpLogic.values.http.post;
|
||||
// const params = {
|
||||
// url,
|
||||
// api_key: apiKey,
|
||||
// service_type: 'external',
|
||||
// };
|
||||
try {
|
||||
// const response = await http<SourceConfigData>(route, {
|
||||
// body: JSON.stringify(params),
|
||||
// });
|
||||
|
||||
flashSuccessToast(
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.externalConnectorCreated',
|
||||
{
|
||||
defaultMessage: 'Successfully updated configuration.',
|
||||
}
|
||||
)
|
||||
);
|
||||
// TODO: use response data instead
|
||||
actions.saveExternalConnectorConfigSuccess('external');
|
||||
KibanaLogic.values.navigateToUrl(
|
||||
getSourcesPath(`${getAddPath('external')}`, AppLogic.values.isOrganization)
|
||||
);
|
||||
} catch (e) {
|
||||
// flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -61,7 +61,8 @@ export const GitHubViaApp: React.FC<GithubViaAppProps> = ({ isGithubEnterpriseSe
|
|||
|
||||
const { hasPlatinumLicense } = useValues(LicensingLogic);
|
||||
const name = isGithubEnterpriseServer ? SOURCE_NAMES.GITHUB_ENTERPRISE : SOURCE_NAMES.GITHUB;
|
||||
const data = staticSourceData.find((source) => source.name === name);
|
||||
const serviceType = isGithubEnterpriseServer ? 'github_enterprise_server' : 'github';
|
||||
const data = staticSourceData.find((source) => source.serviceType === serviceType);
|
||||
const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
|
|
|
@ -11,40 +11,45 @@ import React from 'react';
|
|||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiLink, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
|
||||
|
||||
import { LicenseBadge } from '../../../../components/shared/license_badge';
|
||||
import { staticCustomSourceData } from '../../source_data';
|
||||
|
||||
import { SaveCustom } from './save_custom';
|
||||
|
||||
describe('SaveCustom', () => {
|
||||
const props = {
|
||||
documentationUrl: 'http://string.boolean',
|
||||
const mockValues = {
|
||||
newCustomSource: {
|
||||
accessToken: 'dsgfsd',
|
||||
key: 'sdfs',
|
||||
name: 'source',
|
||||
id: '12e1',
|
||||
id: 'id',
|
||||
accessToken: 'token',
|
||||
name: 'name',
|
||||
},
|
||||
sourceData: staticCustomSourceData,
|
||||
isOrganization: true,
|
||||
header: <h1>Header</h1>,
|
||||
hasPlatinumLicense: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockValues(mockValues);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
setMockValues({ hasPlatinumLicense: true });
|
||||
const wrapper = shallow(<SaveCustom {...props} />);
|
||||
const wrapper = shallow(<SaveCustom />);
|
||||
|
||||
expect(wrapper.find(EuiPanel)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiTitle)).toHaveLength(4);
|
||||
expect(wrapper.find(EuiLinkTo)).toHaveLength(1);
|
||||
expect(wrapper.find(LicenseBadge)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders platinum LicenseBadge and link', () => {
|
||||
setMockValues({ hasPlatinumLicense: false });
|
||||
const wrapper = shallow(<SaveCustom {...props} />);
|
||||
it('renders platinum license badge if license is not present', () => {
|
||||
setMockValues({ ...mockValues, hasPlatinumLicense: false });
|
||||
const wrapper = shallow(<SaveCustom />);
|
||||
|
||||
expect(wrapper.find(LicenseBadge)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiLink)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiTitle)).toHaveLength(4);
|
||||
expect(wrapper.find(EuiLinkTo)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -27,6 +28,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { docLinks } from '../../../../../shared/doc_links';
|
||||
import { LicensingLogic } from '../../../../../shared/licensing';
|
||||
import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
import { LicenseBadge } from '../../../../components/shared/license_badge';
|
||||
import {
|
||||
SOURCES_PATH,
|
||||
|
@ -34,11 +36,12 @@ import {
|
|||
getContentSourcePath,
|
||||
getSourcesPath,
|
||||
} from '../../../../routes';
|
||||
import { CustomSource } from '../../../../types';
|
||||
import { LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants';
|
||||
|
||||
import { SourceIdentifier } from '../source_identifier';
|
||||
|
||||
import { AddCustomSourceLogic } from './add_custom_source_logic';
|
||||
import { AddSourceHeader } from './add_source_header';
|
||||
import {
|
||||
SAVE_CUSTOM_BODY1,
|
||||
SAVE_CUSTOM_BODY2,
|
||||
|
@ -51,23 +54,20 @@ import {
|
|||
SAVE_CUSTOM_DOC_PERMISSIONS_LINK,
|
||||
} from './constants';
|
||||
|
||||
interface SaveCustomProps {
|
||||
documentationUrl: string;
|
||||
newCustomSource: CustomSource;
|
||||
isOrganization: boolean;
|
||||
header: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SaveCustom: React.FC<SaveCustomProps> = ({
|
||||
documentationUrl,
|
||||
newCustomSource: { id, name },
|
||||
isOrganization,
|
||||
header,
|
||||
}) => {
|
||||
export const SaveCustom: React.FC = () => {
|
||||
const { newCustomSource, sourceData } = useValues(AddCustomSourceLogic);
|
||||
const { isOrganization } = useValues(AppLogic);
|
||||
const { hasPlatinumLicense } = useValues(LicensingLogic);
|
||||
const {
|
||||
serviceType,
|
||||
configuration: { githubRepository, documentationUrl },
|
||||
name,
|
||||
categories = [],
|
||||
} = sourceData;
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
<AddSourceHeader name={name} serviceType={serviceType} categories={categories} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow={2}>
|
||||
|
@ -84,7 +84,7 @@ export const SaveCustom: React.FC<SaveCustomProps> = ({
|
|||
'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading',
|
||||
{
|
||||
defaultMessage: '{name} Created',
|
||||
values: { name },
|
||||
values: { name: newCustomSource.name },
|
||||
}
|
||||
)}
|
||||
</h1>
|
||||
|
@ -93,7 +93,22 @@ export const SaveCustom: React.FC<SaveCustomProps> = ({
|
|||
<EuiText grow={false}>
|
||||
<EuiTextAlign textAlign="center">
|
||||
{SAVE_CUSTOM_BODY1}
|
||||
<br />
|
||||
<EuiSpacer size="s" />
|
||||
{serviceType !== 'custom' && githubRepository && (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.repositoryInstructions"
|
||||
defaultMessage="First you'll need to clone and deploy this repository"
|
||||
/>
|
||||
<br />
|
||||
<EuiCode>
|
||||
<EuiLinkTo to={`https://github.com/${githubRepository}`}>
|
||||
{githubRepository}
|
||||
</EuiLinkTo>
|
||||
</EuiCode>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
{SAVE_CUSTOM_BODY2}
|
||||
<br />
|
||||
<EuiLinkTo to={getSourcesPath(SOURCES_PATH, isOrganization)}>
|
||||
|
@ -105,7 +120,7 @@ export const SaveCustom: React.FC<SaveCustomProps> = ({
|
|||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer size="s" />
|
||||
<SourceIdentifier id={id} />
|
||||
<SourceIdentifier id={newCustomSource.id} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
|
@ -119,17 +134,32 @@ export const SaveCustom: React.FC<SaveCustomProps> = ({
|
|||
<EuiSpacer size="xs" />
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.documentation.text"
|
||||
defaultMessage="{link} to learn more about Custom API Sources."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink target="_blank" href={documentationUrl}>
|
||||
{SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{serviceType === 'custom' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.documentation.text"
|
||||
defaultMessage="{link} to learn more about Custom API Sources."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink target="_blank" href={documentationUrl}>
|
||||
{SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.namedSourceDocumentation.text"
|
||||
defaultMessage="{link} to learn more about deploying a {name} source."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink target="_blank" href={documentationUrl}>
|
||||
{SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
|
@ -149,7 +179,7 @@ export const SaveCustom: React.FC<SaveCustomProps> = ({
|
|||
<EuiLinkTo
|
||||
to={getContentSourcePath(
|
||||
SOURCE_DISPLAY_SETTINGS_PATH,
|
||||
id,
|
||||
newCustomSource.id,
|
||||
isOrganization
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -41,7 +41,7 @@ import {
|
|||
SAVE_CHANGES_BUTTON,
|
||||
REMOVE_BUTTON,
|
||||
} from '../../../constants';
|
||||
import { SourceDataItem } from '../../../types';
|
||||
import { getEditPath } from '../../../routes';
|
||||
import { handlePrivateKeyUpload } from '../../../utils';
|
||||
import { AddSourceLogic } from '../components/add_source/add_source_logic';
|
||||
import {
|
||||
|
@ -57,7 +57,6 @@ import {
|
|||
SYNC_DIAGNOSTICS_DESCRIPTION,
|
||||
SYNC_DIAGNOSTICS_BUTTON,
|
||||
} from '../constants';
|
||||
import { staticSourceData } from '../source_data';
|
||||
import { SourceLogic } from '../source_logic';
|
||||
|
||||
import { DownloadDiagnosticsButton } from './download_diagnostics_button';
|
||||
|
@ -96,8 +95,7 @@ export const SourceSettings: React.FC = () => {
|
|||
|
||||
const editPath = isGithubApp
|
||||
? undefined // undefined for GitHub apps, as they are configured source-wide, and don't use a connector where you can edit the configuration
|
||||
: (staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem)
|
||||
.editPath;
|
||||
: getEditPath(serviceType);
|
||||
|
||||
const [inputValue, setValue] = useState(name);
|
||||
const [confirmModalVisible, setModalVisibility] = useState(false);
|
||||
|
|
|
@ -10,52 +10,13 @@ import { i18n } from '@kbn/i18n';
|
|||
import { docLinks } from '../../../shared/doc_links';
|
||||
|
||||
import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants';
|
||||
import {
|
||||
ADD_BOX_PATH,
|
||||
ADD_CONFLUENCE_PATH,
|
||||
ADD_CONFLUENCE_SERVER_PATH,
|
||||
ADD_DROPBOX_PATH,
|
||||
ADD_GITHUB_ENTERPRISE_PATH,
|
||||
ADD_GITHUB_PATH,
|
||||
ADD_GMAIL_PATH,
|
||||
ADD_GOOGLE_DRIVE_PATH,
|
||||
ADD_JIRA_PATH,
|
||||
ADD_JIRA_SERVER_PATH,
|
||||
ADD_ONEDRIVE_PATH,
|
||||
ADD_SALESFORCE_PATH,
|
||||
ADD_SALESFORCE_SANDBOX_PATH,
|
||||
ADD_SERVICENOW_PATH,
|
||||
ADD_SHAREPOINT_PATH,
|
||||
ADD_SLACK_PATH,
|
||||
ADD_ZENDESK_PATH,
|
||||
ADD_CUSTOM_PATH,
|
||||
EDIT_BOX_PATH,
|
||||
EDIT_CONFLUENCE_PATH,
|
||||
EDIT_CONFLUENCE_SERVER_PATH,
|
||||
EDIT_DROPBOX_PATH,
|
||||
EDIT_GITHUB_ENTERPRISE_PATH,
|
||||
EDIT_GITHUB_PATH,
|
||||
EDIT_GMAIL_PATH,
|
||||
EDIT_GOOGLE_DRIVE_PATH,
|
||||
EDIT_JIRA_PATH,
|
||||
EDIT_JIRA_SERVER_PATH,
|
||||
EDIT_ONEDRIVE_PATH,
|
||||
EDIT_SALESFORCE_PATH,
|
||||
EDIT_SALESFORCE_SANDBOX_PATH,
|
||||
EDIT_SERVICENOW_PATH,
|
||||
EDIT_SHAREPOINT_PATH,
|
||||
EDIT_SLACK_PATH,
|
||||
EDIT_ZENDESK_PATH,
|
||||
EDIT_CUSTOM_PATH,
|
||||
} from '../../routes';
|
||||
import { FeatureIds, SourceDataItem } from '../../types';
|
||||
|
||||
export const staticSourceData = [
|
||||
export const staticSourceData: SourceDataItem[] = [
|
||||
{
|
||||
name: SOURCE_NAMES.BOX,
|
||||
iconName: SOURCE_NAMES.BOX,
|
||||
serviceType: 'box',
|
||||
addPath: ADD_BOX_PATH,
|
||||
editPath: EDIT_BOX_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -79,12 +40,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.CONFLUENCE,
|
||||
iconName: SOURCE_NAMES.CONFLUENCE,
|
||||
serviceType: 'confluence_cloud',
|
||||
addPath: ADD_CONFLUENCE_PATH,
|
||||
editPath: EDIT_CONFLUENCE_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -113,12 +74,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.CONFLUENCE_SERVER,
|
||||
iconName: SOURCE_NAMES.CONFLUENCE_SERVER,
|
||||
serviceType: 'confluence_server',
|
||||
addPath: ADD_CONFLUENCE_SERVER_PATH,
|
||||
editPath: EDIT_CONFLUENCE_SERVER_PATH,
|
||||
configuration: {
|
||||
isPublicKey: true,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -145,12 +106,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.DROPBOX,
|
||||
iconName: SOURCE_NAMES.DROPBOX,
|
||||
serviceType: 'dropbox',
|
||||
addPath: ADD_DROPBOX_PATH,
|
||||
editPath: EDIT_DROPBOX_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -174,12 +135,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.GITHUB,
|
||||
iconName: SOURCE_NAMES.GITHUB,
|
||||
serviceType: 'github',
|
||||
addPath: ADD_GITHUB_PATH,
|
||||
editPath: EDIT_GITHUB_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -210,12 +171,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.GITHUB_ENTERPRISE,
|
||||
iconName: SOURCE_NAMES.GITHUB_ENTERPRISE,
|
||||
serviceType: 'github_enterprise_server',
|
||||
addPath: ADD_GITHUB_ENTERPRISE_PATH,
|
||||
editPath: EDIT_GITHUB_ENTERPRISE_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -252,12 +213,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.GMAIL,
|
||||
iconName: SOURCE_NAMES.GMAIL,
|
||||
serviceType: 'gmail',
|
||||
addPath: ADD_GMAIL_PATH,
|
||||
editPath: EDIT_GMAIL_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -273,9 +234,8 @@ export const staticSourceData = [
|
|||
},
|
||||
{
|
||||
name: SOURCE_NAMES.GOOGLE_DRIVE,
|
||||
iconName: SOURCE_NAMES.GOOGLE_DRIVE,
|
||||
serviceType: 'google_drive',
|
||||
addPath: ADD_GOOGLE_DRIVE_PATH,
|
||||
editPath: EDIT_GOOGLE_DRIVE_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -303,12 +263,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.JIRA,
|
||||
iconName: SOURCE_NAMES.JIRA,
|
||||
serviceType: 'jira_cloud',
|
||||
addPath: ADD_JIRA_PATH,
|
||||
editPath: EDIT_JIRA_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -339,12 +299,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.JIRA_SERVER,
|
||||
iconName: SOURCE_NAMES.JIRA_SERVER,
|
||||
serviceType: 'jira_server',
|
||||
addPath: ADD_JIRA_SERVER_PATH,
|
||||
editPath: EDIT_JIRA_SERVER_PATH,
|
||||
configuration: {
|
||||
isPublicKey: true,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -374,12 +334,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.ONEDRIVE,
|
||||
iconName: SOURCE_NAMES.ONEDRIVE,
|
||||
serviceType: 'one_drive',
|
||||
addPath: ADD_ONEDRIVE_PATH,
|
||||
editPath: EDIT_ONEDRIVE_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -403,12 +363,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.SALESFORCE,
|
||||
iconName: SOURCE_NAMES.SALESFORCE,
|
||||
serviceType: 'salesforce',
|
||||
addPath: ADD_SALESFORCE_PATH,
|
||||
editPath: EDIT_SALESFORCE_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -439,12 +399,13 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: SOURCE_NAMES.SALESFORCE_SANDBOX,
|
||||
iconName: SOURCE_NAMES.SALESFORCE_SANDBOX,
|
||||
serviceType: 'salesforce_sandbox',
|
||||
addPath: ADD_SALESFORCE_SANDBOX_PATH,
|
||||
editPath: EDIT_SALESFORCE_SANDBOX_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -475,12 +436,12 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.SERVICENOW,
|
||||
iconName: SOURCE_NAMES.SERVICENOW,
|
||||
serviceType: 'service_now',
|
||||
addPath: ADD_SERVICENOW_PATH,
|
||||
editPath: EDIT_SERVICENOW_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: false,
|
||||
|
@ -508,12 +469,44 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.SHAREPOINT,
|
||||
iconName: SOURCE_NAMES.SHAREPOINT,
|
||||
serviceType: 'share_point',
|
||||
addPath: ADD_SHAREPOINT_PATH,
|
||||
editPath: EDIT_SHAREPOINT_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
needsBaseUrl: false,
|
||||
documentationUrl: docLinks.workplaceSearchSharePoint,
|
||||
applicationPortalUrl: 'https://portal.azure.com/',
|
||||
},
|
||||
objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.SITES, 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,
|
||||
internalConnectorAvailable: true,
|
||||
externalConnectorAvailable: true,
|
||||
},
|
||||
// TODO: temporary hack until backend sends us stuff
|
||||
{
|
||||
name: SOURCE_NAMES.SHAREPOINT,
|
||||
iconName: SOURCE_NAMES.SHAREPOINT,
|
||||
serviceType: 'external',
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -537,12 +530,54 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
externalConnectorAvailable: false,
|
||||
customConnectorAvailable: false,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.SHAREPOINT_SERVER,
|
||||
iconName: SOURCE_NAMES.SHAREPOINT_SERVER,
|
||||
categories: [
|
||||
i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.fileSharing', {
|
||||
defaultMessage: 'File Sharing',
|
||||
}),
|
||||
i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.storage', {
|
||||
defaultMessage: 'Storage',
|
||||
}),
|
||||
i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.cloud', {
|
||||
defaultMessage: 'Cloud',
|
||||
}),
|
||||
i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.microsoft', {
|
||||
defaultMessage: 'Microsoft',
|
||||
}),
|
||||
i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.categories.office', {
|
||||
defaultMessage: 'Office 365',
|
||||
}),
|
||||
],
|
||||
serviceType: 'share_point_server', // this doesn't exist on the BE
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: false,
|
||||
needsBaseUrl: false,
|
||||
// helpText: i18n.translate( // TODO updatae this
|
||||
// 'xpack.enterpriseSearch.workplaceSearch.sources.helpText.sharepointServer',
|
||||
// {
|
||||
// defaultMessage:
|
||||
// "Here is some help text. It should probably give the user a heads up that they're going to have to deploy some code.",
|
||||
// }
|
||||
// ),
|
||||
documentationUrl: docLinks.workplaceSearchCustomSources, // TODO update this
|
||||
applicationPortalUrl: '',
|
||||
githubRepository: 'elastic/enterprise-search-sharepoint-server-connector',
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: false,
|
||||
customConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.SLACK,
|
||||
iconName: SOURCE_NAMES.SLACK,
|
||||
serviceType: 'slack',
|
||||
addPath: ADD_SLACK_PATH,
|
||||
editPath: EDIT_SLACK_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -559,12 +594,13 @@ export const staticSourceData = [
|
|||
platinumPrivateContext: [FeatureIds.Remote, FeatureIds.Private, FeatureIds.SearchableContent],
|
||||
},
|
||||
accountContextOnly: true,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: SOURCE_NAMES.ZENDESK,
|
||||
iconName: SOURCE_NAMES.ZENDESK,
|
||||
serviceType: 'zendesk',
|
||||
addPath: ADD_ZENDESK_PATH,
|
||||
editPath: EDIT_ZENDESK_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: true,
|
||||
|
@ -588,23 +624,26 @@ export const staticSourceData = [
|
|||
],
|
||||
},
|
||||
accountContextOnly: false,
|
||||
internalConnectorAvailable: true,
|
||||
},
|
||||
{
|
||||
name: SOURCE_NAMES.CUSTOM,
|
||||
serviceType: 'custom',
|
||||
addPath: ADD_CUSTOM_PATH,
|
||||
editPath: EDIT_CUSTOM_PATH,
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: false,
|
||||
needsBaseUrl: false,
|
||||
helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', {
|
||||
defaultMessage:
|
||||
'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.',
|
||||
}),
|
||||
documentationUrl: docLinks.workplaceSearchCustomSources,
|
||||
applicationPortalUrl: '',
|
||||
},
|
||||
accountContextOnly: false,
|
||||
];
|
||||
|
||||
export const staticCustomSourceData: SourceDataItem = {
|
||||
name: SOURCE_NAMES.CUSTOM,
|
||||
iconName: SOURCE_NAMES.CUSTOM,
|
||||
categories: ['API', 'Custom'],
|
||||
serviceType: 'custom',
|
||||
configuration: {
|
||||
isPublicKey: false,
|
||||
hasOauthRedirect: false,
|
||||
needsBaseUrl: false,
|
||||
helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', {
|
||||
defaultMessage:
|
||||
'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.',
|
||||
}),
|
||||
documentationUrl: docLinks.workplaceSearchCustomSources,
|
||||
applicationPortalUrl: '',
|
||||
},
|
||||
] as SourceDataItem[];
|
||||
accountContextOnly: false,
|
||||
customConnectorAvailable: true,
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ jest.mock('../../app_logic', () => ({
|
|||
import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers';
|
||||
import { AppLogic } from '../../app_logic';
|
||||
|
||||
import { staticSourceData } from './source_data';
|
||||
import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic';
|
||||
|
||||
describe('SourcesLogic', () => {
|
||||
|
@ -32,8 +33,8 @@ describe('SourcesLogic', () => {
|
|||
const defaultValues = {
|
||||
contentSources: [],
|
||||
privateContentSources: [],
|
||||
sourceData: [],
|
||||
availableSources: [],
|
||||
sourceData: staticSourceData.map((data) => ({ ...data, connected: false })),
|
||||
availableSources: staticSourceData.map((data) => ({ ...data, connected: false })),
|
||||
configuredSources: [],
|
||||
serviceTypes: [],
|
||||
permissionsModal: null,
|
||||
|
@ -316,7 +317,7 @@ describe('SourcesLogic', () => {
|
|||
it('availableSources & configuredSources have correct length', () => {
|
||||
SourcesLogic.actions.onInitializeSources(serverResponse);
|
||||
|
||||
expect(SourcesLogic.values.availableSources).toHaveLength(1);
|
||||
expect(SourcesLogic.values.availableSources).toHaveLength(14);
|
||||
expect(SourcesLogic.values.configuredSources).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -178,7 +178,7 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
|
|||
if (isOrganization && !values.serverStatuses) {
|
||||
// We want to get the initial statuses from the server to compare our polling results to.
|
||||
const sourceStatuses = await fetchSourceStatuses(isOrganization, breakpoint);
|
||||
actions.setServerSourceStatuses(sourceStatuses);
|
||||
actions.setServerSourceStatuses(sourceStatuses ?? []);
|
||||
}
|
||||
},
|
||||
// We poll the server and if the status update, we trigger a new fetch of the sources.
|
||||
|
@ -190,7 +190,7 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
|
|||
pollingInterval = window.setInterval(async () => {
|
||||
const sourceStatuses = await fetchSourceStatuses(isOrganization, breakpoint);
|
||||
|
||||
sourceStatuses.some((source: ContentSourceStatus) => {
|
||||
(sourceStatuses ?? []).some((source: ContentSourceStatus) => {
|
||||
if (serverStatuses && serverStatuses[source.id] !== source.status.status) {
|
||||
return actions.initializeSources();
|
||||
}
|
||||
|
@ -249,7 +249,7 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
|
|||
export const fetchSourceStatuses = async (
|
||||
isOrganization: boolean,
|
||||
breakpoint: BreakPointFunction
|
||||
) => {
|
||||
): Promise<ContentSourceStatus[] | undefined> => {
|
||||
const route = isOrganization
|
||||
? '/internal/workplace_search/org/sources/status'
|
||||
: '/internal/workplace_search/account/sources/status';
|
||||
|
@ -267,8 +267,7 @@ export const fetchSourceStatuses = async (
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: remove casting. return type should be ContentSourceStatus[] | undefined
|
||||
return response as ContentSourceStatus[];
|
||||
return response;
|
||||
};
|
||||
|
||||
const updateSourcesOnToggle = (
|
||||
|
@ -293,7 +292,7 @@ const updateSourcesOnToggle = (
|
|||
* The second is the base list of available sources that the server sends back in the collection,
|
||||
* `availableTypes` that is the source of truth for the name and whether the source has been configured.
|
||||
*
|
||||
* Fnally, also in the collection response is the current set of connected sources. We check for the
|
||||
* Finally, also in the collection response is the current set of connected sources. We check for the
|
||||
* existence of a `connectedSource` of the type in the loop and set `connected` to true so that the UI
|
||||
* can diplay "Add New" instead of "Connect", the latter of which is displated only when a connector
|
||||
* has been configured but there are no connected sources yet.
|
||||
|
@ -304,13 +303,13 @@ export const mergeServerAndStaticData = (
|
|||
contentSources: ContentSourceDetails[]
|
||||
) => {
|
||||
const combined = [] as CombinedDataItem[];
|
||||
serverData.forEach((serverItem) => {
|
||||
const type = serverItem.serviceType;
|
||||
const staticItem = staticData.find(({ serviceType }) => serviceType === type);
|
||||
staticData.forEach((staticItem) => {
|
||||
const type = staticItem.serviceType;
|
||||
const serverItem = serverData.find(({ serviceType }) => serviceType === type);
|
||||
const connectedSource = contentSources.find(({ serviceType }) => serviceType === type);
|
||||
combined.push({
|
||||
...serverItem,
|
||||
...staticItem,
|
||||
...serverItem,
|
||||
connected: !!connectedSource,
|
||||
} as CombinedDataItem);
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('SourcesRouter', () => {
|
|||
});
|
||||
|
||||
it('renders sources routes', () => {
|
||||
const TOTAL_ROUTES = 63;
|
||||
const TOTAL_ROUTES = 86;
|
||||
const wrapper = shallow(<SourcesRouter />);
|
||||
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
|
@ -45,8 +45,8 @@ describe('SourcesRouter', () => {
|
|||
setMockValues({ ...mockValues, hasPlatinumLicense: false });
|
||||
const wrapper = shallow(<SourcesRouter />);
|
||||
|
||||
expect(wrapper.find(Redirect).first().prop('from')).toEqual(ADD_SOURCE_PATH);
|
||||
expect(wrapper.find(Redirect).first().prop('to')).toEqual(SOURCES_PATH);
|
||||
expect(wrapper.find(Redirect).last().prop('from')).toEqual(ADD_SOURCE_PATH);
|
||||
expect(wrapper.find(Redirect).last().prop('to')).toEqual(SOURCES_PATH);
|
||||
});
|
||||
|
||||
it('redirects when cannot create sources', () => {
|
||||
|
|
|
@ -14,19 +14,27 @@ import { useActions, useValues } from 'kea';
|
|||
import { LicensingLogic } from '../../../shared/licensing';
|
||||
import { AppLogic } from '../../app_logic';
|
||||
import {
|
||||
ADD_GITHUB_VIA_APP_PATH,
|
||||
ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH,
|
||||
GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE,
|
||||
GITHUB_VIA_APP_SERVICE_TYPE,
|
||||
} from '../../constants';
|
||||
import {
|
||||
ADD_SOURCE_PATH,
|
||||
SOURCE_DETAILS_PATH,
|
||||
PRIVATE_SOURCES_PATH,
|
||||
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 { ConfigurationChoice } from './components/add_source/configuration_choice';
|
||||
import { ExternalConnectorConfig } from './components/add_source/external_connector_config';
|
||||
import { OrganizationSources } from './organization_sources';
|
||||
import { PrivateSources } from './private_sources';
|
||||
import { staticSourceData } from './source_data';
|
||||
import { staticCustomSourceData, staticSourceData as sources } from './source_data';
|
||||
import { SourceRouter } from './source_router';
|
||||
import { SourcesLogic } from './sources_logic';
|
||||
|
||||
|
@ -68,36 +76,121 @@ export const SourcesRouter: React.FC = () => {
|
|||
<Route exact path={SOURCES_PATH}>
|
||||
<OrganizationSources />
|
||||
</Route>
|
||||
<Route exact path={ADD_GITHUB_VIA_APP_PATH}>
|
||||
<Route exact path={getAddPath(GITHUB_VIA_APP_SERVICE_TYPE)}>
|
||||
<GitHubViaApp isGithubEnterpriseServer={false} />
|
||||
</Route>
|
||||
<Route exact path={ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH}>
|
||||
<Route exact path={getAddPath(GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE)}>
|
||||
<GitHubViaApp isGithubEnterpriseServer />
|
||||
</Route>
|
||||
{staticSourceData.map(({ addPath, accountContextOnly }, i) => (
|
||||
<Route key={i} exact path={getSourcesPath(addPath, isOrganization)}>
|
||||
{!hasPlatinumLicense && accountContextOnly ? (
|
||||
<Redirect exact from={ADD_SOURCE_PATH} to={SOURCES_PATH} />
|
||||
) : (
|
||||
<AddSource sourceIndex={i} />
|
||||
)}
|
||||
</Route>
|
||||
))}
|
||||
{staticSourceData.map(({ addPath }, i) => (
|
||||
<Route key={i} exact path={`${getSourcesPath(addPath, isOrganization)}/connect`}>
|
||||
<AddSource connect sourceIndex={i} />
|
||||
</Route>
|
||||
))}
|
||||
{staticSourceData.map(({ addPath }, i) => (
|
||||
<Route key={i} exact path={`${getSourcesPath(addPath, isOrganization)}/reauthenticate`}>
|
||||
<AddSource reAuthenticate sourceIndex={i} />
|
||||
</Route>
|
||||
))}
|
||||
{staticSourceData.map(({ addPath, configuration: { needsConfiguration } }, i) => {
|
||||
if (needsConfiguration)
|
||||
{sources.map((sourceData, i) => {
|
||||
const { serviceType, externalConnectorAvailable, internalConnectorAvailable } = sourceData;
|
||||
const path = `${getSourcesPath(getAddPath(serviceType), isOrganization)}`;
|
||||
const defaultOption = internalConnectorAvailable
|
||||
? 'internal'
|
||||
: externalConnectorAvailable
|
||||
? 'external'
|
||||
: 'custom';
|
||||
return (
|
||||
<Route key={i} exact path={path}>
|
||||
{hasMultipleConnectorOptions(sourceData) ? (
|
||||
<ConfigurationChoice sourceData={sourceData} />
|
||||
) : (
|
||||
<Redirect exact from={path} to={`${path}/${defaultOption}`} />
|
||||
)}
|
||||
</Route>
|
||||
);
|
||||
})}
|
||||
<Route exact path={getSourcesPath(ADD_CUSTOM_PATH, isOrganization)}>
|
||||
<AddCustomSource sourceData={staticCustomSourceData} />
|
||||
</Route>
|
||||
{sources
|
||||
.filter((sourceData) => sourceData.internalConnectorAvailable)
|
||||
.map((sourceData, i) => {
|
||||
const { serviceType, accountContextOnly } = sourceData;
|
||||
return (
|
||||
<Route key={i} exact path={`${getSourcesPath(addPath, isOrganization)}/configure`}>
|
||||
<AddSource configure sourceIndex={i} />
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -33,9 +33,7 @@ import {
|
|||
PRIVATE_SOURCE,
|
||||
UPDATE_BUTTON,
|
||||
} from '../../../constants';
|
||||
import { getSourcesPath } from '../../../routes';
|
||||
import { SourceDataItem } from '../../../types';
|
||||
import { staticSourceData } from '../../content_sources/source_data';
|
||||
import { getAddPath, getEditPath, getSourcesPath } from '../../../routes';
|
||||
import { SettingsLogic } from '../settings_logic';
|
||||
|
||||
export const Connectors: React.FC = () => {
|
||||
|
@ -52,9 +50,9 @@ export const Connectors: React.FC = () => {
|
|||
);
|
||||
|
||||
const getRowActions = (configured: boolean, serviceType: string, supportedByLicense: boolean) => {
|
||||
const { addPath, editPath } = staticSourceData.find(
|
||||
(s) => s.serviceType === serviceType
|
||||
) as SourceDataItem;
|
||||
const addPath = getAddPath(serviceType);
|
||||
const editPath = getEditPath(serviceType);
|
||||
|
||||
const configurePath = getSourcesPath(addPath, true);
|
||||
|
||||
const updateButtons = (
|
||||
|
|
|
@ -18,6 +18,8 @@ import { 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', () => {
|
||||
|
@ -31,7 +33,7 @@ describe('SourceConfig', () => {
|
|||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<SourceConfig sourceIndex={1} />);
|
||||
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
|
||||
const saveConfig = wrapper.find(SaveConfig);
|
||||
|
||||
// Trigger modal visibility
|
||||
|
@ -42,13 +44,13 @@ describe('SourceConfig', () => {
|
|||
|
||||
it('renders a breadcrumb fallback while data is loading', () => {
|
||||
setMockValues({ dataLoading: true, sourceConfigData: {} });
|
||||
const wrapper = shallow(<SourceConfig sourceIndex={1} />);
|
||||
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
|
||||
|
||||
expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']);
|
||||
});
|
||||
|
||||
it('handles delete click', () => {
|
||||
const wrapper = shallow(<SourceConfig sourceIndex={1} />);
|
||||
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
|
||||
const saveConfig = wrapper.find(SaveConfig);
|
||||
|
||||
// Trigger modal visibility
|
||||
|
@ -60,7 +62,7 @@ describe('SourceConfig', () => {
|
|||
});
|
||||
|
||||
it('saves source config', () => {
|
||||
const wrapper = shallow(<SourceConfig sourceIndex={1} />);
|
||||
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
|
||||
const saveConfig = wrapper.find(SaveConfig);
|
||||
|
||||
// Trigger modal visibility
|
||||
|
@ -72,7 +74,7 @@ describe('SourceConfig', () => {
|
|||
});
|
||||
|
||||
it('cancels and closes modal', () => {
|
||||
const wrapper = shallow(<SourceConfig sourceIndex={1} />);
|
||||
const wrapper = shallow(<SourceConfig sourceData={staticSourceData[1]} />);
|
||||
const saveConfig = wrapper.find(SaveConfig);
|
||||
|
||||
// Trigger modal visibility
|
||||
|
|
|
@ -18,16 +18,15 @@ 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 { staticSourceData } from '../../content_sources/source_data';
|
||||
import { SettingsLogic } from '../settings_logic';
|
||||
|
||||
interface SourceConfigProps {
|
||||
sourceIndex: number;
|
||||
sourceData: SourceDataItem;
|
||||
}
|
||||
|
||||
export const SourceConfig: React.FC<SourceConfigProps> = ({ sourceIndex }) => {
|
||||
export const SourceConfig: React.FC<SourceConfigProps> = ({ sourceData }) => {
|
||||
const [confirmModalVisible, setConfirmModalVisibility] = useState(false);
|
||||
const { configuration, serviceType } = staticSourceData[sourceIndex] as SourceDataItem;
|
||||
const { configuration, serviceType } = sourceData;
|
||||
const { deleteSourceConfig } = useActions(SettingsLogic);
|
||||
const { saveSourceConfig, getSourceConfigData } = useActions(AddSourceLogic);
|
||||
const {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
ORG_SETTINGS_CUSTOMIZE_PATH,
|
||||
ORG_SETTINGS_CONNECTORS_PATH,
|
||||
ORG_SETTINGS_OAUTH_APPLICATION_PATH,
|
||||
getEditPath,
|
||||
} from '../../routes';
|
||||
import { staticSourceData } from '../content_sources/source_data';
|
||||
|
||||
|
@ -41,9 +42,9 @@ export const SettingsRouter: React.FC = () => {
|
|||
<Route exact path={ORG_SETTINGS_OAUTH_APPLICATION_PATH}>
|
||||
<OauthApplication />
|
||||
</Route>
|
||||
{staticSourceData.map(({ editPath }, i) => (
|
||||
<Route key={i} exact path={editPath}>
|
||||
<SourceConfig sourceIndex={i} />
|
||||
{staticSourceData.map((sourceData, i) => (
|
||||
<Route key={i} exact path={getEditPath(sourceData.serviceType)}>
|
||||
<SourceConfig sourceData={sourceData} />
|
||||
</Route>
|
||||
))}
|
||||
<Route>
|
||||
|
|
|
@ -38,6 +38,14 @@ const oauthConfigSchema = schema.object({
|
|||
consumer_key: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
const externalConnectorSchema = schema.object({
|
||||
url: schema.string(),
|
||||
api_key: schema.string(),
|
||||
service_type: schema.string(),
|
||||
});
|
||||
|
||||
const postConnectorSchema = schema.oneOf([externalConnectorSchema, oauthConfigSchema]);
|
||||
|
||||
const displayFieldSchema = schema.object({
|
||||
fieldName: schema.string(),
|
||||
label: schema.string(),
|
||||
|
@ -872,7 +880,7 @@ export function registerOrgSourceOauthConfigurationsRoute({
|
|||
{
|
||||
path: '/internal/workplace_search/org/settings/connectors',
|
||||
validate: {
|
||||
body: oauthConfigSchema,
|
||||
body: postConnectorSchema,
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue