[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:
parent
aa1102d011
commit
67d026e77f
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -31,3 +31,6 @@
|
|||
border-radius: $euiSizeXS $euiSizeXS 0 0;
|
||||
}
|
||||
|
||||
.referencesPanel__code-block {
|
||||
margin-bottom: $euiSizeXL;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.codeResult__code-block {
|
||||
margin-bottom: $euiSizeXL;
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue