diff --git a/x-pack/legacy/plugins/code/index.ts b/x-pack/legacy/plugins/code/index.ts index 96e2efc793834..eb902367047ea 100644 --- a/x-pack/legacy/plugins/code/index.ts +++ b/x-pack/legacy/plugins/code/index.ts @@ -38,6 +38,7 @@ export const code = (kibana: any) => const config = server.config(); return { codeUiEnabled: config.get('xpack.code.ui.enabled'), + codeIntegrationsEnabled: config.get('xpack.code.integrations.enabled'), }; }, hacks: ['plugins/code/hacks/toggle_app_link_in_nav'], @@ -49,6 +50,9 @@ export const code = (kibana: any) => ui: Joi.object({ enabled: Joi.boolean().default(true), }).default(), + integrations: Joi.object({ + enabled: Joi.boolean().default(false), + }).default(), enabled: Joi.boolean().default(true), }).default(); }, diff --git a/x-pack/legacy/plugins/code/public/components/app.tsx b/x-pack/legacy/plugins/code/public/components/app.tsx index 54363213bcad5..e87e9dd88e6a4 100644 --- a/x-pack/legacy/plugins/code/public/components/app.tsx +++ b/x-pack/legacy/plugins/code/public/components/app.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { HashRouter as Router, Redirect, Switch } from 'react-router-dom'; +import chrome from 'ui/chrome'; import { connect } from 'react-redux'; import { RootState } from '../reducers'; import { Admin } from './admin_page/admin'; @@ -17,6 +18,7 @@ import { NotFound } from './main/not_found'; import { Route } from './route'; import * as ROUTES from './routes'; import { Search } from './search_page/search'; +import { Integrations } from './integrations'; const RooComponent = (props: { setupOk?: boolean }) => { if (props.setupOk) { @@ -33,6 +35,8 @@ const Root = connect(mapStateToProps)(RooComponent); const Empty = () => null; +const integrationsEnabled = chrome.getInjected('codeIntegrationsEnabled'); + export const App = () => { return ( @@ -45,6 +49,9 @@ export const App = () => { + {integrationsEnabled && ( + + )} diff --git a/x-pack/legacy/plugins/code/public/components/codeblock/codeblock.tsx b/x-pack/legacy/plugins/code/public/components/codeblock/codeblock.tsx index 910129eb1a3c1..89c9e9137158f 100644 --- a/x-pack/legacy/plugins/code/public/components/codeblock/codeblock.tsx +++ b/x-pack/legacy/plugins/code/public/components/codeblock/codeblock.tsx @@ -5,50 +5,72 @@ */ import { EuiPanel } from '@elastic/eui'; -import { editor, IPosition, IRange } from 'monaco-editor'; +import { editor, IRange } from 'monaco-editor'; import React from 'react'; import { ResizeChecker } from '../shared/resize_checker'; import { monaco } from '../../monaco/monaco'; import { registerEditor } from '../../monaco/single_selection_helper'; -interface Props { - code: string; - fileComponent?: React.ReactNode; - startLine?: number; - language?: string; - highlightRanges?: IRange[]; - onClick?: (event: IPosition) => void; +export interface Position { + lineNumber: string; + column: number; +} + +export interface Props { + content: string; + header: React.ReactNode; + language: string; + highlightRanges: IRange[]; + onClick: (event: Position) => void; folding: boolean; - lineNumbersFunc: (line: number) => string; + /** + * Returns the line number to display for a given line. + * @param lineIndex The index of the given line (0-indexed) + */ + lineNumber: (lineIndex: number) => string; + className?: string; } export class CodeBlock extends React.PureComponent { + static defaultProps = { + header: undefined, + folding: false, + highlightRanges: [], + language: 'text', + lineNumber: String, + onClick: () => {}, + }; + private el: HTMLDivElement | null = null; private ed?: editor.IStandaloneCodeEditor; private resizeChecker?: ResizeChecker; private currentHighlightDecorations: string[] = []; public async componentDidMount() { + const { content, highlightRanges, language, onClick } = this.props; + if (this.el) { - await this.tryLoadFile(this.props.code, this.props.language || 'text'); + await this.tryLoadFile(content, language); this.ed!.onMouseDown((e: editor.IEditorMouseEvent) => { if ( - this.props.onClick && + onClick && (e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS || e.target.type === monaco.editor.MouseTargetType.CONTENT_TEXT) ) { const position = e.target.position || { lineNumber: 0, column: 0 }; - const lineNumber = (this.props.startLine || 0) + position.lineNumber; - this.props.onClick({ + const lineNumber = this.lineNumber(position.lineNumber); + + onClick({ lineNumber, column: position.column, }); } }); registerEditor(this.ed!); - if (this.props.highlightRanges) { - const decorations = this.props.highlightRanges.map((range: IRange) => { + + if (highlightRanges.length) { + const decorations = highlightRanges.map((range: IRange) => { return { range, options: { @@ -66,6 +88,7 @@ export class CodeBlock extends React.PureComponent { }); } } + private async tryLoadFile(code: string, language: string) { try { await monaco.editor.colorize(code, language, {}); @@ -79,7 +102,7 @@ export class CodeBlock extends React.PureComponent { this.ed = monaco.editor.create(this.el!, { value: code, language, - lineNumbers: this.lineNumbersFunc.bind(this), + lineNumbers: this.lineNumber, readOnly: true, folding: this.props.folding, minimap: { @@ -103,17 +126,16 @@ export class CodeBlock extends React.PureComponent { } public componentDidUpdate(prevProps: Readonly) { - if ( - prevProps.code !== this.props.code || - prevProps.highlightRanges !== this.props.highlightRanges - ) { + const { content, highlightRanges } = this.props; + + if (prevProps.content !== content || prevProps.highlightRanges !== highlightRanges) { if (this.ed) { const model = this.ed.getModel(); if (model) { - model.setValue(this.props.code); + model.setValue(content); - if (this.props.highlightRanges) { - const decorations = this.props.highlightRanges!.map((range: IRange) => { + if (highlightRanges.length) { + const decorations = highlightRanges!.map((range: IRange) => { return { range, options: { @@ -138,19 +160,20 @@ export class CodeBlock extends React.PureComponent { } public render() { - const linesCount = this.props.code.split('\n').length; + const { className, header } = this.props; + const height = this.lines.length * 18; + return ( - - {this.props.fileComponent} -
(this.el = r)} style={{ height: linesCount * 18 }} /> + + {header} +
(this.el = r)} style={{ height }} /> ); } - private lineNumbersFunc = (line: number) => { - if (this.props.lineNumbersFunc) { - return this.props.lineNumbersFunc(line); - } - return `${(this.props.startLine || 0) + line}`; - }; + private lineNumber = (lineIndex: number) => this.props.lineNumber(lineIndex - 1); + + private get lines(): string[] { + return this.props.content.split('\n'); + } } diff --git a/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx b/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx index 219c5e837155c..785238eb5fab7 100644 --- a/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx +++ b/x-pack/legacy/plugins/code/public/components/editor/references_panel.tsx @@ -14,13 +14,12 @@ import { EuiTitle, } from '@elastic/eui'; import classname from 'classnames'; -import { IPosition } from 'monaco-editor'; import queryString from 'querystring'; import React from 'react'; import { parseSchema } from '../../../common/uri_util'; import { GroupedFileResults, GroupedRepoResults } from '../../actions'; import { history } from '../../utils/url'; -import { CodeBlock } from '../codeblock/codeblock'; +import { CodeBlock, Position } from '../codeblock/codeblock'; interface Props { isLoading: boolean; @@ -114,12 +113,9 @@ export class ReferencesPanel extends React.Component { private renderReference(file: GroupedFileResults) { const key = `${file.uri}`; - const lineNumberFn = (l: number) => { - return file.lineNumbers[l - 1]; - }; - const fileComponent = ( + const header = ( - + {file.file} @@ -128,23 +124,22 @@ export class ReferencesPanel extends React.Component { return ( file.lineNumbers[i]} highlightRanges={file.highlights} - fileComponent={fileComponent} - onClick={this.onCodeClick.bind(this, file.lineNumbers, file.uri)} + onClick={this.onCodeClick(file.uri)} /> ); } - private onCodeClick(lineNumbers: string[], url: string, pos: IPosition) { - const line = parseInt(lineNumbers[pos.lineNumber - 1], 10); - history.push(this.computeUrl(url, line)); - } + private onCodeClick = (url: string) => (position: Position) => { + const lineNum = parseInt(position.lineNumber, 10); + history.push(this.computeUrl(url, lineNum)); + }; private computeUrl(url: string, line?: number) { const { uri } = parseSchema(url)!; @@ -158,6 +153,7 @@ export class ReferencesPanel extends React.Component { tab: 'references', refUrl: this.props.refUrl, }); + return line !== undefined ? `${uri}!L${line}:0?${query}` : `${uri}?${query}`; } } diff --git a/x-pack/legacy/plugins/code/public/components/integrations/data.ts b/x-pack/legacy/plugins/code/public/components/integrations/data.ts new file mode 100644 index 0000000000000..83137dff158ee --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/integrations/data.ts @@ -0,0 +1,159 @@ +/* + * 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. + */ + +export interface Snippet { + uri: string; + filePath: string; + language?: string; + compositeContent: { + content: string; + lineMapping: string[]; + }; +} + +export type Results = Record; + +export const results: Results = { + 'ringside.ts#L18': { + uri: 'github.com/rylnd/ringside', + filePath: 'src/ringside.ts', + language: 'typescript', + compositeContent: { + content: + "\nimport { fitsInside, fitsOutside } from './fitting';\n\nexport interface RingsideInterface {\n positions(): FittedPosition[];\n}\n\nclass Ringside implements RingsideInterface {\n readonly innerBounds: FullRect;\n readonly outerBounds: FullRect;\n\n}\n\nexport default Ringside;\n", + lineMapping: [ + '..', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '20', + '21', + '..', + '67', + '68', + '69', + '70', + ], + }, + }, + 'ringside.story.tsx#L12': { + uri: 'github.com/rylnd/ringside', + filePath: 'stories/ringside.story.tsx', + language: 'typescript', + compositeContent: { + content: + "\nimport { interpolateRainbow } from 'd3-scale-chromatic';\n\nimport { Ringside } from '../src';\nimport { XAlignment, YAlignment, XBasis, YBasis } from '../src/types';\n\nlet ringside: Ringside;\n\nconst enumKeys: (e: any) => string[] = e =>\n\n\nconst color = position => {\n const combos = ringside.positions().map(p => JSON.stringify(p));\n const hash = combos.indexOf(JSON.stringify(position)) / combos.length;\n\n\n};\n\nconst Stories = storiesOf('Ringside', module).addDecorator(withKnobs);\n\nStories.add('Ringside', () => {\n", + lineMapping: [ + '..', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '..', + '14', + '15', + '16', + '17', + '18', + '..', + '20', + '21', + '22', + '23', + '24', + '..', + ], + }, + }, + + 'ringside.story.tsx#L8': { + uri: 'github.com/rylnd/ringside', + filePath: 'stories/ringside.story.tsx', + language: 'typescript', + compositeContent: { + content: + "import { Ringside } from '../src';\n\ndescribe('Ringside', () => {\n let inner;\n let outer;\n let height;\n let width;\n let ringside: Ringside;\n\n beforeEach(() => {\n\n width = 50;\n\n ringside = new Ringside(inner, outer, height, width);\n });\n\n", + lineMapping: [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '..', + '14', + '15', + '16', + '17', + '18', + '..', + ], + }, + }, + + 'ringside.story.tsx#L14': { + uri: 'github.com/rylnd/ringside', + filePath: 'stories/ringside.story.tsx', + language: 'typescript', + compositeContent: { + content: + "import { Ringside } from '../src';\n\ndescribe('Ringside', () => {\n let inner;\n let outer;\n let height;\n let width;\n let ringside: Ringside;\n\n beforeEach(() => {\n\n width = 50;\n\n ringside = new Ringside(inner, outer, height, width);\n });\n\n", + lineMapping: [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '..', + '14', + '15', + '16', + '17', + '18', + '..', + ], + }, + }, +}; + +export interface Frame { + fileName: string; + lineNumber: number; + functionName?: string; +} + +export const frames: Frame[] = [ + { fileName: 'ringside.ts', lineNumber: 18 }, + { fileName: 'node_modules/library_code.js', lineNumber: 100 }, + { fileName: 'ringside.story.tsx', lineNumber: 8 }, + { fileName: 'node_modules/other_stuff.js', lineNumber: 58 }, + { fileName: 'node_modules/other/other.js', lineNumber: 3 }, + { fileName: 'ringside.story.tsx', lineNumber: 12 }, + { fileName: 'ringside.story.tsx', lineNumber: 14 }, +]; + +export const repos = [ + 'https://github.com/a/repo', + 'https://github.com/another/repo', + 'https://github.com/also/a_repo', +]; diff --git a/x-pack/legacy/plugins/code/public/components/integrations/frame_header.tsx b/x-pack/legacy/plugins/code/public/components/integrations/frame_header.tsx new file mode 100644 index 0000000000000..0fa482f8bce36 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/integrations/frame_header.tsx @@ -0,0 +1,40 @@ +/* + * 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 from 'react'; + +import { EuiButtonIcon, EuiFlexGroup, EuiLink, EuiText, EuiTextColor } from '@elastic/eui'; + +export const FrameHeader = ({ + fileName, + lineNumber, + onClick, +}: { + fileName: string; + lineNumber: number | string; + onClick: () => void; +}) => ( + + + {fileName} + at + line {lineNumber} + + + Last updated: 14 mins ago + + + +); diff --git a/x-pack/legacy/plugins/code/public/components/integrations/helpers.ts b/x-pack/legacy/plugins/code/public/components/integrations/helpers.ts new file mode 100644 index 0000000000000..6b55a5e2a0f45 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/integrations/helpers.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +// TODO(rylnd): make this an actual external link +export const externalFileURI: (repoUri: string, filePath: string) => string = (uri, path) => + `/${uri}/blob/HEAD/${path}`; diff --git a/x-pack/legacy/plugins/code/public/components/integrations/index.tsx b/x-pack/legacy/plugins/code/public/components/integrations/index.tsx new file mode 100644 index 0000000000000..5052b3502748d --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/integrations/index.tsx @@ -0,0 +1,61 @@ +/* + * 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 from 'react'; +import { EuiFlexGroup, EuiText } from '@elastic/eui'; + +import { CodeBlock } from '../codeblock/codeblock'; +import { history } from '../../utils/url'; +import { FrameHeader } from './frame_header'; +import { RepoTitle } from './repo_title'; +import { externalFileURI } from './helpers'; +import { frames, results } from './data'; + +export const Integrations = () => ( +
+ {frames.map(frame => { + const { fileName, lineNumber } = frame; + const key = `${fileName}#L${lineNumber}`; + const snippet = results[key]; + + if (snippet) { + const { compositeContent, filePath, language, uri } = snippet; + const { content, lineMapping } = compositeContent; + const fileUrl = externalFileURI(uri, filePath); + + return ( +
+ + history.push(fileUrl)} + /> + } + language={language} + lineNumber={i => lineMapping[i]} + /> +
+ ); + } + + return ( +
+ + + {fileName} + at + line {lineNumber} + + +
+ ); + })} +
+); diff --git a/x-pack/legacy/plugins/code/public/components/integrations/integrations.scss b/x-pack/legacy/plugins/code/public/components/integrations/integrations.scss new file mode 100644 index 0000000000000..24fe406cd0960 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/integrations/integrations.scss @@ -0,0 +1,43 @@ +.integrations__container { + padding: $euiSize; +} + +.integrations__frame { + margin: $euiSizeS 0; +} + +.integrations__code { + @include euiCodeFont; +} + +.integrations__link--external { + margin-left: $euiSizeS; +} + +.integrations__preposition { + margin: 0 $euiSizeS; + color: $euiColorMediumShade; +} + +.integrations__button-icon { + padding: $euiSizeXS; + background-color: $euiColorLightestShade; + border: 1px solid $euiColorLightShade; +} + +.integrations__snippet-info { + margin-bottom: $euiSizeS; +} + +.integrations__snippet-title { + margin-bottom: $euiSizeS; +} + +.integrations__text--bold { + font-weight: $euiFontWeightBold; +} + +.integrations__popover { + margin-bottom: 1rem; + width: 300px; +} diff --git a/x-pack/legacy/plugins/code/public/components/integrations/repo_title.tsx b/x-pack/legacy/plugins/code/public/components/integrations/repo_title.tsx new file mode 100644 index 0000000000000..2ed3d529b699b --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/integrations/repo_title.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiText } from '@elastic/eui'; +import { RepositoryUtils } from '../../../common/repository_utils'; + +export const RepoTitle = ({ uri }: { uri: string }) => { + const org = RepositoryUtils.orgNameFromUri(uri); + const name = RepositoryUtils.repoNameFromUri(uri); + + return ( + + {org}/ + {name} + + ); +}; diff --git a/x-pack/legacy/plugins/code/public/components/routes.ts b/x-pack/legacy/plugins/code/public/components/routes.ts index 0d4ccf4e5e826..d741ca1896850 100644 --- a/x-pack/legacy/plugins/code/public/components/routes.ts +++ b/x-pack/legacy/plugins/code/public/components/routes.ts @@ -15,3 +15,4 @@ export const REPO = `/:resource/:org/:repo`; export const MAIN_ROOT = `/:resource/:org/:repo/${pathTypes}/:revision`; export const ADMIN = '/admin'; export const SEARCH = '/search'; +export const INTEGRATIONS = '/integrations'; diff --git a/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx b/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx index 632c221f0680b..72aacce978671 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx @@ -6,13 +6,12 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IPosition } from 'monaco-editor'; import React from 'react'; import { Link } from 'react-router-dom'; import { RepositoryUtils } from '../../../common/repository_utils'; import { history } from '../../utils/url'; -import { CodeBlock } from '../codeblock/codeblock'; +import { CodeBlock, Position } from '../codeblock/codeblock'; interface Props { query: string; @@ -22,15 +21,14 @@ interface Props { export class CodeResult extends React.PureComponent { public render() { const { results, query } = this.props; + return results.map(item => { - const { uri, filePath, hits, compositeContent } = item; + const { compositeContent, filePath, hits, language, uri } = item; const { content, lineMapping, ranges } = compositeContent; - const repoLinkUrl = `/${uri}/tree/HEAD/`; - const fileLinkUrl = `/${uri}/blob/HEAD/${filePath}`; const key = `${uri}-${filePath}-${query}`; - const lineMappingFunc = (l: number) => { - return lineMapping[l - 1]; - }; + const repoLinkUrl = `/${uri}/tree/HEAD/`; + const fileLinkUrl = `/${uri}/blob/HEAD/${filePath}`; // TODO(rylnd) move these to link helpers + return (
@@ -75,23 +73,23 @@ export class CodeResult extends React.PureComponent { lineMapping[i]} + onClick={this.onCodeClick(fileLinkUrl)} />
); }); } - private onCodeClick(lineNumbers: string[], fileUrl: string, pos: IPosition) { - const line = parseInt(lineNumbers[pos.lineNumber - 1], 10); - if (!isNaN(line)) { - history.push(`${fileUrl}!L${line}:0`); + private onCodeClick = (url: string) => (position: Position) => { + const lineNumber = parseInt(position.lineNumber, 10); + + if (!isNaN(lineNumber)) { + history.push(`${url}!L${lineNumber}:0`); } - } + }; } diff --git a/x-pack/legacy/plugins/code/public/index.scss b/x-pack/legacy/plugins/code/public/index.scss index aecaa46272ded..6d996a80f283f 100644 --- a/x-pack/legacy/plugins/code/public/index.scss +++ b/x-pack/legacy/plugins/code/public/index.scss @@ -4,6 +4,7 @@ @import "./monaco/override_monaco_styles.scss"; @import "./components/diff_page/diff.scss"; @import "./components/main/main.scss"; +@import "./components/integrations/integrations.scss"; // TODO: Cleanup everything above this line diff --git a/x-pack/legacy/plugins/code/public/monaco/override_monaco_styles.scss b/x-pack/legacy/plugins/code/public/monaco/override_monaco_styles.scss index 3e03c5693d434..b6f39a34e8653 100644 --- a/x-pack/legacy/plugins/code/public/monaco/override_monaco_styles.scss +++ b/x-pack/legacy/plugins/code/public/monaco/override_monaco_styles.scss @@ -9,6 +9,7 @@ .monaco-editor.mac .margin-view-overlays .line-numbers { cursor: pointer; + color: $euiColorMediumShade; background-color: $euiColorLightestShade; }