From 6b40877dab2c94109dac9ec9841034fa6dcd9b54 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Mon, 6 Dec 2021 14:24:59 -0800 Subject: [PATCH] [Workplace Search] Add a technical preview of connecting GitHub via GitHub apps (#119764) * Add new routes Render the new github_via_app component on the new routes (added in future commits). Use isGithubEnterpriseServer prop as differentiator between github and github enterprise server * Add readUploadedFileAsText * Add new view and logic for creating a GitHub content source via GitHub apps Also rename github_app to github_via_app to match service_type on backend * Make editPath (path to connector settings) optional as it is not available for GitHub apps which are configured on a source level * Update source_settings and source_logic to include configuration for new source types. Add new "secret" field to ContentSourceFullData and mocks * Rename indexPermissions to index_permissions * Extract handlePrivateKeyUpload into a utility function * Extract `github_via_app` and `github_enterprise_server_via_app` to constants * Add a basic validation: submit button is disabled if fields are empty * Address PR feedback * Do not rely on baseUrl field emptyness to define the service type Rely on explicit parameter instead * Add icons to the new GitHub service types * Fix a bug where indexPermissionsValue was true even on basic license. The solution copied from the add_source component. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__mocks__/content_sources.mock.ts | 5 + .../shared/assets/source_icons/index.ts | 2 + .../workplace_search/constants.ts | 3 + .../applications/workplace_search/routes.ts | 8 +- .../applications/workplace_search/types.ts | 9 +- .../utils/handle_private_key_upload.ts | 21 +++ .../workplace_search/utils/index.ts | 2 + .../utils/read_uploaded_file_as_text.ts | 22 +++ .../components/add_source/add_source_logic.ts | 2 +- .../components/add_source/github_app.tsx | 64 --------- .../components/add_source/github_via_app.tsx | 128 ++++++++++++++++++ .../add_source/github_via_app_logic.ts | 116 ++++++++++++++++ .../components/add_source/index.ts | 2 +- .../components/source_settings.tsx | 77 +++++++++-- .../content_sources/source_logic.test.ts | 2 + .../views/content_sources/source_logic.ts | 52 +++++++ .../content_sources/sources_router.test.tsx | 2 +- .../views/content_sources/sources_router.tsx | 12 +- .../views/settings/components/connectors.tsx | 2 +- .../routes/workplace_search/sources.test.ts | 4 +- .../server/routes/workplace_search/sources.ts | 8 +- 21 files changed, 456 insertions(+), 87 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 85ffde0acfea3..7af40b23d9f64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -123,6 +123,11 @@ export const fullContentSources = [ urlFieldIsLinkable: true, createdAt: '2021-01-20', serviceName: 'myService', + secret: { + app_id: '99999', + fingerprint: '65xM7s0RE6tEWNhnuXpK5EvZ5OAMIcbDHIISm/0T23Y=', + base_url: 'http://github.com', + }, }, { ...contentSources[1], diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts index e6a994d05f3ff..fdccd536c3c6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts @@ -31,6 +31,8 @@ export const images = { dropbox, github, githubEnterpriseServer: github, + githubViaApp: github, + githubEnterpriseServerViaApp: github, gmail, googleDrive, jira, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 43da4ccef223a..5b364f814083d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -336,6 +336,9 @@ export const GITHUB_LINK_TITLE = i18n.translate( } ); +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 WORKPLACE_SEARCH_URL_PREFIX = '/app/enterprise_search/workplace_search'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 5f3c79f9432e7..91ca24e8a56fd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -9,6 +9,11 @@ import { generatePath } from 'react-router-dom'; import { docLinks } from '../shared/doc_links'; +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'; @@ -70,7 +75,8 @@ 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_APP_PATH = `${SOURCES_PATH}/add/github_app`; +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`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 0fa8c00409d1a..20545d0842d10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -81,7 +81,7 @@ export interface SourceDataItem { features?: Features; objTypes?: string[]; addPath: string; - editPath: 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; } @@ -181,6 +181,12 @@ export interface IndexingConfig { schedule: IndexingSchedule; } +interface AppSecret { + app_id: string; + fingerprint: string; + base_url?: string; +} + export interface ContentSourceFullData extends ContentSourceDetails { activities: SourceActivity[]; details: DescriptionList[]; @@ -201,6 +207,7 @@ export interface ContentSourceFullData extends ContentSourceDetails { urlFieldIsLinkable: boolean; createdAt: string; serviceName: string; + secret?: AppSecret; // undefined for all content sources except GitHub apps } export interface ContentSourceStatus { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts new file mode 100644 index 0000000000000..b1a8877c165e0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts @@ -0,0 +1,21 @@ +/* + * 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 { readUploadedFileAsText } from './read_uploaded_file_as_text'; + +export const handlePrivateKeyUpload = async ( + files: FileList | null, + callback: (text: string) => void +) => { + if (!files || files.length < 1) { + return null; + } + const file = files[0]; + const text = await readUploadedFileAsText(file); + + callback(text); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts index fb9846dbccde8..92f27500d7262 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts @@ -9,3 +9,5 @@ export { toSentenceSerial } from './to_sentence_serial'; export { getAsLocalDateTimeString } from './get_as_local_datetime_string'; 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'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts new file mode 100644 index 0000000000000..c4e8e54057545 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts @@ -0,0 +1,22 @@ +/* + * 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 const readUploadedFileAsText = (fileInput: File): Promise => { + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = () => { + resolve(reader.result as string); + }; + try { + reader.readAsText(fileInput); + } catch { + reader.abort(); + reject(new Error()); + } + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 05a5fd5a73fe8..6dbac2dcd1452 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -571,7 +571,7 @@ export const AddSourceLogic = kea 0 ? githubOrganizations : undefined, - indexPermissions: indexPermissionsValue || undefined, + index_permissions: indexPermissionsValue || undefined, } as { [key: string]: string | string[] | undefined; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx deleted file mode 100644 index 7f518d272d842..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useValues } from 'kea'; - -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; - -import { AppLogic } from '../../../../app_logic'; -import { - WorkplaceSearchPageTemplate, - PersonalDashboardLayout, -} from '../../../../components/layout'; -import { NAV, SOURCE_NAMES } from '../../../../constants'; - -import { staticSourceData } from '../../source_data'; - -import { AddSourceHeader } from './add_source_header'; -import { SourceFeatures } from './source_features'; - -export const GitHubApp: React.FC = () => { - const { isOrganization } = useValues(AppLogic); - - const name = SOURCE_NAMES.GITHUB; - const data = staticSourceData.find((source) => (source.name = name)); - const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; - - return ( - -
'TODO: use method from add_source_logic'}> - - - - - - - - - - - - - form goes here - - -
-
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx new file mode 100644 index 0000000000000..a08f49b8bbe78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx @@ -0,0 +1,128 @@ +/* + * 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, { useEffect } from 'react'; +import type { FormEvent } from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + EuiFieldText, + EuiFormRow, + EuiFilePicker, + EuiButton, +} from '@elastic/eui'; + +import { LicensingLogic } from '../../../../../shared/licensing'; +import { AppLogic } from '../../../../app_logic'; + +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { NAV, SOURCE_NAMES } from '../../../../constants'; +import { handlePrivateKeyUpload } from '../../../../utils'; + +import { staticSourceData } from '../../source_data'; + +import { AddSourceHeader } from './add_source_header'; +import { DocumentPermissionsCallout } from './document_permissions_callout'; +import { DocumentPermissionsField } from './document_permissions_field'; +import { GithubViaAppLogic } from './github_via_app_logic'; +import { SourceFeatures } from './source_features'; + +interface GithubViaAppProps { + isGithubEnterpriseServer: boolean; +} + +export const GitHubViaApp: React.FC = ({ isGithubEnterpriseServer }) => { + const { isOrganization } = useValues(AppLogic); + const { githubAppId, githubEnterpriseServerUrl, isSubmitButtonLoading, indexPermissionsValue } = + useValues(GithubViaAppLogic); + const { + setGithubAppId, + setGithubEnterpriseServerUrl, + setStagedPrivateKey, + createContentSource, + setSourceIndexPermissionsValue, + } = useActions(GithubViaAppLogic); + + const { hasPlatinumLicense } = useValues(LicensingLogic); + const name = isGithubEnterpriseServer ? SOURCE_NAMES.GITHUB_ENTERPRISE : SOURCE_NAMES.GITHUB; + const data = staticSourceData.find((source) => source.name === name); + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + createContentSource(isGithubEnterpriseServer); + }; + + // Default indexPermissions to true, if needed + useEffect(() => { + setSourceIndexPermissionsValue(isOrganization && hasPlatinumLicense); + }, []); + + return ( + +
+ + + + + + + + + + + + + {!hasPlatinumLicense && } + {hasPlatinumLicense && isOrganization && ( + + )} + + + setGithubAppId(e.target.value)} /> + + {isGithubEnterpriseServer && ( + + setGithubEnterpriseServerUrl(e.target.value)} + /> + + )} + + handlePrivateKeyUpload(files, setStagedPrivateKey)} + accept=".pem" + /> + + + {isSubmitButtonLoading ? 'Connecting…' : `Connect ${name}`} + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.ts new file mode 100644 index 0000000000000..e779d53b6a1eb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.ts @@ -0,0 +1,116 @@ +/* + * 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, flashSuccessToast } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { AppLogic } from '../../../../app_logic'; +import { + GITHUB_VIA_APP_SERVICE_TYPE, + GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, +} from '../../../../constants'; +import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; +import { ContentSourceFullData } from '../../../../types'; + +interface GithubViaAppValues { + githubAppId: string; + githubEnterpriseServerUrl: string; + stagedPrivateKey: string | null; + isSubmitButtonLoading: boolean; + indexPermissionsValue: boolean; +} + +interface GithubViaAppActions { + setGithubAppId(githubAppId: string): string; + setGithubEnterpriseServerUrl(githubEnterpriseServerUrl: string): string; + setStagedPrivateKey(stagedPrivateKey: string | null): string | null; + setButtonNotLoading(): void; + createContentSource(isGithubEnterpriseServer: boolean): boolean; + setSourceIndexPermissionsValue(indexPermissionsValue: boolean): boolean; +} + +export const GithubViaAppLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'github_via_app_logic'], + actions: { + setGithubAppId: (githubAppId: string) => githubAppId, + setGithubEnterpriseServerUrl: (githubEnterpriseServerUrl: string) => githubEnterpriseServerUrl, + createContentSource: (isGithubEnterpriseServer: boolean) => isGithubEnterpriseServer, + setStagedPrivateKey: (stagedPrivateKey: string) => stagedPrivateKey, + setButtonNotLoading: false, + setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue, + }, + reducers: { + githubAppId: [ + '', + { + setGithubAppId: (_, githubAppId) => githubAppId, + }, + ], + githubEnterpriseServerUrl: [ + '', + { + setGithubEnterpriseServerUrl: (_, githubEnterpriseServerUrl) => githubEnterpriseServerUrl, + }, + ], + stagedPrivateKey: [ + null, + { + setStagedPrivateKey: (_, stagedPrivateKey) => stagedPrivateKey, + }, + ], + isSubmitButtonLoading: [ + false, + { + createContentSource: () => true, + setButtonNotLoading: () => false, + }, + ], + indexPermissionsValue: [ + true, + { + setSourceIndexPermissionsValue: (_, indexPermissionsValue) => indexPermissionsValue, + resetSourceState: () => false, + }, + ], + }, + listeners: ({ actions, values }) => ({ + createContentSource: async (isGithubEnterpriseServer) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? '/internal/workplace_search/org/create_source' + : '/internal/workplace_search/account/create_source'; + + const { githubAppId, githubEnterpriseServerUrl, stagedPrivateKey, indexPermissionsValue } = + values; + + const params = { + service_type: isGithubEnterpriseServer + ? GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE + : GITHUB_VIA_APP_SERVICE_TYPE, + app_id: githubAppId, + base_url: githubEnterpriseServerUrl, + private_key: stagedPrivateKey, + index_permissions: indexPermissionsValue, + }; + + try { + const response = await HttpLogic.values.http.post(route, { + body: JSON.stringify({ ...params }), + }); + + KibanaLogic.values.navigateToUrl(`${getSourcesPath(SOURCES_PATH, isOrganization)}`); + flashSuccessToast(`${response.serviceName} connected`); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setButtonNotLoading(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts index 033cf9f356342..8daa71672d203 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts @@ -7,4 +7,4 @@ export { AddSource } from './add_source'; export { AddSourceList } from './add_source_list'; -export { GitHubApp } from './github_app'; +export { GitHubViaApp } from './github_via_app'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index f0ccfb201e3b3..e5924b672c771 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -17,6 +17,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiForm, + EuiSpacer, + EuiFilePicker, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -26,7 +29,11 @@ import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { NAV } from '../../../constants'; +import { + NAV, + GITHUB_VIA_APP_SERVICE_TYPE, + GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, +} from '../../../constants'; import { CANCEL_BUTTON, @@ -36,6 +43,7 @@ import { REMOVE_BUTTON, } from '../../../constants'; import { SourceDataItem } from '../../../types'; +import { handlePrivateKeyUpload } from '../../../utils'; import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { SOURCE_SETTINGS_HEADING, @@ -58,12 +66,19 @@ import { SourceLayout } from './source_layout'; export const SourceSettings: React.FC = () => { const { http } = useValues(HttpLogic); - const { updateContentSource, removeContentSource } = useActions(SourceLogic); + const { + updateContentSource, + removeContentSource, + setStagedPrivateKey, + updateContentSourceConfiguration, + } = useActions(SourceLogic); const { getSourceConfigData } = useActions(AddSourceLogic); const { - contentSource: { name, id, serviceType, isOauth1 }, + contentSource: { name, id, serviceType, isOauth1, secret }, buttonLoading, + stagedPrivateKey, + isConfigurationUpdateButtonLoading, } = useValues(SourceLogic); const { @@ -76,16 +91,22 @@ export const SourceSettings: React.FC = () => { getSourceConfigData(serviceType); }, []); - const { editPath } = staticSourceData.find( - (source) => source.serviceType === serviceType - ) as SourceDataItem; + const isGithubApp = + serviceType === GITHUB_VIA_APP_SERVICE_TYPE || + serviceType === GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE; + + 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; const [inputValue, setValue] = useState(name); const [confirmModalVisible, setModalVisibility] = useState(false); const showConfirm = () => setModalVisibility(true); const hideConfirm = () => setModalVisibility(false); - const showConfig = isOrganization && !isEmpty(configuredFields); + const showOauthConfig = !isGithubApp && isOrganization && !isEmpty(configuredFields); + const showGithubAppConfig = isGithubApp; const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; @@ -102,6 +123,11 @@ export const SourceSettings: React.FC = () => { updateContentSource(id, { name: inputValue }); }; + const submitConfigurationChange = (e: FormEvent) => { + e.preventDefault(); + updateContentSourceConfiguration(id, { private_key: stagedPrivateKey }); + }; + const handleSourceRemoval = () => { /** * The modal was just hanging while the UI waited for the server to respond. @@ -164,7 +190,7 @@ export const SourceSettings: React.FC = () => { - {showConfig && ( + {showOauthConfig && ( { baseUrl={baseUrl} /> - + {SOURCE_CONFIG_LINK} )} + {showGithubAppConfig && ( + + + +
{secret!.app_id}
+
+ {secret!.base_url && ( + +
{secret!.base_url}
+
+ )} + + <> +
SHA256:{secret!.fingerprint}
+ + handlePrivateKeyUpload(files, setStagedPrivateKey)} + initialPromptText="Upload a new .pem file to rotate the private key" + accept=".pem" + /> + +
+ + {isConfigurationUpdateButtonLoading ? 'Loading…' : 'Save'} + +
+
+ )} { buttonLoading: false, contentMeta: DEFAULT_META, contentFilterValue: '', + isConfigurationUpdateButtonLoading: false, + stagedPrivateKey: null, }; const searchServerResponse = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index e97d48889d809..b76627f57b3a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -39,10 +39,19 @@ export interface SourceActions { sourceId: string; source: ContentSourceFullData; }; + updateContentSourceConfiguration( + sourceId: string, + source: SourceUpdatePayload + ): { + sourceId: string; + source: ContentSourceFullData; + }; resetSourceState(): void; removeContentSource(sourceId: string): { sourceId: string }; initializeSource(sourceId: string): { sourceId: string }; setButtonNotLoading(): void; + setStagedPrivateKey(stagedPrivateKey: string | null): string | null; + setConfigurationUpdateButtonNotLoading(): void; } interface SourceValues { @@ -53,6 +62,8 @@ interface SourceValues { contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; + stagedPrivateKey: string | null; + isConfigurationUpdateButtonLoading: boolean; } interface SearchResultsResponse { @@ -62,6 +73,7 @@ interface SearchResultsResponse { interface SourceUpdatePayload { name?: string; + private_key?: string | null; indexing?: { enabled?: boolean; features?: { @@ -85,11 +97,17 @@ export const SourceLogic = kea>({ initializeSourceSynchronization: (sourceId: string) => ({ sourceId }), searchContentSourceDocuments: (sourceId: string) => ({ sourceId }), updateContentSource: (sourceId: string, source: SourceUpdatePayload) => ({ sourceId, source }), + updateContentSourceConfiguration: (sourceId: string, source: SourceUpdatePayload) => ({ + sourceId, + source, + }), removeContentSource: (sourceId: string) => ({ sourceId, }), resetSourceState: () => true, setButtonNotLoading: () => false, + setStagedPrivateKey: (stagedPrivateKey: string) => stagedPrivateKey, + setConfigurationUpdateButtonNotLoading: () => false, }, reducers: { contentSource: [ @@ -150,6 +168,20 @@ export const SourceLogic = kea>({ resetSourceState: () => '', }, ], + stagedPrivateKey: [ + null, + { + setStagedPrivateKey: (_, stagedPrivateKey) => stagedPrivateKey, + setContentSource: () => null, + }, + ], + isConfigurationUpdateButtonLoading: [ + false, + { + updateContentSourceConfiguration: () => true, + setConfigurationUpdateButtonNotLoading: () => false, + }, + ], }, listeners: ({ actions, values }) => ({ initializeSource: async ({ sourceId }) => { @@ -233,6 +265,26 @@ export const SourceLogic = kea>({ flashAPIErrors(e); } }, + updateContentSourceConfiguration: async ({ sourceId, source }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/internal/workplace_search/org/sources/${sourceId}/settings` + : `/internal/workplace_search/account/sources/${sourceId}/settings`; + + try { + const response = await HttpLogic.values.http.patch(route, { + body: JSON.stringify({ content_source: source }), + }); + + actions.setContentSource(response); + + flashSuccessToast('Content source configuration was updated.'); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setConfigurationUpdateButtonNotLoading(); + } + }, removeContentSource: async ({ sourceId }) => { clearFlashMessages(); const { isOrganization } = AppLogic.values; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx index bcf2b2792c5d5..cf5dc48682ae8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx @@ -34,7 +34,7 @@ describe('SourcesRouter', () => { }); it('renders sources routes', () => { - const TOTAL_ROUTES = 62; + const TOTAL_ROUTES = 63; const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index 5142f5d6597ae..23109506b364e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -14,7 +14,8 @@ import { useActions, useValues } from 'kea'; import { LicensingLogic } from '../../../shared/licensing'; import { AppLogic } from '../../app_logic'; import { - ADD_GITHUB_APP_PATH, + ADD_GITHUB_VIA_APP_PATH, + ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH, ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, PRIVATE_SOURCES_PATH, @@ -22,7 +23,7 @@ import { getSourcesPath, } from '../../routes'; -import { AddSource, AddSourceList, GitHubApp } from './components/add_source'; +import { AddSource, AddSourceList, GitHubViaApp } from './components/add_source'; import { OrganizationSources } from './organization_sources'; import { PrivateSources } from './private_sources'; import { staticSourceData } from './source_data'; @@ -67,8 +68,11 @@ export const SourcesRouter: React.FC = () => { - - + + + + + {staticSourceData.map(({ addPath, accountContextOnly }, i) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx index f86b390f99ceb..85f91f769cc77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx @@ -60,7 +60,7 @@ export const Connectors: React.FC = () => { const updateButtons = ( - + {UPDATE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 961635c3f9001..3702298e8bcae 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -180,7 +180,7 @@ describe('sources routes', () => { login: 'user', password: 'changeme', organizations: ['swiftype'], - indexPermissions: true, + index_permissions: true, }, }; mockRouter.shouldValidate(request); @@ -688,7 +688,7 @@ describe('sources routes', () => { login: 'user', password: 'changeme', organizations: ['swiftype'], - indexPermissions: true, + index_permissions: true, }, }; mockRouter.shouldValidate(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 011fe341d6edf..12f4844461409 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -60,6 +60,7 @@ const displaySettingsSchema = schema.object({ const sourceSettingsSchema = schema.object({ content_source: schema.object({ name: schema.maybe(schema.string()), + private_key: schema.maybe(schema.nullable(schema.string())), indexing: schema.maybe( schema.object({ enabled: schema.maybe(schema.boolean()), @@ -178,7 +179,7 @@ export function registerAccountCreateSourceRoute({ login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), organizations: schema.maybe(schema.arrayOf(schema.string())), - indexPermissions: schema.maybe(schema.boolean()), + index_permissions: schema.maybe(schema.boolean()), }), }, }, @@ -522,7 +523,10 @@ export function registerOrgCreateSourceRoute({ login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), organizations: schema.maybe(schema.arrayOf(schema.string())), - indexPermissions: schema.maybe(schema.boolean()), + index_permissions: schema.maybe(schema.boolean()), + app_id: schema.maybe(schema.string()), + base_url: schema.maybe(schema.string()), + private_key: schema.nullable(schema.maybe(schema.string())), }), }, },