[Code] Code Integrator Component (#47180)

* Add CodeIntegrator component

* Abstracts shared ImportModal component
* Uses placeholder data and callbacks in lieu of real integration
This commit is contained in:
Ryland Herrick 2019-10-04 20:26:55 -05:00 committed by GitHub
parent aa1102d011
commit 67d026e77f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 292 additions and 95 deletions

View file

@ -6,23 +6,13 @@
import {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiGlobalToastList,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSpacer,
EuiSuperSelect,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -39,6 +29,7 @@ import { ToastType } from '../../reducers/repository_management';
import { isImportRepositoryURLInvalid } from '../../utils/url';
import { ProjectItem } from './project_item';
import { ProjectSettings } from './project_settings';
import { ImportModal } from '../integrations/import_modal';
enum SortOptionsValue {
AlphabeticalAsc = 'alphabetical_asc',
@ -169,70 +160,21 @@ class CodeProjectTab extends React.PureComponent<Props, State> {
}
};
public updateIsInvalid = () => {
this.setState({ isInvalid: isImportRepositoryURLInvalid(this.state.repoURL) });
};
public renderImportModal = () => {
return (
<EuiOverlayMask>
<EuiModal onClose={this.closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.code.adminPage.repoTab.importRepoTitle"
defaultMessage="Import a new repo"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.code.adminPage.repoTab.repositoryUrlFormLabel"
defaultMessage="Repository URL"
/>
</h3>
</EuiTitle>
<EuiForm>
<EuiFormRow
isInvalid={this.state.isInvalid}
error={i18n.translate('xpack.code.adminPage.repoTab.repositoryUrlEmptyText', {
defaultMessage: "The URL shouldn't be empty.",
})}
>
<EuiFieldText
value={this.state.repoURL}
onChange={this.onChange}
onBlur={this.updateIsInvalid}
placeholder="https://github.com/Microsoft/TypeScript-Node-Starter"
aria-label="input project url"
data-test-subj="importRepositoryUrlInputBox"
isLoading={this.props.importLoading}
fullWidth={true}
isInvalid={this.state.isInvalid}
autoFocus={true}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={this.closeModal}>
<FormattedMessage
id="xpack.code.adminPage.repoTab.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton fill onClick={this.submitImportProject} disabled={this.props.importLoading}>
<FormattedMessage
id="xpack.code.adminPage.repoTab.importButtonLabel"
defaultMessage="Import"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
const { isInvalid, repoURL, showImportProjectModal } = this.state;
if (showImportProjectModal) {
return (
<ImportModal
isInvalid={isInvalid}
isLoading={this.props.importLoading}
onChange={this.onChange}
onClose={this.closeModal}
onSubmit={this.submitImportProject}
value={repoURL}
/>
);
}
};
public setSortOption = (value: string) => {
@ -242,7 +184,6 @@ class CodeProjectTab extends React.PureComponent<Props, State> {
public render() {
const { projects, status, toastMessage, showToast, toastType } = this.props;
const projectsCount = projects.length;
const modal = this.state.showImportProjectModal && this.renderImportModal();
const sortedProjects = projects.sort(sortFunctionsFactory(status)[this.state.sortOption]);
const repoList = sortedProjects.map((repo: Repository) => (
@ -321,7 +262,7 @@ class CodeProjectTab extends React.PureComponent<Props, State> {
</EuiText>
<EuiSpacer />
{repoList}
{modal}
{this.renderImportModal()}
{settings}
</div>
);

View file

@ -31,3 +31,6 @@
border-radius: $euiSizeXS $euiSizeXS 0 0;
}
.referencesPanel__code-block {
margin-bottom: $euiSizeXL;
}

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiButtonEmpty, EuiPopover, EuiText } from '@elastic/eui';
import { RepoSelector } from './repo_selector';
interface Props {
onRepoSelect: (repo: string) => void;
onImportSuccess: (repo: string) => void;
repos: string[];
}
export const CodeIntegrator = ({ onRepoSelect, onImportSuccess, repos }: Props) => {
const [showSelector, setShowSelector] = useState(false);
const handleClick = () => setShowSelector(true);
const handleSelect = (codeId: string) => {
onRepoSelect(codeId);
setShowSelector(false);
// TODO: show success
};
const link = (
<EuiButtonEmpty
className="codeIntegrations__link--external"
iconType="codeApp"
onClick={handleClick}
>
<EuiText size="s">View in Code</EuiText>
</EuiButtonEmpty>
);
return (
<EuiPopover
anchorPosition="leftCenter"
button={link}
isOpen={showSelector}
closePopover={() => setShowSelector(false)}
>
<EuiText size="s" className="codeIntegrations__popover">
<h3>No repository mapping found</h3>
<p>
We can't find the mapping between service and the source code. Select the repository or
import a new one.
</p>
</EuiText>
<RepoSelector onSelect={handleSelect} onImport={onImportSuccess} repos={repos} />
</EuiPopover>
);
};

View file

@ -18,7 +18,7 @@ export const FrameHeader = ({
onClick: () => void;
}) => (
<EuiFlexGroup
className="integrations__snippet-info"
className="codeIntegrations__snippet-info"
alignItems="center"
justifyContent="spaceBetween"
gutterSize="none"
@ -31,7 +31,7 @@ export const FrameHeader = ({
<EuiText size="xs">
<EuiTextColor color="subdued">Last updated: 14 mins ago</EuiTextColor>
<EuiButtonIcon
className="integrations__link--external integrations__button-icon"
className="codeIntegrations__link--external codeIntegrations__button-icon"
iconType="codeApp"
onClick={onClick}
/>

View file

@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { ChangeEvent } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
interface Props {
isInvalid: boolean;
isLoading: boolean;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onClose: () => void;
onSubmit: () => void;
value: string;
}
export const ImportModal = ({
isInvalid,
isLoading,
onChange,
onClose,
onSubmit,
value,
}: Props) => (
<EuiOverlayMask>
<EuiModal onClose={onClose}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.code.adminPage.repoTab.importRepoTitle"
defaultMessage="Import a new repo"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.code.adminPage.repoTab.repositoryUrlFormLabel"
defaultMessage="Repository URL"
/>
</h3>
</EuiTitle>
<EuiForm>
<EuiFormRow
isInvalid={isInvalid}
error={i18n.translate('xpack.code.adminPage.repoTab.repositoryUrlEmptyText', {
defaultMessage: "The URL shouldn't be empty.",
})}
>
<EuiFieldText
value={value}
onChange={onChange}
onBlur={onChange}
placeholder="https://github.com/Microsoft/TypeScript-Node-Starter"
aria-label="input project url"
data-test-subj="importRepositoryUrlInputBox"
isLoading={isLoading}
fullWidth={true}
isInvalid={isInvalid}
autoFocus={true}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onClose}>
<FormattedMessage
id="xpack.code.adminPage.repoTab.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton fill onClick={onSubmit} disabled={isLoading}>
<FormattedMessage
id="xpack.code.adminPage.repoTab.importButtonLabel"
defaultMessage="Import"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);

View file

@ -11,11 +11,17 @@ import { CodeBlock } from '../codeblock/codeblock';
import { history } from '../../utils/url';
import { FrameHeader } from './frame_header';
import { RepoTitle } from './repo_title';
import { CodeIntegrator } from './code_integrator';
import { externalFileURI } from './helpers';
import { frames, results } from './data';
import { frames, Frame, repos, results } from './data';
const associateToService = (frame: Frame) => (repo: string) =>
alert(`repo ${repo} associated with service ${JSON.stringify(frame)}`);
const handleImport = (repo: string) => alert(`import done: ${repo}`);
export const Integrations = () => (
<div className="codeContainer__root integrations__container">
<div className="codeContainer__root codeIntegrations__container">
{frames.map(frame => {
const { fileName, lineNumber } = frame;
const key = `${fileName}#L${lineNumber}`;
@ -27,7 +33,7 @@ export const Integrations = () => (
const fileUrl = externalFileURI(uri, filePath);
return (
<div key={key} className="integrations__frame">
<div key={key} className="codeIntegrations__frame">
<RepoTitle uri={snippet.uri} />
<CodeBlock
content={content}
@ -46,13 +52,18 @@ export const Integrations = () => (
}
return (
<div key={key} className="integrations__frame">
<div key={key} className="codeIntegrations__frame">
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" alignItems="center">
<EuiText size="s" className="integrations__code">
<EuiText size="s" className="codeIntegrations__code">
<span>{fileName}</span>
<span className="integrations__preposition">at</span>
<span className="codeIntegrations__preposition">at</span>
<span>line {lineNumber}</span>
</EuiText>
<CodeIntegrator
onRepoSelect={associateToService(frame)}
onImportSuccess={handleImport}
repos={repos}
/>
</EuiFlexGroup>
</div>
);

View file

@ -1,43 +1,43 @@
.integrations__container {
.codeIntegrations__container {
padding: $euiSize;
}
.integrations__frame {
.codeIntegrations__frame {
margin: $euiSizeS 0;
}
.integrations__code {
.codeIntegrations__code {
@include euiCodeFont;
}
.integrations__link--external {
.codeIntegrations__link--external {
margin-left: $euiSizeS;
}
.integrations__preposition {
.codeIntegrations__preposition {
margin: 0 $euiSizeS;
color: $euiColorMediumShade;
}
.integrations__button-icon {
.codeIntegrations__button-icon {
padding: $euiSizeXS;
background-color: $euiColorLightestShade;
border: 1px solid $euiColorLightShade;
}
.integrations__snippet-info {
.codeIntegrations__snippet-info {
margin-bottom: $euiSizeS;
}
.integrations__snippet-title {
.codeIntegrations__snippet-title {
margin-bottom: $euiSizeS;
}
.integrations__text--bold {
.codeIntegrations__text--bold {
font-weight: $euiFontWeightBold;
}
.integrations__popover {
.codeIntegrations__popover {
margin-bottom: 1rem;
width: 300px;
}

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiButton, EuiSelect } from '@elastic/eui';
import { ImportModal } from './import_modal';
import { isImportRepositoryURLInvalid } from '../../utils/url';
interface Props {
onSelect: (repo: string) => void;
onImport: (repo: string) => void;
repos: string[];
}
const placeHolderOption = { value: 'select_new', text: 'Select' };
const importNewOption = { value: 'import_new', text: 'Import new' };
const importStub: (repo: string) => Promise<string> = repo =>
new Promise(resolve => setTimeout(() => resolve(repo), 5000));
export const RepoSelector = ({ onImport, onSelect, repos: _repos }: Props) => {
const [selectedValue, setSelectedValue] = useState(placeHolderOption.value);
const [newRepo, setNewRepo] = useState('');
const [isInvalid, setIsInvalid] = useState(false);
const [showModal, setShowModal] = useState(false);
const repos = newRepo ? [..._repos, newRepo] : _repos;
const selectedRepo = repos.find(repo => repo === selectedValue);
const options = [
placeHolderOption,
...repos.map(repo => ({ value: repo, text: repo })),
importNewOption,
];
const handleNewRepoChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setIsInvalid(isImportRepositoryURLInvalid(value));
setNewRepo(value);
};
const handleChange = ({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedValue(value);
if (value === 'import_new') {
setShowModal(true);
}
};
const handleSave = () => selectedRepo && onSelect(selectedRepo);
const handleImportSubmit = () => {
setSelectedValue(newRepo);
importStub(newRepo).then(onImport);
setShowModal(false);
};
const handleClose = () => {
setSelectedValue(placeHolderOption.value);
setShowModal(false);
};
return (
<>
<EuiSelect options={options} value={selectedValue} onChange={handleChange} />
<EuiButton disabled={!selectedRepo} onClick={handleSave} style={{ marginTop: '1rem' }}>
Save Mapping
</EuiButton>
{showModal && (
<ImportModal
onChange={handleNewRepoChange}
value={newRepo}
isInvalid={isInvalid}
isLoading={false}
onClose={handleClose}
onSubmit={handleImportSubmit}
/>
)}
</>
);
};

View file

@ -14,9 +14,9 @@ export const RepoTitle = ({ uri }: { uri: string }) => {
const name = RepositoryUtils.repoNameFromUri(uri);
return (
<EuiText size="s" className="integrations__snippet-title">
<EuiText size="s" className="codeIntegrations__snippet-title">
<span>{org}/</span>
<span className="integrations__text--bold">{name}</span>
<span className="codeIntegrations__text--bold">{name}</span>
</EuiText>
);
};

View file

@ -0,0 +1,3 @@
.codeResult__code-block {
margin-bottom: $euiSizeXL;
}

View file

@ -4,6 +4,7 @@
@import "./monaco/override_monaco_styles.scss";
@import "./components/diff_page/diff.scss";
@import "./components/main/main.scss";
@import "./components/search_page/search.scss";
@import "./components/integrations/integrations.scss";
// TODO: Cleanup everything above this line