Skip to content

Commit

Permalink
[Workplace Search] Add a technical preview of connecting GitHub via G…
Browse files Browse the repository at this point in the history
…itHub 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 <[email protected]>
  • Loading branch information
yakhinvadim and kibanamachine authored Dec 6, 2021
1 parent 6a311d0 commit 60f463a
Show file tree
Hide file tree
Showing 21 changed files with 456 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:',
},
},
{
...contentSources[1],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const images = {
dropbox,
github,
githubEnterpriseServer: github,
githubViaApp: github,
githubEnterpriseServerViaApp: github,
gmail,
googleDrive,
jira,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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[];
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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<string> => {
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());
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export const AddSourceLogic = kea<MakeLogicType<AddSourceValues, AddSourceAction
login: loginValue || undefined,
password: passwordValue || undefined,
organizations: githubOrganizations.length > 0 ? githubOrganizations : undefined,
indexPermissions: indexPermissionsValue || undefined,
index_permissions: indexPermissionsValue || undefined,
} as {
[key: string]: string | string[] | undefined;
};
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<GithubViaAppProps> = ({ 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 (
<Layout pageChrome={[NAV.SOURCES, NAV.ADD_SOURCE, name || '...']} isLoading={false}>
<form onSubmit={handleSubmit}>
<EuiPanel paddingSize="none" hasShadow={false} color="subdued">
<EuiPanel hasShadow={false} paddingSize="l" color="subdued">
<AddSourceHeader
name={name}
serviceType="github"
categories={['Software', 'Version Control', 'Code Repository']} // TODO: get from API
/>
</EuiPanel>
<EuiHorizontalRule margin="xs" />
<EuiPanel hasShadow={false} paddingSize="l" color="subdued">
<SourceFeatures features={data!.features} name={name} objTypes={data!.objTypes} />
</EuiPanel>
</EuiPanel>

<EuiSpacer />

{!hasPlatinumLicense && <DocumentPermissionsCallout />}
{hasPlatinumLicense && isOrganization && (
<DocumentPermissionsField
needsPermissions
indexPermissionsValue={indexPermissionsValue}
setValue={setSourceIndexPermissionsValue}
/>
)}

<EuiFormRow label="GitHub App ID">
<EuiFieldText value={githubAppId} onChange={(e) => setGithubAppId(e.target.value)} />
</EuiFormRow>
{isGithubEnterpriseServer && (
<EuiFormRow label="Base URL">
<EuiFieldText
value={githubEnterpriseServerUrl}
onChange={(e) => setGithubEnterpriseServerUrl(e.target.value)}
/>
</EuiFormRow>
)}
<EuiFormRow label="Private key" helpText="Upload private key (.pem) to authenticate GitHub">
<EuiFilePicker
onChange={(files) => handlePrivateKeyUpload(files, setStagedPrivateKey)}
accept=".pem"
/>
</EuiFormRow>
<EuiButton
fill
type="submit"
isLoading={isSubmitButtonLoading}
isDisabled={!githubAppId || (isGithubEnterpriseServer && !githubEnterpriseServerUrl)}
>
{isSubmitButtonLoading ? 'Connecting…' : `Connect ${name}`}
</EuiButton>
</form>
</Layout>
);
};
Loading

0 comments on commit 60f463a

Please sign in to comment.