[7.x] Backport code commits (#35548)

* Create a squash commit for code (#35547)

* [Code] Use system JDK if it is available (#31389)

* [Code] fix functional tests (#31555)

* fix(code/frontend): main page (#29811)

* [Code] fix find references style (#30911)

* [Code]: properly reconnect when langserver is down and clean up logs, fix #839, #891 (#30601)

* fix(code/frontend): wider clickable area for file and structure node (#31176)

* fix(code/frontend): fix project filter press enter location change (#29919)

* Code: move all non-error LSP logging to debug level

* [Code] Dark mode cleanup (#31208)

* fix(code/frontend): fix componentWillMount (#31772)

* [Code] Remove socket.io and use polling message to pull progresses (#31398)

* [Code] Remove socket.io and use polling message to pull progresses

* [Code] refactor the status polling logic

* [Code] fix a minor test issue

* [Code] correctly handle url when workspace is a symbol link. (#31782)

* fix(code/frontend): lose symbols (#31664)

* [Code] functional test for code intelligence (#31673)

* [Code] Add api test for multi code node setup (#31460)

* [Code] fix the bug the first type in querybar is alwasy discarded (#31884)

* [Code] disable cross repo jump functional test

* fix(code/frontend): replace deprecated react lifecycle methods (#31874)

* [Code] Add duration for queued tasks (#31885)

* [Code] fix editor lifecycle method (#31983)

* [Code] force delete repository (#31995)

* feature(code/frontend): implement new breadcrumb design (#31247)

* [Code] handle import error (#31875)

* fix(code/frontend): show import project error message

* [Code] increase the git clone/update throttle param to make ES data update less frequent (#31988)

* fix(code/frontend): side navigation bar width should be fixed (#31876)

* fix(code/frontend): should show import modal (#31987)

* [Code] update repo by set target ref directly (#32002)

* [Code] show nothing if setup status is not ready yet. (#31993)

* [Code] fix editor `goto line` (#32094)

* fix(code/frontend): match props missing (#32100)

* [Code] Improve repository progress polling when clone/index is interrupted by delete (#31989)

* [Code] fix tree flatten/expand/collapse problems (#32099)

* [Code] Fix check for JDK's version (#32104)

* fix(code/frontend): type errors (#32119)

* [Code] specify nodegit commit sha in package.json (#32170)

* [Code] fix setup page (#32179)

* [Code] fix type error in java launcher (#32270)

* [plugin installer] Keep external attributes of files during unarchiving (#32105)

* [plugin installer] Keep external attributes of files during unarchiving

* [plugin installer] add test for files' modes check

* [Code] Ignore certificate check for clone (#32271)

* [Code] fix tsc error

* [Code] fix yarn.lock

* Code: fix getClient is not a function error after merging (#32338)

* [Code] fix load file tree by refresh (#32280)

* [Code] Adjust `GoLauncher` to adapt to the running pattern of go server. (#32293)

This adjust will let the code plugin connect the go-langserver actively.
It should be noted that the `GoLauncher` is a semi-manufacture only
support the detach mode for now.

* [Code] Fix a line number bug for composite content calculation (#32376)

* [Code] function test for securities (#32278)

* [Code] hide import button if current user is not code_admin

* [Code] function test for securities

* [Code] apply the correct timestamp to admin page (#32379)

* [Code]: fix duplicate import agains kbn/test/types package, update typescript language server version (#32439)

* fix(code/frontend): highlight structure node (#32034)

* fix(code/frontend): special symbol container name (#32281)

* Monaco Editor Dark Mode (#32263)

* Initial go at light/dark mode compatability for the Monaco editor.

* Alphebetizing the imports.

* Using the color-convert package to convert rgb to hex values. Updating the monaco hover widget for dark mode.

* Changes to highlight and selection colors.

* Misspelled an EUI color variable name.

* Dark mode for the search results page.

* Prettifying code_result.tsx.

* Removing the monaco scroll decorator from the editor.

* Fixing some type errors for color-convert

* Markdown styling for dark mode.

* Changing the import location of 'chrome' in the monaco editor

* Adding a constant for the getTheme() method and adjusting blame view dark mode styles.

* [Code]: always downgrade language server logging level by one

* [Code]: upgrade ts langserver version, reduce the timeout for waiting langserver init

* [Code] replace _term aggregator order to _key (#32541)

* fix(code/frontend): reset breadcrumbs and fix code href (#32277)

* [Code] improve index progress calculation (#32537)

* [Code]: clean up the way we config LSP related configs (#32607)

* [Code] change nodegit to @elastic/nodegit (#32543)

* [Code] fix tree loading when jumping between different repos (#32650)

* [Code] Use data dir as config dir (#32609)

* [Code]: remove uneccessary color convert after upgrade to EUI 9

* [Code] fix a undefined path problem

* [Code] compute url for language server plugin (#32644)

* [Code] fix typo (#32751)

[Code] fixed some type errors
[Code] try change nodegit_info task to typescript to avoid ci problem

* [Code]: Update UI test snapshot

* [Code]: Disable project setting, branch selection and diff page (#32799)

* [Code] adjust search bar suggestions style (#32726)

* [Code] disable indent guides lines in editors (#32730)

* feature(code/frontend): show loading spinner when loading file/structure tree (#32775)

* fix(code/frontend): truncate blame date (#32764)

* [Code] Call default to lsp options (#32843)

* [Code] fix path handling in windows (#32882)

* feature(code/frontend): implement new 404 page (#32859)

* fix(code/frontend): truncate directory node and fix margin (#32858)

* [Code]: Upgrade ts langserver version

* [Code]: upgrade test snapshot

* [Build]: use the resolution in CreatePackageJsonTask task

* [Code]: correctly handle errors in JDK finding (#32824)

* [Code] Fix functional test (#32991)

* fix(code): clear import project input after submit (#32970)

* [Code] Show some content for file name matching (#32958)

* [Code] fix goto definition failed after user click `go back` (#32968)

* Forcing the filetree open on mobile devices. (#33056)

* [Code] remove requirement for SettingRepository button in tests (#33081)

* [Code]: clean up the code breadcrumb (#33069)

* [Code]: fix top bar button size, width and right margin (#33061)

* [Code]: link to the setup guide button should be the entire button (#33031)

* [Code] Remove the repository status in repo search result item (#32967)

* [Code] Add additional git url validation in clone worker (#33097)

* [Code] hide language server errors (#33082)

* [Code] fix tree expand problems (#32984)

* Fixes for dark mode (#33014)

* Fixing the keyboard shortcut modal appearance in dark mode.

* Fixing the language server icon colors in dark mode.

* Fixing the white background in markdown code blocks in dark mode (Code issue #942)

* Fixing the file tree background color (Code repo issue #986)

* Updating file_tree snapshots.

* Using variables in the shortcuts.scss file, moving the language icon selector to the path rather than the SVG itself.

* Fixing a type error for an unused import.

* [Code] make find references panel's file name clickable (#33083)

* [Code] update worker queue index name to exclude from code user/admin roles (#33223)

* fix(code): symbol tree style (#33224)

* [Code] Index job timeout should show repository index error (#33140)

* [Code] Index job timeout should show repository index error

* [Code] Add a new unit test for clone worker git url validation

* [Code] fix the test

* [Code] adjust repo search scope REST API params (#33219)

* [Code] scroll the selected file into view when navigate files (#33225)

* [Code] fix a minor bug for clone repo

* [Code] fix a tree expand/collapse problem (#33227)

* [Code] fixes for search page (#33281)

* [Code] fixes for search page

* [Code] fix functional test

* [Code] fix functioanl test

* [Code] Calculate the index job timeout based on the size of the repo (#33226)

* [Code]: add a test util file and move common class into it (#33283)

* fix(code/frontend): unset min-width of breadcrumb (#33298)

* fix(code): highlight only one symbol and unexpected tree loading (#33114)

* [Code] use callWithRequest instead of callWithInternalUser in cluster routes (#33098)

* [Code] Add options to disable maven/gradle importer and autobuild (#33240)

* [Code] Add options to disable maven/gradle importer and autobuild

* [Code] rename option to codeSecurity

* [Code] Add initial options to request expander

* [Code]: add option to disable node depdendency downloading (#33340)

* [Code]: change config code.codeSecurity to code.security

* [Code]: more clean up to the test option (#33355)

* [Code] Add git clone url host and protocol whitelist (#33371)

* [Code] align search page border and correct the rendering of empty search page (#33378)

* [Code] focus input when switch search scope with shortcuts (#33379)

* [Code] focus input when switch search scope with shortcuts

* [Code] prevent default action of the shortcut event

* fix(code): add project root link to kibana breadcrumb (#33297)

* Revert "fix(code): add project root link to kibana breadcrumb (#33297)"

This reverts commit e206b71171.

* [Code]: upgrade to typescript server 0.1.19

* [Code] Fix randomized port in Java launcher (#33495)

* [Code] fix popover style changes when click on buttons (#33472)

* [Code] fix setup guide style (#33474)

* [Code] fix a problem we start more than one lang-server for the same repo. (#33382)

* fix(code): use monospace font for commit hash (#33307)

* [Code] fix reference panel layout problems (#33546)

* [Code] fix lang-sever initializing popover (#33482)

* fix(code/frontend): truncate commit message (#33548)

* fix(code/frontend): use eui toast for import message (#33487)

* fix(code/frontend): check file path before reveal position (#33555)

* fix(code/frontend): should have no container (#33492)

* [Code] change lsp http error codes (#33633)

* fix(code/frontend): combobox in search setting flyout should be stretched to fit the width (#33553)

* [Code]  Enabgle `go` language of the monaco editor. (#33476)

This change will make the code plugin have the ability in the
development mode to highlight the go source code and send
the go-to definition request to the lang server.

* fix(code/frontend): error message for empty project url (#33549)

* [Code] connect the modify search settings button with the search scope settings (#33691)

* [Code] connect the modify search settings button with the search scope setting

* [Code] a minor fix

* [Code] minor style improvement

* [Code]: fix integration test using new API (#33730)

* [Code] fix a tree expanding problem (#33766)

* Fixing the directory node focus state. (#33821)

* [Code] fix a reference panel height problem (#33767)

* [Code] upgrade nodegit, set max returned commits results (#33913)

* [Code]: Upgrade dependencies

* [Code]: Syntax clean after bable-ts-transform upgrade

* [Code] don't patch native modules when build oss package (#33915)

* [Code] improve repository index naming (#33911)

* Revert "Fixing the directory node focus state. (#33821)"

This reverts commit 866db39ec3.

* [Code] Remove regex git url validation and add more unit tests for repository utils (#33919)

* [Code] more unit tests

* [Code] fix ci breaks

* [Code] handle nodegit deprecation warnings (#33932)

* Reducing top and bottom padding of the directory and file nodes in the directory view. (#34007)

* Styling the File Tree Scrollbar (#33988)

* Styling the scrollbar.

* Removing the duplicated mixin code.

* [Code] check file path in lsp requests (#33916)

* [Code] Implement the index checkpointing (#32682)

* [Code] Persist index checkpoint into index progress in ES

* [Code] apply checkpoint to lsp indexer

* [Code] Add unit tests for index checkpointing

* [Code] move checkpoint from text to object

* [Code]: raise default security level (#33956)

* fix(code/frontend): should not show import error at the first time (#33921)

* [Code]: add missing dependencies that are not in x-pack

* [Code]: fix test snapshot and eui usages

* chore(code/frontend): move props files to __fixtures__ folder (#34031)

* [Code] fix a tree collapse problem (#34030)

* [Code] fix a tree collapse problem
added functional tests for file tree

* Fix type errors and snapshot

* [Code]: simplify the path computation of ts server

* [Code]: clean uneccessary ts-ignores (#34203)

* [Code] apply repo search scope right away in search page (#34029)

* [Code] upgrade git-url-parse version and enable ssh git clone protocol (#34336)

* [Code] upgrade git-url-parse version and enable ssh git clone protocol

* [Code] fix unit test

* [Code]: minor clean up of tslint usage, up ts server version

* fix(code/frontend): use different actions to handle repo scope search and repo search (#34043)

* [Code] fix the crash when we refresh the blame view (#34335)

* [Code] enable ssh protocol, only read ssh key pairs from data folder. (#34412)

* [Code] reset processing jobs when system is initializing (#34408)

* [Code] functional test for git:// url (#34512)

* [Code] fix search query bar item selection issue (#34514)

* [Code]: use absolute path for api path (#34582)

* [Code]: use absolute path for api path

* [Code]: always use url.format to construct url with queries that have variables

* [Code]: prefix lsp api with code

* [Code]: Add chrome.addBasePath call for raw fetch argument

* [Code]: minor UI adjustment (#34659)

* [Code]: fix new eslint errors (#34671)

* [Code] save code_node_info in an index (#34244)

* [Code] fix line height changed after find reference is open (#34682)

* [Code] add description for file and repo typeahead items (#34681)

* [Code] encode/decode branchs and tags (#34683)

* [Code] Incremental Indexing (#33485)

* [Code] Add a git api to get diff from arbitrary 2 revisions

* [Code] Apply incremental index triggering

* [Code] implement the actual incremental indexing

* [Code] apply checkpoint validation for both lsp and lsp incremental indexer

* [Code] add unit tests

* [Code] only disable index scheduler but leave update scheduler on

* [Code]: disable more eslint error due to nodegit

* [Code]: add a go language icon

* [Code]: fix test snapshot after upgrade eui

* fix(code/frontend): make blame view scrollable (#34519)

* [Code]: add beta indicator (#34899)

* [Code]: add a toast for permission change in setup page (#34901)

* [Code] support '/' in getCommit (#34774)

* [Code]: show error message when importing repo at import repo page, fix type error (#34898)

* [Code]: add setup guide link in help menu and pre-define document links (#34902)

* [Code] hide index button when repo is in indexing (#34904)

* [Code]: clean uneccessary code, lint error

* [Code] apply encode to revision in url (#34906)

* [Code] disable blame button when select a non-text file (#34775)

* [Code] disable blame button when select a non-text file

* [Code] Change button label based on file type

* [Code] Provide more reasons for git url validation (#34914)

* fix(code/frontend): should disable structure tab if no structure or load failed (#34908)

* [Code] don't allow access secured routes before x-pack info is available (#34994)

* fix(code/frontend): remove line decorations if no line number specified (#35047)

* Reset initialized when proxy re-connects (#34970)

* [Code] setup multi-code mode by config (#34988)

* [Code] Persist clone error messages (#34977)

* test(code/frontend): history functional test (#34921)

* Implement Code feature control (#35115)

* update security api tests

* rough POC to migrate Code to use Feature Controls

* fix tests

* [Code]: Integrate with Feature control

* Rename callWithRequest to callCluster

* feature(code/frontend): search filter default repo options (#35202)

* [Code] minor fix of proejct status update (#35207)

* feature(code/frontend): search in project page set current repo as default scope (#35062)

* fix(code/frontend): structure tab highlight, align and collapse (#35221)

* [Code] add api integration tests for feature control (#35146)

* [Code] Ignore certificateCheck in update as well (#35273)

* [Code]: fix test snapshot

* [Code] Removing styled components & SCSS cleanup (#35107)

* Removing the sidebar class from the project container and replacing styled component eui buttons with a className.

* Renaming scss includes.

* Moving admin.scss content into _buttons.scss.

* Refactor project_item removing styled components

* Refactor admin.tsx to remove styled components

* Refactor import_project.tsx to remove styled components

* Refactor lang server tab to remove styled components

* Refactor project settings modal removing styled components

* Refactoring setup_guide to remove styled components

* Cleanup sidebar.scss: follow convention for classes

* Refactor codeblock css naming conventions

* Resolving an issue with the monaco scss file name

* Editor file cleanup. Renaming css classes

* Cleaning up the file_tree component.

* Hover widget cleanup.

* Blame cleanup.

* Breadcrumb cleanup.

* Cleaning up clone status — removing ProgressContainer export. Didn't seem to be used anywhere. You can use the codeContainer__progress class to apply those styles now.

* Cleaning up commit history styles

* Putting the indentation back in the file tree.

* Refactoring the main content window.

* Cleaning up the directory component.

* Reducing spacing between directory and file lists.

* Removing styled components from the error panel.

* Reducing the font size of buttons in the source view button groups.

* Removing styled components from main.tsx

* CSS naming & removing styled components from not_found.tsx

* CSS naming & removing styled components from not_found.tsx

* Removing styled components from search_bar and top_bar.

* Removing styled components from query_bar components.

* Removing styled components from search_page components.

* removing styled components from code_symbol_tree

* Fixing a few css issues.

* Updating test snapshots.

* Removing a stray '>' symbol from the search tabs.

* Condensing the spacing of the EUI facets on the search page.

* class name of the flyout container.

* Revert "class name of the flyout container."

This reverts commit 35e9d5c16fd20db5ef15a686eda79bb0fd3f40a6.

* class name tweaks.

* Fixing type errors.

* Putting back an accidental deletion in file_tree.tsx.

* Updating file_tree snapshot.

* Implementing changes from 604e4d1173 to address failing tests.

* Adding in additional classes deleted during merge

* Updating test snapshots.

* Removing the focusring from the items in the file_tree. (#35364)

* [Code] pagination for history (#35329)

* Updating the markdown rendering to use EUI styles. (#35439)

* [Code]: fix icon for module and namespace (#35428)

* [Code] apply git clone/update cert check for production env (#35399)

* fix(code/frontend): source view page, click line number should stay in the same side tab (#35396)

* [Code] Add a security flag for git certificate check (#35445)

* fix(code/frontend): fix blank left to blame (#35449)

* [Code]: Improve code setup guide text

* [Code]: fix the capabilities type (#35499)

* [Code]: fix the capabilities type

* [Code] disable the functional test

* [core/ftr] disable tests by commenting out test file

* Updating the font sizes on the directory view. (#35507)

* Updating the font sizes on the directory view.

* Adding less left margin to the recent commits heading.
This commit is contained in:
Fuyao Zhao 2019-04-24 12:03:16 -07:00 committed by GitHub
parent d2edef1965
commit d6a11e717a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
357 changed files with 38591 additions and 104 deletions

View file

@ -207,7 +207,8 @@
"react-color": "^2.13.8",
"react-dom": "^16.8.0",
"react-grid-layout": "^0.16.2",
"react-markdown": "^3.1.4",
"react-input-range": "^1.3.0",
"react-markdown": "^3.4.1",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-sizeme": "^2.3.6",
@ -272,6 +273,7 @@
"@types/bluebird": "^3.1.1",
"@types/boom": "^7.2.0",
"@types/chance": "^1.0.0",
"@types/cheerio": "^0.22.10",
"@types/chromedriver": "^2.38.0",
"@types/classnames": "^2.2.3",
"@types/d3": "^3.5.41",
@ -284,7 +286,7 @@
"@types/execa": "^0.9.0",
"@types/fetch-mock": "7.2.1",
"@types/getopts": "^2.0.1",
"@types/glob": "^5.0.35",
"@types/glob": "^7.1.1",
"@types/globby": "^8.0.0",
"@types/graphql": "^0.13.1",
"@types/hapi": "^17.0.18",
@ -320,7 +322,7 @@
"@types/rimraf": "^2.0.2",
"@types/selenium-webdriver": "^3.0.15",
"@types/semver": "^5.5.0",
"@types/sinon": "^5.0.1",
"@types/sinon": "^7.0.0",
"@types/strip-ansi": "^3.0.0",
"@types/styled-components": "^3.0.1",
"@types/supertest": "^2.0.5",
@ -398,7 +400,7 @@
"multistream": "^2.1.1",
"murmurhash3js": "3.0.1",
"mutation-observer": "^1.0.3",
"nock": "8.0.0",
"nock": "10.0.4",
"node-sass": "^4.9.4",
"normalize-path": "^3.0.0",
"pixelmatch": "4.0.2",
@ -412,7 +414,7 @@
"sass-lint": "^1.12.1",
"selenium-webdriver": "^4.0.0-alpha.1",
"simple-git": "1.37.0",
"sinon": "^5.0.7",
"sinon": "^7.2.2",
"strip-ansi": "^3.0.1",
"supertest": "^3.1.0",
"supertest-as-promised": "^4.0.2",

View file

@ -65,7 +65,7 @@
"redux": "3.7.2",
"redux-thunk": "2.2.0",
"sass-loader": "^7.1.0",
"sinon": "^5.0.7",
"sinon": "^7.2.2",
"style-loader": "^0.23.1",
"webpack": "^4.23.1",
"webpack-dev-server": "^3.1.10",

View file

@ -181,6 +181,7 @@ export default class ClusterManager {
/[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/,
/\.test\.js$/,
...extraIgnores,
'plugins/java_languageserver'
],
});

View file

@ -132,7 +132,7 @@ export function extractArchive(archive, targetDir, extractPath) {
return reject(err);
}
readStream.pipe(createWriteStream(fileName));
readStream.pipe(createWriteStream(fileName, { mode: entry.externalFileAttributes >>> 16 }));
readStream.on('end', function () {
zipfile.readEntry();
});

View file

@ -21,6 +21,7 @@ import rimraf from 'rimraf';
import path from 'path';
import os from 'os';
import glob from 'glob';
import fs from 'fs';
import { analyzeArchive, extractArchive, _isDirectory } from './zip';
describe('kibana cli', function () {
@ -72,6 +73,28 @@ describe('kibana cli', function () {
});
});
describe('checkFilePermission', () => {
it('verify consistency of modes of files', async () => {
const archivePath = path.resolve(repliesPath, 'test_plugin.zip');
await extractArchive(archivePath, tempPath, 'kibana/libs');
const files = await glob.sync('**/*', { cwd: tempPath });
const expected = [
'executable',
'unexecutable'
];
expect(files.sort()).toEqual(expected.sort());
const executableMode = '0' + (fs.statSync(path.resolve(tempPath, 'executable')).mode & parseInt('777', 8)).toString(8);
const unExecutableMode = '0' + (fs.statSync(path.resolve(tempPath, 'unexecutable')).mode & parseInt('777', 8)).toString(8);
expect(executableMode).toEqual('0755');
expect(unExecutableMode).toEqual('0644');
});
});
it('handles a corrupt zip archive', async () => {
try {
await extractArchive(path.resolve(repliesPath, 'corrupt.zip'));

View file

@ -44,6 +44,7 @@ import {
ExtractNodeBuildsTask,
InstallDependenciesTask,
OptimizeBuildTask,
PatchNativeModulesTask,
RemovePackageJsonDepsTask,
RemoveWorkspacesTask,
TranspileBabelTask,
@ -131,6 +132,7 @@ export async function buildDistributables(options) {
* directories and perform platform-specific steps
*/
await run(CreateArchivesSourcesTask);
await run(PatchNativeModulesTask);
await run(CleanExtraBinScriptsTask);
await run(CleanExtraBrowsersTask);
await run(CleanNodeBuildsTask);

View file

@ -37,4 +37,5 @@ export * from './typecheck_typescript_task';
export * from './transpile_scss_task';
export * from './verify_env_task';
export * from './write_sha_sums_task';
export * from './patch_native_modules_task';
export * from './path_length_task';

View file

@ -0,0 +1,91 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { scanCopy, untar, deleteAll } from '../lib';
import { createWriteStream } from 'fs';
import { binaryInfo } from '../../../../x-pack/plugins/code/tasks/nodegit_info';
import wreck from 'wreck';
import mkdirp from 'mkdirp';
import { dirname, join, basename } from 'path';
import { createPromiseFromStreams } from '../../../legacy/utils/streams';
async function download(url, destination, log) {
const response = await wreck.request('GET', url);
if (response.statusCode !== 200) {
throw new Error(
`Unexpected status code ${response.statusCode} when downloading ${url}`
);
}
mkdirp.sync(dirname(destination));
await createPromiseFromStreams([
response,
createWriteStream(destination)
]);
log.debug('Downloaded ', url);
}
async function downloadAndExtractTarball(url, dest, log, retry) {
try {
await download(url, dest, log);
const extractDir = join(dirname(dest), basename(dest, '.tar.gz'));
await untar(dest, extractDir, {
strip: 1
});
return extractDir;
} catch (e) {
if (retry > 0) {
await downloadAndExtractTarball(url, dest, log, retry - 1);
} else {
throw e;
}
}
}
async function patchNodeGit(config, log, build, platform) {
const plat = platform.isWindows() ? 'win32' : platform.getName();
const arch = platform.getNodeArch().split('-')[1];
const { downloadUrl, packageName } = binaryInfo(plat, arch);
const downloadPath = build.resolvePathForPlatform(platform, '.nodegit_binaries', packageName);
const extractDir = await downloadAndExtractTarball(downloadUrl, downloadPath, log, 3);
const destination = build.resolvePathForPlatform(platform, 'node_modules/nodegit/build/Release');
log.debug('Replacing nodegit binaries from ', extractDir);
await deleteAll([destination], log);
await scanCopy({
source: extractDir,
destination: destination,
time: new Date(),
});
await deleteAll([extractDir, downloadPath], log);
}
export const PatchNativeModulesTask = {
description: 'Patching platform-specific native modules directories',
async run(config, log, build) {
await Promise.all(config.getTargetPlatforms().map(async platform => {
if (!build.isOss()) {
await patchNodeGit(config, log, build, platform);
}
}));
}
};

View file

@ -72,6 +72,13 @@ function createBreadcrumbsApi(chrome: { [key: string]: any }) {
filter(fn: (breadcrumb: Breadcrumb, i: number, all: Breadcrumb[]) => boolean) {
newPlatformChrome.setBreadcrumbs(currentBreadcrumbs.filter(fn));
},
/**
* Remove last element of the breadcrumb
*/
pop() {
newPlatformChrome.setBreadcrumbs(currentBreadcrumbs.slice(0, -1));
},
},
};
}

View file

@ -18,6 +18,7 @@ import { dashboardMode } from './plugins/dashboard_mode';
import { logstash } from './plugins/logstash';
import { beats } from './plugins/beats_management';
import { apm } from './plugins/apm';
import { code } from './plugins/code';
import { maps } from './plugins/maps';
import { licenseManagement } from './plugins/license_management';
import { cloud } from './plugins/cloud';
@ -55,6 +56,7 @@ module.exports = function (kibana) {
logstash(kibana),
beats(kibana),
apm(kibana),
code(kibana),
maps(kibana),
canvas(kibana),
licenseManagement(kibana),

View file

@ -42,6 +42,7 @@
"@storybook/react": "^5.0.5",
"@storybook/theming": "^5.0.5",
"@types/angular": "1.6.50",
"@types/boom": "^7.2.0",
"@types/base64-js": "^1.2.5",
"@types/cheerio": "^0.22.10",
"@types/chroma-js": "^1.4.1",
@ -52,32 +53,46 @@
"@types/d3-time": "^1.0.7",
"@types/d3-time-format": "^2.1.0",
"@types/elasticsearch": "^5.0.30",
"@types/git-url-parse": "^9.0.0",
"@types/glob": "^7.1.1",
"@types/file-saver": "^2.0.0",
"@types/graphql": "^0.13.1",
"@types/hapi-auth-cookie": "^9.1.0",
"@types/history": "^4.6.2",
"@types/jest": "^24.0.9",
"@types/joi": "^13.4.2",
"@types/js-yaml": "^3.11.1",
"@types/json-stable-stringify": "^1.0.32",
"@types/jsonwebtoken": "^7.2.7",
"@types/lodash": "^3.10.1",
"@types/mkdirp": "^0.5.2",
"@types/mime": "^2.0.1",
"@types/mocha": "^5.2.6",
"@types/nock": "^9.3.0",
"@types/node": "^10.12.27",
"@types/node-fetch": "^2.1.4",
"@types/object-hash": "^1.2.0",
"@types/papaparse": "^4.5.5",
"@types/pngjs": "^3.3.1",
"@types/prop-types": "^15.5.3",
"@types/proper-lockfile": "^3.0.0",
"@types/react": "^16.8.0",
"@types/react-dom": "^16.8.0",
"@types/react-redux": "^6.0.6",
"@types/react-router-dom": "^4.3.1",
"@types/react-test-renderer": "^16.8.0",
"@types/recompose": "^0.30.2",
"@types/reduce-reducers": "^0.1.3",
"@types/sinon": "^5.0.1",
"@types/redux-actions": "^2.2.1",
"@types/rimraf": "^2.0.2",
"@types/sinon": "^7.0.0",
"@types/storybook__addon-actions": "^3.4.2",
"@types/storybook__addon-info": "^4.1.1",
"@types/storybook__addon-knobs": "^4.0.4",
"@types/storybook__react": "^4.0.1",
"@types/styled-components": "^3.0.1",
"@types/supertest": "^2.0.5",
"@types/tar-fs": "^1.16.1",
"@types/tinycolor2": "^1.4.1",
"@types/uuid": "^3.4.4",
"abab": "^1.0.4",
@ -133,7 +148,7 @@
"run-sequence": "^2.2.1",
"sass-loader": "^7.1.0",
"simple-git": "1.37.0",
"sinon": "^5.0.7",
"sinon": "^7.2.2",
"string-replace-loader": "^2.1.1",
"supertest": "^3.1.0",
"supertest-as-promised": "^4.0.2",
@ -151,7 +166,10 @@
"@babel/runtime": "^7.3.4",
"@elastic/datemath": "5.0.2",
"@elastic/eui": "10.1.0",
"@elastic/javascript-typescript-langserver": "^0.1.23",
"@elastic/lsp-extension": "^0.1.1",
"@elastic/node-crypto": "0.1.2",
"@elastic/nodegit": "0.25.0-alpha.12",
"@elastic/numeral": "2.3.3",
"@kbn/babel-preset": "1.0.0",
"@kbn/elastic-idx": "1.0.0",
@ -195,26 +213,33 @@
"elasticsearch": "^15.4.1",
"extract-zip": "1.5.0",
"file-saver": "^1.3.8",
"file-type": "^10.9.0",
"font-awesome": "4.4.0",
"formsy-react": "^1.1.5",
"get-port": "2.1.0",
"get-port": "4.2.0",
"getos": "^3.1.0",
"glob": "6.0.4",
"git-url-parse": "11.1.2",
"github-markdown-css": "^2.10.0",
"glob": "^7.1.2",
"graphql": "^0.13.2",
"graphql-fields": "^1.0.2",
"graphql-tag": "^2.9.2",
"graphql-tools": "^3.0.2",
"h2o2": "^8.1.2",
"handlebars": "^4.0.13",
"hapi-auth-cookie": "^9.0.0",
"history": "4.7.2",
"history-extra": "^4.0.2",
"humps": "2.0.1",
"icalendar": "0.7.1",
"idx": "^2.5.2",
"immer": "^1.5.0",
"inline-style": "^2.0.0",
"intl": "^1.2.5",
"io-ts": "^1.4.2",
"joi": "^13.5.2",
"jquery": "^3.4.0",
"js-yaml": "3.4.1",
"json-stable-stringify": "^1.0.1",
"jsonwebtoken": "^8.3.0",
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
@ -234,7 +259,9 @@
"moment": "^2.20.1",
"moment-duration-format": "^1.3.0",
"moment-timezone": "^0.5.14",
"monaco-editor": "^0.14.3",
"ngreact": "^0.5.1",
"nock": "10.0.4",
"node-fetch": "^2.1.2",
"nodemailer": "^4.6.4",
"object-hash": "^1.3.1",
@ -245,7 +272,9 @@
"pluralize": "3.1.0",
"pngjs": "3.3.1",
"polished": "^1.9.2",
"popper.js": "^1.14.3",
"prop-types": "^15.6.0",
"proper-lockfile": "^3.0.2",
"puid": "1.0.5",
"puppeteer-core": "^1.13.0",
"raw-loader": "0.5.1",
@ -257,6 +286,7 @@
"react-dom": "^16.8.0",
"react-dropzone": "^4.2.9",
"react-fast-compare": "^2.0.4",
"react-markdown": "^3.4.1",
"react-markdown-renderer": "^1.4.0",
"react-portal": "^3.2.0",
"react-redux": "^5.0.7",
@ -272,6 +302,7 @@
"redux": "4.0.0",
"redux-actions": "2.2.1",
"redux-observable": "^1.0.0",
"redux-saga": "^0.16.0",
"redux-thunk": "2.3.0",
"redux-thunks": "^1.0.0",
"request": "^2.88.0",
@ -282,6 +313,7 @@
"rxjs": "^6.2.1",
"semver": "5.1.0",
"squel": "^5.12.2",
"stats-lite": "^2.2.0",
"style-it": "2.1.2",
"styled-components": "3.3.3",
"tar-fs": "1.13.0",
@ -297,6 +329,9 @@
"unstated": "^2.1.1",
"uuid": "3.0.1",
"venn.js": "0.2.9",
"vscode-jsonrpc": "^3.6.2",
"vscode-languageserver": "^4.2.1",
"vscode-languageserver-types": "^3.10.0",
"xml2js": "^0.4.19",
"xregexp": "3.2.0"
},

View file

@ -0,0 +1,19 @@
/*
* 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 GitBlame {
committer: {
name: string;
email: string;
};
startLine: number;
lines: number;
commit: {
id: string;
message: string;
date: string;
};
}

View file

@ -0,0 +1,35 @@
/*
* 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 { CommitInfo } from '../model/commit';
export interface Diff {
additions: number;
deletions: number;
files: FileDiff[];
}
export interface CommitDiff extends Diff {
commit: CommitInfo;
}
export interface FileDiff {
path: string;
originPath?: string;
kind: DiffKind;
originCode?: string;
modifiedCode?: string;
language?: string;
additions: number;
deletions: number;
}
export enum DiffKind {
ADDED = 'ADDED',
DELETED = 'DELETED',
MODIFIED = 'MODIFIED',
RENAMED = 'RENAMED',
}

View file

@ -0,0 +1,55 @@
/*
* 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 { validateGitUrl } from './git_url_utils';
test('Git url validation', () => {
// An url ends with .git
expect(validateGitUrl('https://github.com/elastic/elasticsearch.git')).toBeTruthy();
// An url ends without .git
expect(validateGitUrl('https://github.com/elastic/elasticsearch')).toBeTruthy();
// An url with http://
expect(validateGitUrl('http://github.com/elastic/elasticsearch')).toBeTruthy();
// An url with ssh://
expect(validateGitUrl('ssh://elastic@github.com/elastic/elasticsearch.git')).toBeTruthy();
// An url with ssh:// and port
expect(validateGitUrl('ssh://elastic@github.com:9999/elastic/elasticsearch.git')).toBeTruthy();
// An url with git://
expect(validateGitUrl('git://elastic@github.com/elastic/elasticsearch.git')).toBeTruthy();
// An url with an invalid protocol
expect(() => {
validateGitUrl('file:///Users/elastic/elasticsearch', [], ['ssh', 'https', 'git']);
}).toThrow('Git url protocol is not whitelisted.');
// An url without protocol
expect(() => {
validateGitUrl('/Users/elastic/elasticsearch', [], ['ssh', 'https', 'git']);
}).toThrow('Git url protocol is not whitelisted.');
expect(() => {
validateGitUrl('github.com/elastic/elasticsearch', [], ['ssh', 'https', 'git']);
}).toThrow('Git url protocol is not whitelisted.');
// An valid git url but without whitelisted host
expect(() => {
validateGitUrl('https://github.com/elastic/elasticsearch.git', ['gitlab.com']);
}).toThrow('Git url host is not whitelisted.');
// An valid git url but without whitelisted protocol
expect(() => {
validateGitUrl('https://github.com/elastic/elasticsearch.git', [], ['ssh']);
}).toThrow('Git url protocol is not whitelisted.');
// An valid git url with both whitelisted host and protocol
expect(
validateGitUrl('https://github.com/elastic/elasticsearch.git', ['github.com'], ['https'])
).toBeTruthy();
});

View file

@ -0,0 +1,32 @@
/*
* 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 GitUrlParse from 'git-url-parse';
// return true if the git url is valid, otherwise throw Error with
// exact reasons.
export function validateGitUrl(
url: string,
hostWhitelist?: string[],
protocolWhitelist?: string[]
): boolean {
const repo = GitUrlParse(url);
if (hostWhitelist && hostWhitelist.length > 0) {
const hostSet = new Set(hostWhitelist);
if (!hostSet.has(repo.source)) {
throw new Error('Git url host is not whitelisted.');
}
}
if (protocolWhitelist && protocolWhitelist.length > 0) {
const protocolSet = new Set(protocolWhitelist);
if (!protocolSet.has(repo.protocol)) {
throw new Error('Git url protocol is not whitelisted.');
}
}
return true;
}

View file

@ -0,0 +1,26 @@
/*
* 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 enum InstallationType {
Embed,
Download,
Plugin,
}
export enum InstallEventType {
DOWNLOADING,
UNPACKING,
DONE,
FAIL,
}
export interface InstallEvent {
langServerName: string;
eventType: InstallEventType;
progress?: number;
message?: string;
params?: any;
}

View file

@ -0,0 +1,25 @@
/*
* 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 { InstallationType } from './installation';
export enum LanguageServerStatus {
NOT_INSTALLED,
INSTALLING,
READY, // installed but not running
RUNNING,
}
export interface LanguageServer {
name: string;
languages: string[];
installationType: InstallationType;
version?: string;
build?: string;
status?: LanguageServerStatus;
downloadUrl?: any;
pluginName?: string;
}

View file

@ -0,0 +1,37 @@
/*
* 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 _ from 'lodash';
import { SourceLocation } from '../model';
export class LineMapper {
private lines: string[];
private acc: number[];
constructor(content: string) {
this.lines = content.split('\n');
this.acc = [0];
this.getLocation = this.getLocation.bind(this);
for (let i = 0; i < this.lines.length - 1; i++) {
this.acc[i + 1] = this.acc[i] + this.lines[i].length + 1;
}
}
public getLocation(offset: number): SourceLocation {
let line = _.sortedIndex(this.acc, offset);
if (offset !== this.acc[line]) {
line -= 1;
}
const column = offset - this.acc[line];
return { line, column, offset };
}
public getLines(): string[] {
return this.lines;
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 { ResponseError, ResponseMessage } from 'vscode-jsonrpc/lib/messages';
export { TextDocumentMethods } from './text_document_methods';
import { kfetch } from 'ui/kfetch';
export interface LspClient {
sendRequest(method: string, params: any, singal?: AbortSignal): Promise<ResponseMessage>;
}
export class LspRestClient implements LspClient {
private baseUri: string;
constructor(baseUri: string) {
this.baseUri = baseUri;
}
public async sendRequest(
method: string,
params: any,
signal?: AbortSignal
): Promise<ResponseMessage> {
try {
const response = await kfetch({
pathname: `${this.baseUri}/${method}`,
method: 'POST',
body: JSON.stringify(params),
signal,
});
return response as ResponseMessage;
} catch (e) {
let error = e;
if (error.body && error.body.error) {
error = error.body.error;
}
throw new ResponseError<any>(error.code, error.message, error.data);
}
}
}

View file

@ -0,0 +1,13 @@
/*
* 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 { ErrorCodes } from 'vscode-jsonrpc/lib/messages';
export const ServerNotInitialized: number = ErrorCodes.ServerNotInitialized;
export const UnknownErrorCode: number = ErrorCodes.UnknownErrorCode;
export const UnknownFileLanguage: number = -42404;
export const LanguageServerNotInstalled: number = -42403;
export const LanguageDisabled: number = -42402;

View file

@ -0,0 +1,39 @@
/*
* 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 { AsyncTask } from '../public/monaco/computer';
import { LspClient } from './lsp_client';
export class LspMethod<INPUT, OUTPUT> {
private client: LspClient;
private method: string;
constructor(method: string, client: LspClient) {
this.client = client;
this.method = method;
}
public asyncTask(input: INPUT): AsyncTask<OUTPUT> {
const abortController = new AbortController();
const promise = () => {
return this.client
.sendRequest(this.method, input, abortController.signal)
.then(result => result.result as OUTPUT);
};
return {
cancel() {
abortController.abort();
},
promise,
};
}
public async send(input: INPUT): Promise<OUTPUT> {
return await this.client
.sendRequest(this.method, input)
.then(result => result.result as OUTPUT);
}
}

View file

@ -0,0 +1,206 @@
/*
* 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 { FileTreeItemType } from '../model';
import { RepositoryUtils } from './repository_utils';
test('Repository url parsing', () => {
// Valid git url without .git suffix.
const repo1 = RepositoryUtils.buildRepository('https://github.com/apache/sqoop');
expect(repo1).toEqual({
uri: 'github.com/apache/sqoop',
url: 'https://github.com/apache/sqoop',
name: 'sqoop',
org: 'apache',
protocol: 'https',
});
// Valid git url with .git suffix.
const repo2 = RepositoryUtils.buildRepository('https://github.com/apache/sqoop.git');
expect(repo2).toEqual({
uri: 'github.com/apache/sqoop',
url: 'https://github.com/apache/sqoop.git',
name: 'sqoop',
protocol: 'https',
org: 'apache',
});
// An invalid git url
const repo3 = RepositoryUtils.buildRepository('github.com/apache/sqoop');
expect(repo3).toMatchObject({
uri: 'github.com/apache/sqoop',
url: 'github.com/apache/sqoop',
});
const repo4 = RepositoryUtils.buildRepository('git://a/b');
expect(repo4).toEqual({
uri: 'a/_/b',
url: 'git://a/b',
name: 'b',
org: '_',
protocol: 'git',
});
const repo5 = RepositoryUtils.buildRepository('git://a/b/c');
expect(repo5).toEqual({
uri: 'a/b/c',
url: 'git://a/b/c',
name: 'c',
org: 'b',
protocol: 'git',
});
const repo6 = RepositoryUtils.buildRepository('git@github.com:foo/bar.git');
expect(repo6).toEqual({
uri: 'github.com/foo/bar',
url: 'git@github.com:foo/bar.git',
name: 'bar',
protocol: 'ssh',
org: 'foo',
});
const repo7 = RepositoryUtils.buildRepository('ssh://git@github.com:foo/bar.git');
expect(repo7).toEqual({
uri: 'github.com/foo/bar',
url: 'ssh://git@github.com:foo/bar.git',
name: 'bar',
org: 'foo',
protocol: 'ssh',
});
});
test('Repository url parsing with non standard segments', () => {
const repo1 = RepositoryUtils.buildRepository('git://a/b/c/d');
expect(repo1).toEqual({
uri: 'a/b_c/d',
url: 'git://a/b/c/d',
name: 'd',
org: 'b_c',
protocol: 'git',
});
const repo2 = RepositoryUtils.buildRepository('git://a/b/c/d/e');
expect(repo2).toEqual({
uri: 'a/b_c_d/e',
url: 'git://a/b/c/d/e',
name: 'e',
org: 'b_c_d',
protocol: 'git',
});
const repo3 = RepositoryUtils.buildRepository('git://a');
expect(repo3).toEqual({
uri: 'a/_/_',
url: 'git://a',
name: '_',
protocol: 'git',
org: '_',
});
});
test('Repository url parsing with port', () => {
const repo1 = RepositoryUtils.buildRepository('ssh://mine@mydomain.com:27017/gitolite-admin');
expect(repo1).toEqual({
uri: 'mydomain.com:27017/mine/gitolite-admin',
url: 'ssh://mine@mydomain.com:27017/gitolite-admin',
name: 'gitolite-admin',
org: 'mine',
protocol: 'ssh',
});
const repo2 = RepositoryUtils.buildRepository(
'ssh://mine@mydomain.com:27017/elastic/gitolite-admin'
);
expect(repo2).toEqual({
uri: 'mydomain.com:27017/elastic/gitolite-admin',
url: 'ssh://mine@mydomain.com:27017/elastic/gitolite-admin',
name: 'gitolite-admin',
protocol: 'ssh',
org: 'elastic',
});
});
test('Normalize repository index name', () => {
const indexName1 = RepositoryUtils.normalizeRepoUriToIndexName('github.com/elastic/Kibana');
const indexName2 = RepositoryUtils.normalizeRepoUriToIndexName('github.com/elastic/kibana');
expect(indexName1 === indexName2).toBeFalsy();
expect(indexName1).toEqual('github.com-elastic-kibana-e2b881a9');
expect(indexName2).toEqual('github.com-elastic-kibana-7bf00473');
const indexName3 = RepositoryUtils.normalizeRepoUriToIndexName('github.com/elastic-kibana/code');
const indexName4 = RepositoryUtils.normalizeRepoUriToIndexName('github.com/elastic/kibana-code');
expect(indexName3 === indexName4).toBeFalsy();
});
test('Parse repository uri', () => {
expect(RepositoryUtils.orgNameFromUri('github.com/elastic/kibana')).toEqual('elastic');
expect(RepositoryUtils.repoNameFromUri('github.com/elastic/kibana')).toEqual('kibana');
expect(RepositoryUtils.repoFullNameFromUri('github.com/elastic/kibana')).toEqual(
'elastic/kibana'
);
// For invalid repository uri
expect(() => {
RepositoryUtils.orgNameFromUri('foo/bar');
}).toThrowError('Invalid repository uri.');
expect(() => {
RepositoryUtils.repoNameFromUri('foo/bar');
}).toThrowError('Invalid repository uri.');
expect(() => {
RepositoryUtils.repoFullNameFromUri('foo/bar');
}).toThrowError('Invalid repository uri.');
});
test('Repository local path', () => {
expect(RepositoryUtils.repositoryLocalPath('/tmp', 'github.com/elastic/kibana')).toEqual(
'/tmp/github.com/elastic/kibana'
);
expect(RepositoryUtils.repositoryLocalPath('tmp', 'github.com/elastic/kibana')).toEqual(
'tmp/github.com/elastic/kibana'
);
});
test('Parse location to url', () => {
expect(
RepositoryUtils.locationToUrl({
uri: 'git://github.com/elastic/eui/blob/master/generator-eui/app/component.js',
range: {
start: {
line: 4,
character: 17,
},
end: {
line: 27,
character: 1,
},
},
})
).toEqual('/github.com/elastic/eui/blob/master/generator-eui/app/component.js!L5:17');
});
test('Get all files from a repository file tree', () => {
expect(
RepositoryUtils.getAllFiles({
name: 'foo',
type: FileTreeItemType.Directory,
path: '/foo',
children: [
{
name: 'bar',
type: FileTreeItemType.File,
path: '/foo/bar',
},
{
name: 'boo',
type: FileTreeItemType.File,
path: '/foo/boo',
},
],
childrenCount: 2,
})
).toEqual(['/foo/bar', '/foo/boo']);
});

View file

@ -0,0 +1,114 @@
/*
* 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 crypto from 'crypto';
import GitUrlParse from 'git-url-parse';
import path from 'path';
import { Location } from 'vscode-languageserver';
import { CloneProgress, FileTree, FileTreeItemType, Repository, RepositoryUri } from '../model';
import { parseLspUrl, toCanonicalUrl } from './uri_util';
export class RepositoryUtils {
// Generate a Repository instance by parsing repository remote url
public static buildRepository(remoteUrl: string): Repository {
const repo = GitUrlParse(remoteUrl);
let host = repo.source ? repo.source : '';
if (repo.port !== null) {
host = host + ':' + repo.port;
}
const name = repo.name ? repo.name : '_';
const org = repo.owner ? repo.owner.split('/').join('_') : '_';
const uri: RepositoryUri = host ? `${host}/${org}/${name}` : repo.full_name;
return {
uri,
url: repo.href as string,
name,
org,
protocol: repo.protocol,
};
}
// From uri 'origin/org/name' to 'org'
public static orgNameFromUri(repoUri: RepositoryUri): string {
const segs = repoUri.split('/');
if (segs && segs.length === 3) {
return segs[1];
}
throw new Error('Invalid repository uri.');
}
// From uri 'origin/org/name' to 'name'
public static repoNameFromUri(repoUri: RepositoryUri): string {
const segs = repoUri.split('/');
if (segs && segs.length === 3) {
return segs[2];
}
throw new Error('Invalid repository uri.');
}
// From uri 'origin/org/name' to 'org/name'
public static repoFullNameFromUri(repoUri: RepositoryUri): string {
const segs = repoUri.split('/');
if (segs && segs.length === 3) {
return segs[1] + '/' + segs[2];
}
throw new Error('Invalid repository uri.');
}
// Return the local data path of a given repository.
public static repositoryLocalPath(repoPath: string, repoUri: RepositoryUri) {
return path.join(repoPath, repoUri);
}
public static normalizeRepoUriToIndexName(repoUri: RepositoryUri) {
const hash = crypto
.createHash('md5')
.update(repoUri)
.digest('hex')
.substring(0, 8);
const segs: string[] = repoUri.split('/');
segs.push(hash);
// Elasticsearch index name is case insensitive
return segs.join('-').toLowerCase();
}
public static locationToUrl(loc: Location) {
const url = parseLspUrl(loc.uri);
const { repoUri, file, revision } = url;
if (repoUri && file && revision) {
return toCanonicalUrl({ repoUri, file, revision, position: loc.range.start });
}
return '';
}
public static getAllFiles(fileTree: FileTree): string[] {
if (!fileTree) {
return [];
}
let result: string[] = [];
switch (fileTree.type) {
case FileTreeItemType.File:
result.push(fileTree.path!);
break;
case FileTreeItemType.Directory:
for (const node of fileTree.children!) {
result = result.concat(RepositoryUtils.getAllFiles(node));
}
break;
default:
break;
}
return result;
}
public static hasFullyCloned(cloneProgress?: CloneProgress | null): boolean {
return !!cloneProgress && cloneProgress.isCloned !== undefined && cloneProgress.isCloned;
}
}

View file

@ -0,0 +1,36 @@
/*
* 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 { SymbolLocator } from '@elastic/lsp-extension';
import { TextDocumentPositionParams } from 'vscode-languageserver';
import {
Definition,
DocumentSymbolParams,
Hover,
Location,
SymbolInformation,
} from 'vscode-languageserver-types';
import { LspClient } from './lsp_client';
import { LspMethod } from './lsp_method';
export class TextDocumentMethods {
public documentSymbol: LspMethod<DocumentSymbolParams, SymbolInformation[]>;
public hover: LspMethod<TextDocumentPositionParams, Hover>;
public definition: LspMethod<TextDocumentPositionParams, Definition>;
public edefinition: LspMethod<TextDocumentPositionParams, SymbolLocator[]>;
public references: LspMethod<TextDocumentPositionParams, Location[]>;
private readonly client: LspClient;
constructor(client: LspClient) {
this.client = client;
this.documentSymbol = new LspMethod('textDocument/documentSymbol', this.client);
this.hover = new LspMethod('textDocument/hover', this.client);
this.definition = new LspMethod('textDocument/definition', this.client);
this.edefinition = new LspMethod('textDocument/edefinition', this.client);
this.references = new LspMethod('textDocument/references', this.client);
}
}

View file

@ -0,0 +1,86 @@
/*
* 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 { RepositoryUri } from '../model';
import { parseLspUrl, toCanonicalUrl, toRepoName, toRepoNameWithOrg } from './uri_util';
test('parse a complete uri', () => {
const fullUrl =
'git://github.com/Microsoft/vscode/blob/f2e49a2/src/vs/base/parts/ipc/test/node/ipc.net.test.ts';
const result = parseLspUrl(fullUrl);
expect(result).toEqual({
uri:
'/github.com/Microsoft/vscode/blob/f2e49a2/src/vs/base/parts/ipc/test/node/ipc.net.test.ts',
repoUri: 'github.com/Microsoft/vscode',
pathType: 'blob',
revision: 'f2e49a2',
file: 'src/vs/base/parts/ipc/test/node/ipc.net.test.ts',
schema: 'git:',
});
});
test('parseLspUrl a uri without schema', () => {
const url =
'github.com/Microsoft/vscode/blob/f2e49a2/src/vs/base/parts/ipc/test/node/ipc.net.test.ts';
const result = parseLspUrl(url);
expect(result).toEqual({
uri:
'/github.com/Microsoft/vscode/blob/f2e49a2/src/vs/base/parts/ipc/test/node/ipc.net.test.ts',
repoUri: 'github.com/Microsoft/vscode',
pathType: 'blob',
revision: 'f2e49a2',
file: 'src/vs/base/parts/ipc/test/node/ipc.net.test.ts',
});
});
test('parseLspUrl a tree uri', () => {
const uri = 'github.com/Microsoft/vscode/tree/head/src';
const result = parseLspUrl(uri);
expect(result).toEqual({
uri: '/github.com/Microsoft/vscode/tree/head/src',
repoUri: 'github.com/Microsoft/vscode',
pathType: 'tree',
revision: 'head',
file: 'src',
});
});
test('touri', () => {
const uri =
'git://github.com/Microsoft/vscode/blob/f2e49a2/src/vs/base/parts/ipc/test/node/ipc.net.test.ts';
const result = parseLspUrl(uri);
expect(result).toEqual({
uri:
'/github.com/Microsoft/vscode/blob/f2e49a2/src/vs/base/parts/ipc/test/node/ipc.net.test.ts',
repoUri: 'github.com/Microsoft/vscode',
pathType: 'blob',
revision: 'f2e49a2',
file: 'src/vs/base/parts/ipc/test/node/ipc.net.test.ts',
schema: 'git:',
});
const convertBack = toCanonicalUrl(result!);
expect(convertBack).toEqual(uri);
});
test('toRepoName', () => {
const uri: RepositoryUri = 'github.com/elastic/elasticsearch';
expect(toRepoName(uri)).toEqual('elasticsearch');
const invalidUri: RepositoryUri = 'github.com/elastic/elasticsearch/invalid';
expect(() => {
toRepoName(invalidUri);
}).toThrow();
});
test('toRepoNameWithOrg', () => {
const uri: RepositoryUri = 'github.com/elastic/elasticsearch';
expect(toRepoNameWithOrg(uri)).toEqual('elastic/elasticsearch');
const invalidUri: RepositoryUri = 'github.com/elastic/elasticsearch/invalid';
expect(() => {
toRepoNameWithOrg(invalidUri);
}).toThrow();
});

View file

@ -0,0 +1,131 @@
/*
* 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 { Uri } from 'monaco-editor';
import pathToRegexp from 'path-to-regexp';
import { Position } from 'vscode-languageserver-types';
import { RepositoryUri } from '../model';
import { MAIN, MAIN_ROOT } from '../public/components/routes';
const mainRe = pathToRegexp(MAIN);
const mainRootRe = pathToRegexp(MAIN_ROOT);
export interface ParsedUrl {
schema?: string;
uri?: string;
}
export interface CompleteParsedUrl extends ParsedUrl {
repoUri: string;
revision: string;
pathType?: string;
file?: string;
schema?: string;
position?: Position;
}
export function parseSchema(url: string): { uri: string; schema?: string } {
let [schema, uri] = url.toString().split('//');
if (!uri) {
uri = schema;
// @ts-ignore
schema = undefined;
}
if (!uri.startsWith('/')) {
uri = '/' + uri;
}
return { uri, schema };
}
export function parseGoto(goto: string): Position | undefined {
const regex = /L(\d+)(:\d+)?$/;
const m = regex.exec(goto);
if (m) {
const line = parseInt(m[1], 10);
let character = 0;
if (m[2]) {
character = parseInt(m[2].substring(1), 10);
}
return {
line,
character,
};
}
}
export function parseLspUrl(url: Uri | string): CompleteParsedUrl {
const { schema, uri } = parseSchema(url.toString());
const mainParsed = mainRe.exec(uri);
const mainRootParsed = mainRootRe.exec(uri);
if (mainParsed) {
const [resource, org, repo, pathType, revision, file, goto] = mainParsed.slice(1);
let position;
if (goto) {
position = parseGoto(goto);
}
return {
uri: uri.replace(goto, ''),
repoUri: `${resource}/${org}/${repo}`,
pathType,
revision,
file,
schema,
position,
};
} else if (mainRootParsed) {
const [resource, org, repo, pathType, revision] = mainRootParsed.slice(1);
return {
uri,
repoUri: `${resource}/${org}/${repo}`,
pathType,
revision,
schema,
};
} else {
throw new Error('invalid url ' + url);
}
}
/*
* From RepositoryUri to repository name.
* e.g. github.com/elastic/elasticsearch -> elasticsearch
*/
export function toRepoName(uri: RepositoryUri): string {
const segs = uri.split('/');
if (segs.length !== 3) {
throw new Error(`Invalid repository uri ${uri}`);
}
return segs[2];
}
/*
* From RepositoryUri to repository name with organization prefix.
* e.g. github.com/elastic/elasticsearch -> elastic/elasticsearch
*/
export function toRepoNameWithOrg(uri: RepositoryUri): string {
const segs = uri.split('/');
if (segs.length !== 3) {
throw new Error(`Invalid repository uri ${uri}`);
}
return `${segs[1]}/${segs[2]}`;
}
const compiled = pathToRegexp.compile(MAIN);
export function toCanonicalUrl(lspUrl: CompleteParsedUrl) {
const [resource, org, repo] = lspUrl.repoUri!.split('/');
if (!lspUrl.pathType) {
lspUrl.pathType = 'blob';
}
let goto;
if (lspUrl.position) {
goto = `!L${lspUrl.position.line + 1}:${lspUrl.position.character}`;
}
const data = { resource, org, repo, path: lspUrl.file, goto, ...lspUrl };
const uri = decodeURIComponent(compiled(data));
return lspUrl.schema ? `${lspUrl.schema}/${uri}` : uri;
}

View file

@ -0,0 +1,76 @@
/*
* 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 JoiNamespace from 'joi';
import moment from 'moment';
import { resolve } from 'path';
import { init } from './server/init';
export const code = (kibana: any) =>
new kibana.Plugin({
require: ['kibana', 'elasticsearch', 'xpack_main'],
id: 'code',
configPrefix: 'xpack.code',
publicDir: resolve(__dirname, 'public'),
uiExports: {
app: {
title: 'Code (Beta)',
main: 'plugins/code/app',
euiIconType: 'codeApp',
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
},
config(Joi: typeof JoiNamespace) {
return Joi.object({
enabled: Joi.boolean().default(true),
queueIndex: Joi.string().default('.code_internal-worker-queue'),
// 1 hour by default.
queueTimeout: Joi.number().default(moment.duration(1, 'hour').asMilliseconds()),
// The frequency which update scheduler executes. 5 minutes by default.
updateFrequencyMs: Joi.number().default(moment.duration(5, 'minute').asMilliseconds()),
// The frequency which index scheduler executes. 1 day by default.
indexFrequencyMs: Joi.number().default(moment.duration(1, 'day').asMilliseconds()),
// The frequency which each repo tries to update. 1 hour by default.
updateRepoFrequencyMs: Joi.number().default(moment.duration(1, 'hour').asMilliseconds()),
// The frequency which each repo tries to index. 1 day by default.
indexRepoFrequencyMs: Joi.number().default(moment.duration(1, 'day').asMilliseconds()),
lsp: Joi.object({
// timeout of a request
requestTimeoutMs: Joi.number().default(moment.duration(10, 'second').asMilliseconds()),
// if we want the language server run in seperately
detach: Joi.boolean().default(false),
// whether we want to show more language server logs
verbose: Joi.boolean().default(false),
}).default(),
repos: Joi.array().default([]),
security: Joi.object({
enableMavenImport: Joi.boolean().default(true),
enableGradleImport: Joi.boolean().default(false),
installNodeDependency: Joi.boolean().default(true),
gitHostWhitelist: Joi.array()
.items(Joi.string())
.default([
'github.com',
'gitlab.com',
'bitbucket.org',
'gitbox.apache.org',
'eclipse.org',
]),
gitProtocolWhitelist: Joi.array()
.items(Joi.string())
.default(['https', 'git', 'ssh']),
enableGitCertCheck: Joi.boolean().default(true),
}).default(),
maxWorkspace: Joi.number().default(5), // max workspace folder for each language server
disableIndexScheduler: Joi.boolean().default(true), // Temp option to disable index scheduler.
enableGlobalReference: Joi.boolean().default(false), // Global reference as optional feature for now
codeNodeUrl: Joi.string(),
}).default();
},
init,
});

View file

@ -0,0 +1,27 @@
/*
* 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 CommitInfo {
updated: Date;
message: string;
committer: string;
id: string;
parents: string[];
}
export interface ReferenceInfo {
name: string;
reference: string;
commit: CommitInfo;
type: ReferenceType;
}
export enum ReferenceType {
BRANCH,
TAG,
REMOTE_BRANCH,
OTHER,
}

View file

@ -0,0 +1,19 @@
/*
* 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 type CodeLine = Token[];
export interface Token {
value: string;
scopes: string[];
range?: Range;
}
export interface Range {
start: number; // start pos in line
end: number;
pos?: number; // position in file
}

View file

@ -0,0 +1,13 @@
/*
* 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 * from './highlight';
export * from './search';
export * from './repository';
export * from './task';
export * from './lsp';
export * from './workspace';
export * from './socket';

View file

@ -0,0 +1,16 @@
/*
* 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 LspRequest {
method: string;
params: any;
documentUri?: string; // assert there is only one uri per request for now.
resolvedFilePath?: string;
workspacePath?: string;
workspaceRevision?: string;
isNotification?: boolean; // if this is a notification request that doesn't need response
timeoutForInitializeMs?: number; // If the language server is initialize, how many milliseconds should we wait for it. Default infinite.
}

View file

@ -0,0 +1,149 @@
/*
* 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 { IndexRequest } from './search';
export type RepositoryUri = string;
export interface Repository {
/** In the form of git://github.com/lambdalab/lambdalab */
uri: RepositoryUri;
/** Original Clone Url */
url: string;
name?: string;
org?: string;
defaultBranch?: string;
revision?: string;
protocol?: string;
// The timestamp of next update for this repository.
nextUpdateTimestamp?: Date;
// The timestamp of next index for this repository.
nextIndexTimestamp?: Date;
// The current indexed revision in Elasticsearch.
indexedRevision?: string;
}
export interface RepositoryConfig {
uri: RepositoryUri;
disableGo?: boolean;
disableJava?: boolean;
disableTypescript?: boolean;
}
export interface FileTree {
name: string;
type: FileTreeItemType;
/** Full Path of the tree, don't need to be set by the server */
path?: string;
/**
* Children of the file tree, if it is undefined, then it's a file, if it is null, it means it is a
* directory and its children haven't been evaluated.
*/
children?: FileTree[];
/**
* count of children nodes for current node, use this for pagination
*/
childrenCount?: number;
sha1?: string;
/**
* current repo uri
*/
repoUri?: string;
}
export function sortFileTree(a: FileTree, b: FileTree) {
if (a.type !== b.type) {
return b.type - a.type;
} else {
return a.name.localeCompare(b.name);
}
}
export enum FileTreeItemType {
File,
Directory,
Submodule,
Link,
}
export interface WorkerResult {
uri: string;
}
// TODO(mengwei): create a AbstractGitWorkerResult since we now have an
// AbstractGitWorker now.
export interface CloneWorkerResult extends WorkerResult {
repo: Repository;
}
export interface DeleteWorkerResult extends WorkerResult {
res: boolean;
}
export interface UpdateWorkerResult extends WorkerResult {
branch: string;
revision: string;
}
export enum IndexStatsKey {
File = 'file-added-count',
FileDeleted = 'file-deleted-count',
Symbol = 'symbol-added-count',
SymbolDeleted = 'symbol-deleted-count',
Reference = 'reference-added-count',
ReferenceDeleted = 'reference-deleted-count',
}
export type IndexStats = Map<IndexStatsKey, number>;
export interface IndexWorkerResult extends WorkerResult {
revision: string;
stats: IndexStats;
}
export enum WorkerReservedProgress {
INIT = 0,
COMPLETED = 100,
ERROR = -100,
TIMEOUT = -200,
}
export interface WorkerProgress {
// Job payload repository uri.
uri: string;
progress: number;
timestamp: Date;
revision?: string;
errorMessage?: string;
}
export interface CloneProgress {
isCloned?: boolean;
receivedObjects: number;
indexedObjects: number;
totalObjects: number;
localObjects: number;
totalDeltas: number;
indexedDeltas: number;
receivedBytes: number;
}
export interface CloneWorkerProgress extends WorkerProgress {
cloneProgress?: CloneProgress;
}
export interface IndexProgress {
type: string;
total: number;
success: number;
fail: number;
percentage: number;
checkpoint?: IndexRequest;
}
export interface IndexWorkerProgress extends WorkerProgress {
indexProgress?: IndexProgress;
}

View file

@ -0,0 +1,155 @@
/*
* 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 { DetailSymbolInformation } from '@elastic/lsp-extension';
import { IRange } from 'monaco-editor';
import { DiffKind } from '../common/git_diff';
import { Repository, SourceHit } from '../model';
import { RepositoryUri } from './repository';
export interface Document {
repoUri: RepositoryUri;
path: string;
content: string;
qnames: string[];
language?: string;
sha1?: string;
}
// The base interface of indexer requests
export interface IndexRequest {
repoUri: RepositoryUri;
}
// The request for LspIndexer
export interface LspIndexRequest extends IndexRequest {
localRepoPath: string; // The repository local file path
filePath: string; // The file path within the repository
revision: string; // The revision of the current repository
}
export interface LspIncIndexRequest extends LspIndexRequest {
originPath?: string;
kind: DiffKind;
originRevision: string;
}
// The request for RepositoryIndexer
export interface RepositoryIndexRequest extends IndexRequest {
repoUri: RepositoryUri;
}
// The base interface of any kind of search requests.
export interface SearchRequest {
query: string;
page: number;
resultsPerPage?: number;
}
export interface RepositorySearchRequest extends SearchRequest {
query: string;
repoScope?: RepositoryUri[];
}
export interface DocumentSearchRequest extends SearchRequest {
query: string;
// repoFilters is used for search within these repos but return
// search stats across all repositories.
repoFilters?: string[];
// repoScope hard limit the search coverage only to these repositories.
repoScope?: RepositoryUri[];
langFilters?: string[];
}
export interface SymbolSearchRequest extends SearchRequest {
query: string;
repoScope?: RepositoryUri[];
}
// The base interface of any kind of search result.
export interface SearchResult {
total: number;
took: number;
}
export interface RepositorySearchResult extends SearchResult {
repositories: Repository[];
from?: number;
page?: number;
totalPage?: number;
}
export interface SymbolSearchResult extends SearchResult {
// TODO: we migit need an additional data structure for symbol search result.
symbols: DetailSymbolInformation[];
}
// All the interfaces for search page
// The item of the search result stats. e.g. Typescript -> 123
export interface SearchResultStatsItem {
name: string;
value: number;
}
export interface SearchResultStats {
total: number; // Total number of results
from: number; // The beginning of the result range
to: number; // The end of the result range
page: number; // The page number
totalPage: number; // The total number of pages
repoStats: SearchResultStatsItem[];
languageStats: SearchResultStatsItem[];
}
export interface CompositeSourceContent {
content: string;
lineMapping: string[];
ranges: IRange[];
}
export interface SearchResultItem {
uri: string;
hits: number;
filePath: string;
language: string;
compositeContent: CompositeSourceContent;
}
export interface DocumentSearchResult extends SearchResult {
query: string;
from?: number;
page?: number;
totalPage?: number;
stats?: SearchResultStats;
results?: SearchResultItem[];
repoAggregations?: any[];
langAggregations?: any[];
}
export interface SourceLocation {
line: number;
column: number;
offset: number;
}
export interface SourceRange {
startLoc: SourceLocation;
endLoc: SourceLocation;
}
export interface SourceHit {
range: SourceRange;
score: number;
term: string;
}
export enum SearchScope {
DEFAULT = 'default', // Search everything
SYMBOL = 'symbol', // Only search symbols
REPOSITORY = 'repository', // Only search repositories
FILE = 'file', // Only search files
}

View file

@ -0,0 +1,12 @@
/*
* 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 enum SocketKind {
CLONE_PROGRESS = 'clone-progress',
DELETE_PROGRESS = 'delete-progress',
INDEX_PROGRESS = 'index-progress',
INSTALL_PROGRESS = 'install-progress',
}

View file

@ -0,0 +1,25 @@
/*
* 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 { RepositoryUri } from './repository';
/** Time consuming task that should be queued and executed seperately */
export interface Task {
repoUri: RepositoryUri;
type: TaskType;
/** Percentage of the task, 100 means task completed */
progress: number;
/** Revision of the repo that the task run on. May only apply to Index task */
revision?: string;
}
export enum TaskType {
Import,
Update,
Delete,
Index,
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
export interface Repo {
url: string;
path: string;
language: string;
}
export interface TestConfig {
repos: Repo[];
}
export enum RequestType {
INITIALIZE,
HOVER,
FULL,
}

View file

@ -0,0 +1,15 @@
/*
* 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 type RepoCmd = string | string[];
export interface RepoConfig {
repo: string;
init: RepoCmd;
}
export interface RepoConfigs {
[repoUri: string]: RepoConfig;
}

View file

@ -0,0 +1,18 @@
/*
* 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 { createAction } from 'redux-actions';
import { GitBlame } from '../../common/git_blame';
export interface LoadBlamePayload {
repoUri: string;
revision: string;
path: string;
}
export const loadBlame = createAction<LoadBlamePayload>('LOAD BLAME');
export const loadBlameSuccess = createAction<GitBlame[]>('LOAD BLAME SUCCESS');
export const loadBlameFailed = createAction<Error>('LOAD BLAME FAILED');

View file

@ -0,0 +1,12 @@
/*
* 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 { createAction } from 'redux-actions';
import { CommitDiff } from '../../common/git_diff';
export const loadCommit = createAction<string>('LOAD COMMIT');
export const loadCommitSuccess = createAction<CommitDiff>('LOAD COMMIT SUCCESS');
export const loadCommitFailed = createAction<Error>('LOAD COMMIT FAILED');

View file

@ -0,0 +1,37 @@
/*
* 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 { Range } from 'monaco-editor';
import { createAction } from 'redux-actions';
import { Hover, Position, TextDocumentPositionParams } from 'vscode-languageserver';
export interface ReferenceResults {
repos: GroupedRepoReferences[];
title: string;
}
export interface GroupedRepoReferences {
repo: string;
files: GroupedFileReferences[];
}
export interface GroupedFileReferences {
uri: string;
file: string;
language: string;
code: string;
lineNumbers: string[];
repo: string;
revision: string;
highlights: Range[];
}
export const findReferences = createAction<TextDocumentPositionParams>('FIND REFERENCES');
export const findReferencesSuccess = createAction<ReferenceResults>('FIND REFERENCES SUCCESS');
export const findReferencesFailed = createAction<Error>('FIND REFERENCES ERROR');
export const closeReferences = createAction<boolean>('CLOSE REFERENCES');
export const hoverResult = createAction<Hover>('HOVER RESULT');
export const revealPosition = createAction<Position | undefined>('REVEAL POSITION');

View file

@ -0,0 +1,77 @@
/*
* 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 { createAction } from 'redux-actions';
import { FileTree } from '../../model';
import { CommitInfo, ReferenceInfo } from '../../model/commit';
export interface FetchRepoPayload {
uri: string;
}
export interface FetchRepoPayloadWithRevision extends FetchRepoPayload {
revision: string;
}
export interface FetchFilePayload extends FetchRepoPayloadWithRevision {
path: string;
}
export interface FetchRepoTreePayload extends FetchFilePayload {
limit?: number;
parents?: boolean;
isDir: boolean;
}
export interface FetchFileResponse {
payload: FetchFilePayload;
isNotFound?: boolean;
content?: string;
lang?: string;
isImage?: boolean;
isUnsupported?: boolean;
isOversize?: boolean;
url?: string;
}
export interface RepoTreePayload {
tree: FileTree;
path: string;
withParents: boolean | undefined;
}
export const fetchRepoTree = createAction<FetchRepoTreePayload>('FETCH REPO TREE');
export const fetchRepoTreeSuccess = createAction<RepoTreePayload>('FETCH REPO TREE SUCCESS');
export const fetchRepoTreeFailed = createAction<Error>('FETCH REPO TREE FAILED');
export const resetRepoTree = createAction('CLEAR REPO TREE');
export const closeTreePath = createAction<string>('CLOSE TREE PATH');
export const openTreePath = createAction<string>('OPEN TREE PATH');
export const fetchRepoBranches = createAction<FetchRepoPayload>('FETCH REPO BRANCHES');
export const fetchRepoBranchesSuccess = createAction<ReferenceInfo[]>(
'FETCH REPO BRANCHES SUCCESS'
);
export const fetchRepoBranchesFailed = createAction<Error>('FETCH REPO BRANCHES FAILED');
export const fetchRepoCommits = createAction<FetchRepoPayloadWithRevision>('FETCH REPO COMMITS');
export const fetchRepoCommitsSuccess = createAction<CommitInfo[]>('FETCH REPO COMMITS SUCCESS');
export const fetchRepoCommitsFailed = createAction<Error>('FETCH REPO COMMITS FAILED');
export const fetchFile = createAction<FetchFilePayload>('FETCH FILE');
export const fetchFileSuccess = createAction<FetchFileResponse>('FETCH FILE SUCCESS');
export const fetchFileFailed = createAction<Error>('FETCH FILE ERROR');
export const fetchDirectory = createAction<FetchRepoTreePayload>('FETCH REPO DIR');
export const fetchDirectorySuccess = createAction<FileTree>('FETCH REPO DIR SUCCESS');
export const fetchDirectoryFailed = createAction<Error>('FETCH REPO DIR FAILED');
export const setNotFound = createAction<boolean>('SET NOT FOUND');
export const fetchTreeCommits = createAction<FetchFilePayload>('FETCH TREE COMMITS');
export const fetchTreeCommitsSuccess = createAction<{
path: string;
commits: CommitInfo[];
append?: boolean;
}>('FETCH TREE COMMITS SUCCESS');
export const fetchTreeCommitsFailed = createAction<Error>('FETCH TREE COMMITS FAILED');
export const fetchMoreCommits = createAction<string>('FETCH MORE COMMITS');

View file

@ -0,0 +1,30 @@
/*
* 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 { createAction } from 'redux-actions';
export * from './repository';
export * from './search';
export * from './file';
export * from './structure';
export * from './editor';
export * from './commit';
export * from './status';
export * from './project_config';
export * from './shortcuts';
export interface Match {
isExact?: boolean;
params: { [key: string]: string };
path: string;
url: string;
location: Location;
}
export const routeChange = createAction<Match>('CODE SEARCH ROUTE CHANGE');
export const checkSetupSuccess = createAction('SETUP CHECK SUCCESS');
export const checkSetupFailed = createAction('SETUP CHECK FAILED');

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { createAction } from 'redux-actions';
export const loadLanguageServers = createAction('LOAD LANGUAGE SERVERS');
export const loadLanguageServersSuccess = createAction<any>('LOAD LANGUAGE SERVERS SUCCESS');
export const loadLanguageServersFailed = createAction<Error>('LOAD LANGUAGE SERVERS FAILED');
export const requestInstallLanguageServer = createAction('REQUEST INSTALL LANGUAGE SERVERS');
export const requestInstallLanguageServerSuccess = createAction<any>(
'REQUEST INSTALL LANGUAGE SERVERS SUCCESS'
);
export const requestInstallLanguageServerFailed = createAction<Error>(
'REQUEST INSTALL LANGUAGE SERVERS FAILED'
);
export const installLanguageServerSuccess = createAction<any>('INSTALL LANGUAGE SERVERS SUCCESS');

View file

@ -0,0 +1,14 @@
/*
* 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 { createAction } from 'redux-actions';
import { RepositoryConfig } from '../../model';
export const loadConfigs = createAction('LOAD CONFIGS');
export const loadConfigsSuccess = createAction<{ [key: string]: RepositoryConfig }>(
'LOAD CONFIGS SUCCESS'
);
export const loadConfigsFailed = createAction<Error>('LOAD CONFIGS FAILED');

View file

@ -0,0 +1,11 @@
/*
* 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 { createAction } from 'redux-actions';
export const loadRecentProjects = createAction('LOAD RECENT PROJECTS');
export const loadRecentProjectsSuccess = createAction<any>('LOAD RECENT PROJECTS SUCCESS');
export const loadRecentProjectsFailed = createAction<Error>('LOAD RECENT PROJECTS FAILED');

View file

@ -0,0 +1,46 @@
/*
* 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 { createAction } from 'redux-actions';
import { Repository, RepositoryConfig } from '../../model';
import { RepoConfigs } from '../../model/workspace';
export interface RepoConfigPayload {
repoUri: string;
config: RepositoryConfig;
}
export const fetchRepos = createAction('FETCH REPOS');
export const fetchReposSuccess = createAction<Repository[]>('FETCH REPOS SUCCESS');
export const fetchReposFailed = createAction<Error>('FETCH REPOS FAILED');
export const deleteRepo = createAction<string>('DELETE REPOS');
export const deleteRepoSuccess = createAction<string>('DELETE REPOS SUCCESS');
export const deleteRepoFinished = createAction<string>('DELETE REPOS FINISHED');
export const deleteRepoFailed = createAction<Error>('DELETE REPOS FAILED');
export const indexRepo = createAction<string>('INDEX REPOS');
export const indexRepoSuccess = createAction<string>('INDEX REPOS SUCCESS');
export const indexRepoFailed = createAction<Error>('INDEX REPOS FAILED');
export const importRepo = createAction<string>('IMPORT REPO');
export const importRepoSuccess = createAction<string>('IMPORT REPO SUCCESS');
export const importRepoFailed = createAction<Error>('IMPORT REPO FAILED');
export const closeToast = createAction('CLOSE TOAST');
export const fetchRepoConfigs = createAction('FETCH REPO CONFIGS');
export const fetchRepoConfigSuccess = createAction<RepoConfigs>('FETCH REPO CONFIGS SUCCESS');
export const fetchRepoConfigFailed = createAction<Error>('FETCH REPO CONFIGS FAILED');
export const initRepoCommand = createAction<string>('INIT REPO CMD');
export const gotoRepo = createAction<string>('GOTO REPO');
export const switchLanguageServer = createAction<RepoConfigPayload>('SWITCH LANGUAGE SERVER');
export const switchLanguageServerSuccess = createAction('SWITCH LANGUAGE SERVER SUCCESS');
export const switchLanguageServerFailed = createAction<Error>('SWITCH LANGUAGE SERVER FAILED');

View file

@ -0,0 +1,52 @@
/*
* 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 { createAction } from 'redux-actions';
import { DocumentSearchResult, Repository, SearchScope } from '../../model';
export interface DocumentSearchPayload {
query: string;
page?: string;
languages?: string;
repositories?: string;
repoScope?: string;
}
export interface RepositorySearchPayload {
query: string;
}
export interface SearchOptions {
repoScope: Repository[];
defaultRepoScopeOn: boolean;
}
// For document search page
export const documentSearch = createAction<DocumentSearchPayload>('DOCUMENT SEARCH');
export const documentSearchSuccess = createAction<DocumentSearchResult>('DOCUMENT SEARCH SUCCESS');
export const documentSearchFailed = createAction<Error>('DOCUMENT SEARCH FAILED');
// For repository search page
export const repositorySearch = createAction<RepositorySearchPayload>('REPOSITORY SEARCH');
export const repositorySearchSuccess = createAction('REPOSITORY SEARCH SUCCESS');
export const repositorySearchFailed = createAction<Error>('REPOSITORY SEARCH FAILED');
export const changeSearchScope = createAction<SearchScope>('CHANGE SEARCH SCOPE');
// For repository search typeahead
export const repositorySearchQueryChanged = createAction<RepositorySearchPayload>(
'REPOSITORY SEARCH QUERY CHANGED'
);
export const repositoryTypeaheadSearchSuccess = createAction<string>('REPOSITORY SEARCH SUCCESS');
export const repositoryTypeaheadSearchFailed = createAction<string>('REPOSITORY SEARCH FAILED');
export const saveSearchOptions = createAction<SearchOptions>('SAVE SEARCH OPTIONS');
export const turnOnDefaultRepoScope = createAction('TURN ON DEFAULT REPO SCOPE');
export const searchReposForScope = createAction<RepositorySearchPayload>('SEARCH REPOS FOR SCOPE');
export const searchReposForScopeSuccess = createAction<any>('SEARCH REPOS FOR SCOPE SUCCESS');
export const searchReposForScopeFailed = createAction<any>('SEARCH REPOS FOR SCOPE FAILED');

View file

@ -0,0 +1,13 @@
/*
* 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 { createAction } from 'redux-actions';
import { HotKey } from '../components/shortcuts';
export const registerShortcut = createAction<HotKey>('REGISTER SHORTCUT');
export const unregisterShortcut = createAction<HotKey>('UNREGISTER SHORTCUT');
export const toggleHelp = createAction<boolean | null>('TOGGLE SHORTCUTS HELP');

View file

@ -0,0 +1,24 @@
/*
* 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 { createAction } from 'redux-actions';
import { RepoStatus } from '../reducers';
export const loadStatus = createAction<string>('LOAD STATUS');
export const loadStatusSuccess = createAction<any>('LOAD STATUS SUCCESS');
export const loadStatusFailed = createAction<string>('LOAD STATUS FAILED');
export const pollRepoCloneStatus = createAction<any>('POLL CLONE STATUS');
export const pollRepoIndexStatus = createAction<any>('POLL INDEX STATUS');
export const pollRepoDeleteStatus = createAction<any>('POLL DELETE STATUS');
export const loadRepo = createAction<string>('LOAD REPO');
export const loadRepoSuccess = createAction<any>('LOAD REPO SUCCESS');
export const loadRepoFailed = createAction<any>('LOAD REPO FAILED');
export const updateCloneProgress = createAction<RepoStatus>('UPDATE CLONE PROGRESS');
export const updateIndexProgress = createAction<RepoStatus>('UPDATE INDEX PROGRESS');
export const updateDeleteProgress = createAction<RepoStatus>('UPDATE DELETE PROGRESS');

View file

@ -0,0 +1,20 @@
/*
* 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 { createAction } from 'redux-actions';
import { SymbolInformation } from 'vscode-languageserver-types/lib/esm/main';
export interface SymbolsPayload {
path: string;
data: SymbolInformation[];
}
export const loadStructure = createAction<string>('LOAD STRUCTURE');
export const loadStructureSuccess = createAction<SymbolsPayload>('LOAD STRUCTURE SUCCESS');
export const loadStructureFailed = createAction<Error>('LOAD STRUCTURE FAILED');
export const openSymbolPath = createAction<string>('OPEN SYMBOL PATH');
export const closeSymbolPath = createAction<string>('CLOSE SYMBOL PATH');

View file

@ -0,0 +1,60 @@
/*
* 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 { render, unmountComponentAtNode } from 'react-dom';
import { Provider } from 'react-redux';
import 'ui/autoload/all';
import 'ui/autoload/styles';
import chrome from 'ui/chrome';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { App } from './components/app';
import { HelpMenu } from './components/help_menu';
import { store } from './stores';
const app = uiModules.get('apps/code');
app.config(($locationProvider: any) => {
$locationProvider.html5Mode({
enabled: false,
requireBase: false,
rewriteLinks: false,
});
});
app.config((stateManagementConfigProvider: any) => stateManagementConfigProvider.disable());
function RootController($scope: any, $element: any, $http: any) {
const domNode = $element[0];
// render react to DOM
render(
<Provider store={store}>
<App />
</Provider>,
domNode
);
// unmount react on controller destroy
$scope.$on('$destroy', () => {
unmountComponentAtNode(domNode);
});
}
chrome.setRootController('code', RootController);
chrome.breadcrumbs.set([
{
text: 'Code (Beta)',
href: '#/',
},
]);
chrome.helpExtension.set(domNode => {
render(<HelpMenu />, domNode);
return () => {
unmountComponentAtNode(domNode);
};
});

View file

@ -0,0 +1,49 @@
/*
* 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 { ReactNode } from 'react';
import { SearchScope } from '../../model';
export enum PathTypes {
blob = 'blob',
tree = 'tree',
blame = 'blame',
commits = 'commits',
}
export const SearchScopeText = {
[SearchScope.DEFAULT]: 'Search Everything',
[SearchScope.REPOSITORY]: 'Search Repositories',
[SearchScope.SYMBOL]: 'Search Symbols',
[SearchScope.FILE]: 'Search Files',
};
export const SearchScopePlaceholderText = {
[SearchScope.DEFAULT]: 'Type to find anything',
[SearchScope.REPOSITORY]: 'Type to find repositories',
[SearchScope.SYMBOL]: 'Type to find symbols',
[SearchScope.FILE]: 'Type to find files',
};
export interface MainRouteParams {
path: string;
repo: string;
resource: string;
org: string;
revision: string;
pathType: PathTypes;
goto?: string;
}
export interface EuiSideNavItem {
id: string;
name: string;
isSelected?: boolean;
renderItem?: () => ReactNode;
forceOpen?: boolean;
items?: EuiSideNavItem[];
onClick: () => void;
}

View file

@ -0,0 +1,132 @@
/*
* 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 { parse as parseQuery } from 'querystring';
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import url from 'url';
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui';
import { Repository } from '../../../model';
import { RootState } from '../../reducers';
import { EmptyProject } from './empty_project';
import { LanguageSeverTab } from './language_server_tab';
import { ProjectTab } from './project_tab';
enum AdminTabs {
projects = 'Projects',
roles = 'Roles',
languageServers = 'LanguageServers',
}
interface Props extends RouteComponentProps {
repositories: Repository[];
repositoryLoading: boolean;
}
interface State {
tab: AdminTabs;
}
class AdminPage extends React.PureComponent<Props, State> {
public static getDerivedStateFromProps(props: Props) {
const getTab = () => {
const { search } = props.location;
let qs = search;
if (search.charAt(0) === '?') {
qs = search.substr(1);
}
return parseQuery(qs).tab || AdminTabs.projects;
};
return {
tab: getTab() as AdminTabs,
};
}
public tabs = [
{
id: AdminTabs.projects,
name: AdminTabs.projects,
disabled: false,
},
{
id: AdminTabs.languageServers,
name: 'Language servers',
disabled: false,
},
];
constructor(props: Props) {
super(props);
const getTab = () => {
const { search } = props.location;
let qs = search;
if (search.charAt(0) === '?') {
qs = search.substr(1);
}
return parseQuery(qs).tab || AdminTabs.projects;
};
this.state = {
tab: getTab() as AdminTabs,
};
}
public getAdminTabClickHandler = (tab: AdminTabs) => () => {
this.setState({ tab });
this.props.history.push(url.format({ pathname: '/admin', query: { tab } }));
};
public renderTabs() {
const tabs = this.tabs.map(tab => (
<EuiTab
onClick={this.getAdminTabClickHandler(tab.id)}
isSelected={tab.id === this.state.tab}
disabled={tab.disabled}
key={tab.id}
>
{tab.name}
</EuiTab>
));
return <EuiTabs>{tabs}</EuiTabs>;
}
public filterRepos = () => {
return this.props.repositories;
};
public renderTabContent = () => {
switch (this.state.tab) {
case AdminTabs.languageServers: {
return <LanguageSeverTab />;
}
case AdminTabs.projects:
default: {
const repositoriesCount = this.props.repositories.length;
const showEmpty = repositoriesCount === 0 && !this.props.repositoryLoading;
if (showEmpty) {
return <EmptyProject />;
}
return <ProjectTab />;
}
}
};
public render() {
return (
<EuiFlexGroup direction="row">
<EuiFlexItem className="codeContainer__adminWrapper">
{this.renderTabs()}
{this.renderTabContent()}
</EuiFlexItem>
</EuiFlexGroup>
);
}
}
const mapStateToProps = (state: RootState) => ({
repositories: state.repository.repositories,
repositoryLoading: state.repository.loading,
});
export const Admin = withRouter(connect(mapStateToProps)(AdminPage));

View file

@ -0,0 +1,35 @@
/*
* 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 { Link } from 'react-router-dom';
import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui';
import { capabilities } from 'ui/capabilities';
import { ImportProject } from './import_project';
export const EmptyProject = () => {
const isAdmin = capabilities.get().code.admin as boolean;
return (
<div className="codeTab__projects">
<EuiSpacer size="xl" />
<div className="codeTab__projects--emptyHeader">
<EuiText>
<h1>You don't have any projects yet</h1>
</EuiText>
<EuiText color="subdued">{isAdmin && <p>Let's import your first one</p>}</EuiText>
</div>
{isAdmin && <ImportProject />}
<EuiSpacer />
<EuiFlexGroup justifyContent="center">
<Link to="/setup-guide">
<EuiButton>View the Setup Guide</EuiButton>
</Link>
</EuiFlexGroup>
</div>
);
};

View file

@ -0,0 +1,124 @@
/*
* 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 {
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiGlobalToastList,
EuiSpacer,
} from '@elastic/eui';
import React, { ChangeEvent } from 'react';
import { connect } from 'react-redux';
import { closeToast, importRepo } from '../../actions';
import { RootState } from '../../reducers';
import { ToastType } from '../../reducers/repository';
import { isImportRepositoryURLInvalid } from '../../utils/url';
class CodeImportProject extends React.PureComponent<
{
importRepo: (p: string) => void;
importLoading: boolean;
toastMessage?: string;
showToast: boolean;
toastType?: ToastType;
closeToast: () => void;
},
{ value: string; isInvalid: boolean }
> {
public state = {
value: '',
isInvalid: false,
};
public onChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
value: e.target.value,
isInvalid: isImportRepositoryURLInvalid(e.target.value),
});
};
public submitImportProject = () => {
if (!isImportRepositoryURLInvalid(this.state.value)) {
this.props.importRepo(this.state.value);
} else if (!this.state.isInvalid) {
this.setState({ isInvalid: true });
}
};
public updateIsInvalid = () => {
this.setState({ isInvalid: isImportRepositoryURLInvalid(this.state.value) });
};
public render() {
const { importLoading, toastMessage, showToast, toastType } = this.props;
return (
<div className="codeContainer__import">
{showToast && (
<EuiGlobalToastList
toasts={[{ title: '', color: toastType, text: toastMessage, id: toastMessage || '' }]}
dismissToast={this.props.closeToast}
toastLifeTimeMs={6000}
/>
)}
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label="Repository URL"
helpText="e.g. https://github.com/elastic/elasticsearch"
fullWidth
isInvalid={this.state.isInvalid}
error="The URL shouldn't be empty."
>
<EuiFieldText
value={this.state.value}
onChange={this.onChange}
aria-label="input project url"
data-test-subj="importRepositoryUrlInputBox"
isLoading={importLoading}
fullWidth={true}
onBlur={this.updateIsInvalid}
isInvalid={this.state.isInvalid}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{/*
// @ts-ignore */}
<EuiButton
className="codeButton__projectImport"
onClick={this.submitImportProject}
data-test-subj="importRepositoryButton"
>
Import
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}
}
const mapStateToProps = (state: RootState) => ({
importLoading: state.repository.importLoading,
toastMessage: state.repository.toastMessage,
toastType: state.repository.toastType,
showToast: state.repository.showToast,
});
const mapDispatchToProps = {
importRepo,
closeToast,
};
export const ImportProject = connect(
mapStateToProps,
mapDispatchToProps
)(CodeImportProject);

View file

@ -0,0 +1,240 @@
/*
* 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 {
EuiButton,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiPanel,
EuiSpacer,
EuiTabbedContent,
EuiText,
} from '@elastic/eui';
import React from 'react';
import { connect } from 'react-redux';
import { InstallationType } from '../../../common/installation';
import { LanguageServer, LanguageServerStatus } from '../../../common/language_server';
import { requestInstallLanguageServer } from '../../actions/language_server';
import { RootState } from '../../reducers';
import { JavaIcon, TypeScriptIcon, GoIcon } from '../shared/icons';
const LanguageServerLi = (props: {
languageServer: LanguageServer;
requestInstallLanguageServer: (l: string) => void;
loading: boolean;
}) => {
const { status, name } = props.languageServer;
const languageIcon = () => {
if (name === 'Typescript') {
return <TypeScriptIcon />;
} else if (name === 'Java') {
return <JavaIcon />;
} else if (name === 'Go') {
return <GoIcon />;
}
};
const onInstallClick = () => props.requestInstallLanguageServer(name);
let button = null;
let state = null;
if (status === LanguageServerStatus.RUNNING) {
state = <EuiText size="xs">Running ...</EuiText>;
} else if (status === LanguageServerStatus.NOT_INSTALLED) {
state = (
<EuiText size="xs" color={'subdued'}>
Not Installed
</EuiText>
);
} else if (status === LanguageServerStatus.READY) {
state = (
<EuiText size="xs" color={'subdued'}>
Installed
</EuiText>
);
}
if (props.languageServer.installationType === InstallationType.Plugin) {
button = (
<EuiButton size="s" color="secondary" onClick={onInstallClick}>
Setup
</EuiButton>
);
}
return (
<EuiFlexItem>
<EuiPanel>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}> {languageIcon()} </EuiFlexItem>
<EuiFlexItem>
<EuiText>
<strong>{name}</strong>
</EuiText>
<EuiText size="s">
<h6> {state} </h6>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}> {button} </EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
);
};
interface Props {
languageServers: LanguageServer[];
requestInstallLanguageServer: (ls: string) => void;
installLoading: { [ls: string]: boolean };
}
interface State {
showingInstruction: boolean;
name?: string;
url?: string;
pluginName?: string;
}
class AdminLanguageSever extends React.PureComponent<Props, State> {
constructor(props: Props, context: any) {
super(props, context);
this.state = { showingInstruction: false };
}
public toggleInstruction = (
showingInstruction: boolean,
name?: string,
url?: string,
pluginName?: string
) => {
this.setState({ showingInstruction, name, url, pluginName });
};
public render() {
const languageServers = this.props.languageServers.map(ls => (
<LanguageServerLi
languageServer={ls}
key={ls.name}
requestInstallLanguageServer={() =>
this.toggleInstruction(true, ls.name, ls.downloadUrl, ls.pluginName)
}
loading={this.props.installLoading[ls.name]}
/>
));
return (
<div>
<EuiSpacer />
<EuiText>
<h3>
{this.props.languageServers.length}
{this.props.languageServers.length > 1 ? (
<span> Language servers</span>
) : (
<span> Language server</span>
)}
</h3>
</EuiText>
<EuiSpacer />
<EuiFlexGroup direction="column" gutterSize="s">
{languageServers}
</EuiFlexGroup>
<LanguageServerInstruction
show={this.state.showingInstruction}
name={this.state.name!}
pluginName={this.state.pluginName!}
url={this.state.url!}
close={() => this.toggleInstruction(false)}
/>
</div>
);
}
}
const SupportedOS = [
{ id: 'win', name: 'Windows' },
{ id: 'linux', name: 'Linux' },
{ id: 'darwin', name: 'macOS' },
];
const LanguageServerInstruction = (props: {
name: string;
pluginName: string;
url: string;
show: boolean;
close: () => void;
}) => {
const tabs = SupportedOS.map(({ id, name }) => {
const url = props.url ? props.url.replace('$OS', id) : '';
const installCode = `bin/kibana-plugin install ${url}`;
return {
id,
name,
content: (
<EuiText grow={false}>
<h3>Install</h3>
<p>
Stop your kibana Code node, then use the following command to install {props.name}{' '}
Language Server plugin:
<EuiCodeBlock language="shell">{installCode}</EuiCodeBlock>
</p>
<h3>Uninstall</h3>
<p>
Stop your kibana Code node, then use the following command to remove {props.name}{' '}
Language Server plugin:
<pre>
<code>bin/kibana-plugin remove {props.pluginName}</code>
</pre>
</p>
</EuiText>
),
};
});
return (
<React.Fragment>
{' '}
{props.show && (
<EuiOverlayMask>
<EuiModal onClose={props.close} maxWidth={false}>
<EuiModalHeader>
<EuiModalHeaderTitle>Install Instruction</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[1]} size={'m'} />
</EuiModalBody>
<EuiModalFooter>
<EuiButton onClick={props.close} fill>
Close
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
)}
</React.Fragment>
);
};
const mapStateToProps = (state: RootState) => ({
languageServers: state.languageServer.languageServers,
installLoading: state.languageServer.installServerLoading,
});
const mapDispatchToProps = {
requestInstallLanguageServer,
};
export const LanguageSeverTab = connect(
mapStateToProps,
mapDispatchToProps
)(AdminLanguageSever);

View file

@ -0,0 +1,238 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiProgress,
EuiText,
EuiTextColor,
EuiToolTip,
} from '@elastic/eui';
import moment from 'moment';
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Repository, WorkerReservedProgress } from '../../../model';
import { deleteRepo, indexRepo, initRepoCommand } from '../../actions';
import { RepoState, RepoStatus } from '../../reducers/status';
const stateColor = {
[RepoState.CLONING]: 'secondary',
[RepoState.DELETING]: 'accent',
[RepoState.INDEXING]: 'primary',
};
class CodeProjectItem extends React.PureComponent<{
project: Repository;
enableManagement: boolean;
showStatus: boolean;
status?: RepoStatus;
deleteRepo?: (uri: string) => void;
indexRepo?: (uri: string) => void;
initRepoCommand?: (uri: string) => void;
openSettings?: (uri: string, url: string) => void;
}> {
public render() {
const { project, showStatus, status, enableManagement } = this.props;
const { name, org, uri, url } = project;
const onClickDelete = () => this.props.deleteRepo && this.props.deleteRepo(uri);
const onClickIndex = () => this.props.indexRepo && this.props.indexRepo(uri);
const onClickSettings = () => this.props.openSettings && this.props.openSettings(uri, url);
let footer = null;
let disableRepoLink = false;
let hasError = false;
if (!status) {
footer = <div className="codeFooter">INIT...</div>;
} else if (status.state === RepoState.READY) {
footer = (
<div className="codeFooter" data-test-subj="repositoryIndexDone">
LAST UPDATED: {moment(status.timestamp).fromNow()}
</div>
);
} else if (status.state === RepoState.DELETING) {
footer = <div className="codeFooter">DELETING...</div>;
} else if (status.state === RepoState.INDEXING) {
footer = (
<div className="codeFooter" data-test-subj="repositoryIndexOngoing">
INDEXING...
</div>
);
} else if (status.state === RepoState.CLONING) {
footer = <div className="codeFooter">CLONING...</div>;
} else if (status.state === RepoState.DELETE_ERROR) {
footer = <div className="codeFooter codeFooter--error">ERROR DELETE REPO</div>;
hasError = true;
} else if (status.state === RepoState.INDEX_ERROR) {
footer = <div className="codeFooter codeFooter--error">ERROR INDEX REPO</div>;
hasError = true;
} else if (status.state === RepoState.CLONE_ERROR) {
footer = (
<div className="codeFooter codeFooter--error">
ERROR CLONING REPO&nbsp;
<EuiToolTip position="top" content={status.errorMessage}>
<EuiIcon type="iInCircle" />
</EuiToolTip>
</div>
);
// Disable repo link is clone failed.
disableRepoLink = true;
hasError = true;
}
const repoTitle = (
<EuiText data-test-subj="codeRepositoryItem">
<EuiTextColor color="subdued">{org}</EuiTextColor>/<strong>{name}</strong>
</EuiText>
);
const settingsShow =
status && status.state !== RepoState.CLONING && status.state !== RepoState.DELETING;
const settingsVisibility = settingsShow ? 'visible' : 'hidden';
const indexShow =
status &&
status.state !== RepoState.CLONING &&
status.state !== RepoState.DELETING &&
status.state !== RepoState.INDEXING &&
status.state !== RepoState.CLONE_ERROR;
const indexVisibility = indexShow ? 'visible' : 'hidden';
const deleteShow = status && status.state !== RepoState.DELETING;
const deleteVisibility = deleteShow ? 'visible' : 'hidden';
const projectManagement = (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false} style={{ display: 'none' }}>
<div
className="codeButton__project"
data-test-subj="settingsRepositoryButton"
tabIndex={0}
onKeyDown={onClickSettings}
onClick={onClickSettings}
role="button"
style={{ visibility: settingsVisibility }}
>
<EuiIcon type="gear" />
<EuiText size="xs" color="subdued">
Settings
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div
className="codeButton__project"
data-test-subj="indexRepositoryButton"
tabIndex={0}
onKeyDown={onClickIndex}
onClick={onClickIndex}
role="button"
style={{ visibility: indexVisibility }}
>
<EuiIcon type="indexSettings" />
<EuiText size="xs" color="subdued">
Index
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div
className="codeButton__project"
data-test-subj="deleteRepositoryButton"
tabIndex={0}
onKeyDown={onClickDelete}
onClick={onClickDelete}
role="button"
style={{ visibility: deleteVisibility }}
>
<EuiIcon type="trash" color="danger" />
<EuiText size="xs" color="subdued">
Delete
</EuiText>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
const repoStatus = (
<EuiText>
<h6>
<EuiTextColor color="subdued">{footer}</EuiTextColor>
</h6>
</EuiText>
);
return (
<EuiPanel
className={hasError ? 'codePanel__project codePanel__project--error' : 'codePanel__project'}
>
{this.renderProgress()}
<EuiFlexGroup alignItems="center" justifyContent="flexStart">
<EuiFlexItem grow={3}>
{disableRepoLink ? (
repoTitle
) : (
<Link to={`/${uri}`} data-test-subj={`adminLinkTo${name}`}>
{repoTitle}
</Link>
)}
{showStatus ? repoStatus : null}
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiText color="subdued" size="s">
<a href={'https://' + uri} target="_blank">
{uri}
</a>
</EuiText>
</EuiFlexItem>
{enableManagement && projectManagement}
</EuiFlexGroup>
</EuiPanel>
);
}
private renderProgress() {
const { status } = this.props;
if (
status &&
(status.state === RepoState.CLONING ||
status.state === RepoState.DELETING ||
status.state === RepoState.INDEXING)
) {
const color = stateColor[status.state] as 'primary' | 'secondary' | 'accent';
if (status.progress! === WorkerReservedProgress.COMPLETED) {
return null;
} else if (status.progress! > WorkerReservedProgress.INIT) {
return (
<EuiProgress
max={100}
value={status.progress}
size="s"
color={color}
position="absolute"
/>
);
} else {
return <EuiProgress size="s" color={color} position="absolute" />;
}
}
}
}
const mapDispatchToProps = {
deleteRepo,
indexRepo,
initRepoCommand,
};
export const ProjectItem = connect(
null,
mapDispatchToProps
)(CodeProjectItem);

View file

@ -0,0 +1,150 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSwitch,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { ChangeEvent } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { LanguageServer } from '../../../common/language_server';
import { RepositoryUtils } from '../../../common/repository_utils';
import { RepositoryConfig } from '../../../model';
import { RepoConfigPayload, switchLanguageServer } from '../../actions';
import { RootState } from '../../reducers';
import { JavaIcon, TypeScriptIcon } from '../shared/icons';
const defaultConfig = {
disableGo: true,
disableJava: true,
disableTypescript: true,
};
interface StateProps {
languageServers: LanguageServer[];
config: RepositoryConfig;
}
interface DispatchProps {
switchLanguageServer: (p: RepoConfigPayload) => void;
}
interface OwnProps {
repoUri: string;
url: string;
onClose: () => void;
}
interface State {
config: RepositoryConfig;
}
class ProjectSettingsModal extends React.PureComponent<
StateProps & DispatchProps & OwnProps,
State
> {
public state = {
config: this.props.config,
};
public onSwitchChange = (ls: string) => (e: ChangeEvent<HTMLInputElement>) => {
const { checked } = e.target;
this.setState((prevState: State) => ({
config: { ...prevState.config, [`disable${ls}`]: !checked },
}));
};
public saveChanges = () => {
this.props.switchLanguageServer({
repoUri: this.props.repoUri,
config: this.state.config,
});
};
public render() {
const { repoUri, languageServers, onClose } = this.props;
const { disableJava, disableTypescript } = this.state.config;
const org = RepositoryUtils.orgNameFromUri(repoUri);
const repoName = RepositoryUtils.repoNameFromUri(repoUri);
const languageServerSwitches = languageServers.map(ls => {
const checked = ls.name === 'Java' ? !disableJava : !disableTypescript;
return (
<div key={ls.name}>
<EuiSwitch
name={ls.name}
label={
<span>
{ls.name === 'Java' ? (
<div className="codeSettingsPanel__icon">
<JavaIcon />
</div>
) : (
<div className="codeSettingsPanel__icon">
<TypeScriptIcon />
</div>
)}
{ls.name}
</span>
}
checked={checked}
onChange={this.onSwitchChange(ls.name)}
/>
</div>
);
});
return (
<EuiOverlayMask>
<EuiModal onClose={onClose}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h3>Project Settings</h3>
<EuiText>
{org}/{repoName}
</EuiText>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTitle size="xxs">
<h5>Language Servers</h5>
</EuiTitle>
{languageServerSwitches}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty>
<Link to="/admin?tab=LanguageServers">Manage Language Servers</Link>
</EuiButtonEmpty>
<EuiButton onClick={this.saveChanges}>Save Changes</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}
}
const mapStateToProps = (state: RootState, ownProps: { repoUri: string }) => ({
languageServers: state.languageServer.languageServers,
config: state.repository.projectConfigs![ownProps.repoUri] || defaultConfig,
});
const mapDispatchToProps = {
switchLanguageServer,
};
export const ProjectSettings = connect<StateProps, DispatchProps, OwnProps>(
// @ts-ignore
mapStateToProps,
mapDispatchToProps
)(ProjectSettingsModal);

View file

@ -0,0 +1,286 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiGlobalToastList,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSpacer,
// @ts-ignore
EuiSuperSelect,
EuiText,
EuiTitle,
} from '@elastic/eui';
import moment from 'moment';
import React, { ChangeEvent } from 'react';
import { connect } from 'react-redux';
import { capabilities } from 'ui/capabilities';
import { Repository } from '../../../model';
import { closeToast, importRepo } from '../../actions';
import { RepoStatus, RootState } from '../../reducers';
import { ToastType } from '../../reducers/repository';
import { isImportRepositoryURLInvalid } from '../../utils/url';
import { ProjectItem } from './project_item';
import { ProjectSettings } from './project_settings';
enum SortOptionsValue {
AlphabeticalAsc = 'alphabetical_asc',
AlphabeticalDesc = 'alphabetical_desc',
UpdatedAsc = 'updated_asc',
UpdatedDesc = 'updated_desc',
RecentlyAdded = 'recently_added',
}
const sortFunctionsFactory = (status: { [key: string]: RepoStatus }) => {
const sortFunctions: { [k: string]: (a: Repository, b: Repository) => number } = {
[SortOptionsValue.AlphabeticalAsc]: (a: Repository, b: Repository) =>
a.name!.localeCompare(b.name!),
[SortOptionsValue.AlphabeticalDesc]: (a: Repository, b: Repository) =>
b.name!.localeCompare(a.name!),
[SortOptionsValue.UpdatedAsc]: (a: Repository, b: Repository) =>
moment(status[b.uri].timestamp).diff(moment(status[a.uri].timestamp)),
[SortOptionsValue.UpdatedDesc]: (a: Repository, b: Repository) =>
moment(status[a.uri].timestamp).diff(moment(status[b.uri].timestamp)),
[SortOptionsValue.RecentlyAdded]: () => {
return -1;
},
};
return sortFunctions;
};
const sortOptions = [
{ value: SortOptionsValue.AlphabeticalAsc, inputDisplay: 'A to Z' },
{ value: SortOptionsValue.AlphabeticalDesc, inputDisplay: 'Z to A' },
{ value: SortOptionsValue.UpdatedAsc, inputDisplay: 'Last Updated ASC' },
{ value: SortOptionsValue.UpdatedDesc, inputDisplay: 'Last Updated DESC' },
// { value: SortOptionsValue.recently_added, inputDisplay: 'Recently Added' },
];
interface Props {
projects: Repository[];
status: { [key: string]: RepoStatus };
importRepo: (repoUrl: string) => void;
importLoading: boolean;
toastMessage?: string;
showToast: boolean;
toastType?: ToastType;
closeToast: () => void;
}
interface State {
showImportProjectModal: boolean;
importLoading: boolean;
settingModal: { url?: string; uri?: string; show: boolean };
repoURL: string;
isInvalid: boolean;
sortOption: SortOptionsValue;
}
class CodeProjectTab extends React.PureComponent<Props, State> {
public static getDerivedStateFromProps(props: Readonly<Props>, state: State) {
if (state.importLoading && !props.importLoading) {
return { showImportProjectModal: false, importLoading: props.importLoading, repoURL: '' };
}
return { importLoading: props.importLoading };
}
constructor(props: Props) {
super(props);
this.state = {
importLoading: false,
showImportProjectModal: false,
settingModal: { show: false },
repoURL: '',
sortOption: SortOptionsValue.AlphabeticalAsc,
isInvalid: false,
};
}
public closeModal = () => {
this.setState({ showImportProjectModal: false, repoURL: '', isInvalid: false });
};
public openModal = () => {
this.setState({ showImportProjectModal: true });
};
public openSettingModal = (uri: string, url: string) => {
this.setState({ settingModal: { uri, url, show: true } });
};
public closeSettingModal = () => {
this.setState({ settingModal: { show: false } });
};
public onChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
repoURL: e.target.value,
isInvalid: isImportRepositoryURLInvalid(e.target.value),
});
};
public submitImportProject = () => {
if (!isImportRepositoryURLInvalid(this.state.repoURL)) {
this.props.importRepo(this.state.repoURL);
} else if (!this.state.isInvalid) {
this.setState({ isInvalid: true });
}
};
public updateIsInvalid = () => {
this.setState({ isInvalid: isImportRepositoryURLInvalid(this.state.repoURL) });
};
public renderImportModal = () => {
return (
<EuiOverlayMask>
<EuiModal onClose={this.closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>Add new project</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTitle size="xs">
<h3>Repository URL</h3>
</EuiTitle>
<EuiForm>
<EuiFormRow isInvalid={this.state.isInvalid} error="The URL shouldn't be empty.">
<EuiFieldText
value={this.state.repoURL}
onChange={this.onChange}
onBlur={this.updateIsInvalid}
placeholder="https://github.com/elastic/elasticsearch"
aria-label="input project url"
data-test-subj="importRepositoryUrlInputBox"
isLoading={this.props.importLoading}
fullWidth={true}
isInvalid={this.state.isInvalid}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={this.closeModal}>Cancel</EuiButtonEmpty>
<EuiButton fill onClick={this.submitImportProject} disabled={this.props.importLoading}>
Import project
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
};
public setSortOption = (value: string) => {
this.setState({ sortOption: value as SortOptionsValue });
};
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) => (
<ProjectItem
openSettings={this.openSettingModal}
key={repo.uri}
project={repo}
showStatus={true}
status={status[repo.uri]}
enableManagement={capabilities.get().code.admin as boolean}
/>
));
let settings = null;
if (this.state.settingModal.show) {
settings = (
<ProjectSettings
onClose={this.closeSettingModal}
repoUri={this.state.settingModal.uri}
url={this.state.settingModal.url}
/>
);
}
return (
<div className="code-sidebar" data-test-subj="codeRepositoryList">
{showToast && (
<EuiGlobalToastList
toasts={[{ title: '', color: toastType, text: toastMessage, id: toastMessage || '' }]}
dismissToast={this.props.closeToast}
toastLifeTimeMs={6000}
/>
)}
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="Sort By">
<EuiSuperSelect
options={sortOptions}
valueOfSelected={this.state.sortOption}
onChange={this.setSortOption}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow />
<EuiFlexItem grow />
<EuiFlexItem>
{(capabilities.get().code.admin as boolean) && (
// @ts-ignore
<EuiButton
className="codeButton__projectImport"
onClick={this.openModal}
data-test-subj="newProjectButton"
>
Add New Project
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiText>
<h3>
{projectsCount}
{projectsCount === 1 ? <span> Project</span> : <span> Projects</span>}
</h3>
</EuiText>
<EuiSpacer />
{repoList}
{modal}
{settings}
</div>
);
}
}
const mapStateToProps = (state: RootState) => ({
projects: state.repository.repositories,
status: state.status.status,
importLoading: state.repository.importLoading,
toastMessage: state.repository.toastMessage,
toastType: state.repository.toastType,
showToast: state.repository.showToast,
});
const mapDispatchToProps = {
importRepo,
closeToast,
};
export const ProjectTab = connect(
mapStateToProps,
mapDispatchToProps
)(CodeProjectTab);

View file

@ -0,0 +1,169 @@
/*
* 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 {
EuiButton,
EuiCallOut,
EuiGlobalToastList,
EuiPanel,
EuiSpacer,
EuiSteps,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { documentationLinks } from '../../lib/documentation_links';
import { RootState } from '../../reducers';
const steps = [
{
title: 'Configure Kibana Code Instance for Multiple Kibana Nodes',
children: (
<EuiText>
<p>
If you are using multiple Kibana nodes, then you need to configure 1 Kibana instance as
Code instance. Please add the following line of code into your kibana.yml file for every
instance to indicate your Code instance:
</p>
<pre>
<code>xpack.code.codeNodeUrl: 'http://$YourCodeNodeAddress'</code>
</pre>
<p>Then, restart every Kibana instance.</p>
</EuiText>
),
},
{
title: 'Download and install language servers',
children: (
<EuiText>
<p>
If you need code intelligence support for your repos, you need to install the language
server for the programming languages.
</p>
<p />
<h5>PRE-INSTALLED LANGUAGE SERVERS:</h5>
<p />
Typescript
<p />
<h5>AVAILABLE LANGUAGE SERVERS:</h5>
<p />
Java
<p />
<Link to="/admin?tab=LanguageServers">Manage language server installation</Link>
</EuiText>
),
},
{
title: 'Import a repository from a git address',
children: (
<EuiText>
<p>
You can add a repo to Code by simply putting in the git address of the repo. Usually this
is the same git address you use to run the git clone command, you can find more details
about the formats of git addresses that Code accepts&nbsp;
<a href={documentationLinks.gitFormat}>here</a>.
</p>
</EuiText>
),
},
{
title: 'Verify that your repo has been successfully imported',
children: (
<EuiText>
<p>
Once the repo is added and indexed successfully, you can verify that the repo is
searchable and the code intelligence is available. You can find more details of how the
search and code intelligence work in{' '}
<a href={documentationLinks.codeIntelligence}>our docs</a>.
</p>
</EuiText>
),
},
];
// TODO add link to learn more button
const toastMessage = (
<div>
<p>
Weve made some changes to roles and permissions in Kibana. Read more about what these changes
mean for you below.{' '}
</p>
<EuiButton size="s" href="">
Learn More
</EuiButton>
</div>
);
class SetupGuidePage extends React.PureComponent<{ setupOk?: boolean }, { hideToast: boolean }> {
constructor(props: { setupOk?: boolean }) {
super(props);
this.state = {
hideToast: false,
};
}
public render() {
let setup = null;
if (this.props.setupOk !== undefined) {
setup = (
<div>
{!this.state.hideToast && (
<EuiGlobalToastList
toasts={[
{
title: 'Permission Changes',
color: 'primary',
iconType: 'iInCircle',
text: toastMessage,
id: '',
},
]}
dismissToast={() => {
this.setState({ hideToast: true });
}}
toastLifeTimeMs={10000}
/>
)}
<React.Fragment>
{this.props.setupOk === false && (
<EuiCallOut title="Code instance not found." color="danger" iconType="cross">
<p>
Please follow the guide below to configure your Kibana instance and then refresh
this page.
</p>
</EuiCallOut>
)}
{this.props.setupOk === true && (
<React.Fragment>
<EuiSpacer size="xs" />
<EuiButton iconType="sortLeft">
<Link to="/admin">Back To Project Dashboard</Link>
</EuiButton>
</React.Fragment>
)}
<EuiPanel>
<EuiTitle>
<h3>Getting started in Elastic Code</h3>
</EuiTitle>
<EuiSpacer />
<EuiSteps steps={steps} />
</EuiPanel>
</React.Fragment>
</div>
);
}
return <div className="condeContainer__setup">{setup}</div>;
}
}
const mapStateToProps = (state: RootState) => ({
setupOk: state.setup.ok,
});
export const SetupGuide = connect(mapStateToProps)(SetupGuidePage);

View file

@ -0,0 +1,52 @@
/*
* 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 { HashRouter as Router, Redirect, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import { RootState } from '../reducers';
import { Admin } from './admin_page/admin';
import { SetupGuide } from './admin_page/setup_guide';
import { Diff } from './diff_page/diff';
import { Main } from './main/main';
import { NotFound } from './main/not_found';
import { Route } from './route';
import * as ROUTES from './routes';
import { Search } from './search_page/search';
const Empty = () => null;
const RooComponent = (props: { setupOk?: boolean }) => {
if (props.setupOk) {
return <Redirect to={'/admin'} />;
}
return <SetupGuide />;
};
const mapStateToProps = (state: RootState) => ({
setupOk: state.setup.ok,
});
const Root = connect(mapStateToProps)(RooComponent);
export const App = () => {
return (
<Router>
<Switch>
<Route path={ROUTES.DIFF} exact={true} component={Diff} />
<Route path={ROUTES.ROOT} exact={true} component={Root} />
<Route path={ROUTES.MAIN} component={Main} exact={true} />
<Route path={ROUTES.MAIN_ROOT} component={Main} />
<Route path={ROUTES.ADMIN} component={Admin} />
<Route path={ROUTES.SEARCH} component={Search} />
<Route path={ROUTES.REPO} render={Empty} exact={true} />
<Route path={ROUTES.SETUP} component={SetupGuide} exact={true} />
<Route path="*" component={NotFound} />
</Switch>
</Router>
);
};

View file

@ -0,0 +1,139 @@
/*
* 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 { EuiPanel } from '@elastic/eui';
import { editor, IPosition, IRange } from 'monaco-editor';
import React from 'react';
import { ResizeChecker } from 'ui/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;
folding: boolean;
lineNumbersFunc: (line: number) => string;
}
export class CodeBlock extends React.PureComponent<Props> {
private el: HTMLDivElement | null = null;
private ed?: editor.IStandaloneCodeEditor;
private resizeChecker?: ResizeChecker;
private currentHighlightDecorations: string[] = [];
public componentDidMount(): void {
if (this.el) {
this.ed = monaco.editor.create(this.el!, {
value: this.props.code,
language: this.props.language,
lineNumbers: this.lineNumbersFunc.bind(this),
readOnly: true,
folding: this.props.folding,
minimap: {
enabled: false,
},
scrollbar: {
vertical: 'hidden',
handleMouseWheel: false,
verticalScrollbarSize: 0,
},
hover: {
enabled: false, // disable default hover;
},
contextmenu: false,
selectOnLineNumbers: false,
selectionHighlight: false,
renderLineHighlight: 'none',
renderIndentGuides: false,
automaticLayout: false,
});
this.ed.onMouseDown((e: editor.IEditorMouseEvent) => {
if (
this.props.onClick &&
(e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS ||
e.target.type === monaco.editor.MouseTargetType.CONTENT_TEXT)
) {
const lineNumber = (this.props.startLine || 0) + e.target.position.lineNumber;
this.props.onClick({
lineNumber,
column: e.target.position.column,
});
}
});
registerEditor(this.ed);
if (this.props.highlightRanges) {
const decorations = this.props.highlightRanges.map((range: IRange) => {
return {
range,
options: {
inlineClassName: 'codeSearch__highlight',
},
};
});
this.currentHighlightDecorations = this.ed.deltaDecorations([], decorations);
}
this.resizeChecker = new ResizeChecker(this.el!);
this.resizeChecker.on('resize', () => {
setTimeout(() => {
this.ed!.layout();
});
});
}
}
public componentDidUpdate(prevProps: Readonly<Props>) {
if (
prevProps.code !== this.props.code ||
prevProps.highlightRanges !== this.props.highlightRanges
) {
if (this.ed) {
this.ed.getModel().setValue(this.props.code);
if (this.props.highlightRanges) {
const decorations = this.props.highlightRanges!.map((range: IRange) => {
return {
range,
options: {
inlineClassName: 'codeSearch__highlight',
},
};
});
this.currentHighlightDecorations = this.ed.deltaDecorations(
this.currentHighlightDecorations,
decorations
);
}
}
}
}
public componentWillUnmount(): void {
if (this.ed) {
this.ed.dispose();
}
}
public render() {
const linesCount = this.props.code.split('\n').length;
return (
<EuiPanel style={{ marginBottom: '2rem' }} paddingSize="s">
{this.props.fileComponent}
<div ref={r => (this.el = r)} style={{ height: linesCount * 18 }} />
</EuiPanel>
);
}
private lineNumbersFunc = (line: number) => {
if (this.props.lineNumbersFunc) {
return this.props.lineNumbersFunc(line);
}
return `${(this.props.startLine || 0) + line}`;
};
}

View file

@ -0,0 +1,23 @@
/*
* 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 { EuiBadge /* , EuiLink*/ } from '@elastic/eui';
import React from 'react';
// import { DIFF } from '../routes';
interface Props {
repoUri: string;
commit: string;
children?: any;
}
export const CommitLink = ({ repoUri, commit, children }: Props) => {
// const href = DIFF.replace(':resource/:org/:repo', repoUri).replace(':commitId', commit);
return (
// <EuiLink href={`#${href}`}>
<EuiBadge color="hollow">{children || commit}</EuiBadge>
// </EuiLink>
);
};

View file

@ -0,0 +1,16 @@
.diff > button.euiAccordion__button > div:first-child {
flex-direction: row-reverse;
padding: $euiSize $euiSizeS;
}
.diff > button.euiAccordion__button {
&:hover {
text-decoration: none;
}
}
.euiAccordion__iconWrapper {
cursor: pointer;
}

View file

@ -0,0 +1,246 @@
/*
* 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 { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTitle } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import React, { MouseEvent } from 'react';
import { connect } from 'react-redux';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
import styled from 'styled-components';
import { CommitDiff, FileDiff } from '../../../common/git_diff';
import { SearchScope } from '../../../model';
import { changeSearchScope } from '../../actions';
import { RootState } from '../../reducers';
import { SearchBar } from '../search_page/search_bar';
import { ShortcutsProvider } from '../shortcuts';
import { DiffEditor } from './diff_editor';
const COMMIT_ID_LENGTH = 16;
const B = styled.b`
font-weight: bold;
`;
const PrimaryB = styled(B)`
color: ${theme.euiColorPrimary};
`;
const CommitId = styled.span`
display: inline-block;
padding: 0 ${theme.paddingSizes.xs};
border: ${theme.euiBorderThin};
`;
const Addition = styled.div`
padding: ${theme.paddingSizes.xs} ${theme.paddingSizes.s};
border-radius: ${theme.euiSizeXS};
color: white;
margin-right: ${theme.euiSizeS};
background-color: ${theme.euiColorDanger};
`;
const Deletion = styled(Addition)`
background-color: ${theme.euiColorVis0};
`;
const Container = styled.div`
padding: ${theme.paddingSizes.xs} ${theme.paddingSizes.m};
`;
const TopBarContainer = styled.div`
height: calc(48rem / 14);
border-bottom: ${theme.euiBorderThin};
padding: 0 ${theme.paddingSizes.m};
display: flex;
flex-direction: row;
justify-content: space-between;
`;
const Accordion = styled(EuiAccordion)`
border: ${theme.euiBorderThick};
border-radius: ${theme.euiSizeS};
margin-bottom: ${theme.euiSize};
`;
const Icon = styled(EuiIcon)`
margin-right: ${theme.euiSizeS};
`;
const Parents = styled.div`
border-left: ${theme.euiBorderThin};
height: calc(32rem / 14);
line-height: calc(32rem / 14);
padding-left: ${theme.paddingSizes.s};
margin: ${theme.euiSizeS} 0;
`;
const H4 = styled.h4`
height: 100%;
line-height: calc(48rem / 14);
`;
const ButtonContainer = styled.div`
cursor: default;
`;
interface Props extends RouteComponentProps<{ resource: string; org: string; repo: string }> {
commit: CommitDiff | null;
query: string;
onSearchScopeChanged: (s: SearchScope) => void;
repoScope: string[];
}
export enum DiffLayout {
Unified,
Split,
}
const onClick = (e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const Difference = (props: { fileDiff: FileDiff; repoUri: string; revision: string }) => (
<Accordion
initialIsOpen={true}
id={props.fileDiff.path}
buttonContent={
<ButtonContainer role="button" onClick={onClick}>
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="none">
<Addition>{props.fileDiff.additions}</Addition>
<Deletion>{props.fileDiff.deletions}</Deletion>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>{props.fileDiff.path}</EuiFlexItem>
<EuiFlexItem grow={false}>
<div className="euiButton euiButton--primary euiButton--small" role="button">
<span className="euiButton__content">
<Link to={`/${props.repoUri}/blob/${props.revision}/${props.fileDiff.path}`}>
View File
</Link>
</span>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</ButtonContainer>
}
>
<DiffEditor
originCode={props.fileDiff.originCode!}
modifiedCode={props.fileDiff.modifiedCode!}
language={props.fileDiff.language!}
renderSideBySide={true}
/>
</Accordion>
);
export class DiffPage extends React.Component<Props> {
public state = {
diffLayout: DiffLayout.Split,
};
public setLayoutUnified = () => {
this.setState({ diffLayout: DiffLayout.Unified });
};
public setLayoutSplit = () => {
this.setState({ diffLayout: DiffLayout.Split });
};
public render() {
const { commit, match } = this.props;
const { repo, org, resource } = match.params;
const repoUri = `${resource}/${org}/${repo}`;
if (!commit) {
return null;
}
const { additions, deletions, files } = commit;
const { parents } = commit.commit;
const title = commit.commit.message.split('\n')[0];
let parentsLinks = null;
if (parents.length > 1) {
const [p1, p2] = parents;
parentsLinks = (
<React.Fragment>
<Link to={`/${repoUri}/commit/${p1}`}>{p1}</Link>+
<Link to={`/${repoUri}/commit/${p2}`}>{p2}</Link>
</React.Fragment>
);
} else if (parents.length === 1) {
parentsLinks = <Link to={`/${repoUri}/commit/${parents[0]}`}>{parents[0]}</Link>;
}
const topBar = (
<TopBarContainer>
<div>
<EuiTitle size="xs">
<H4>{title}</H4>
</EuiTitle>
</div>
<div>
<Parents>Parents: {parentsLinks}</Parents>
</div>
</TopBarContainer>
);
const fileCount = files.length;
const diffs = commit.files.map(file => (
<Difference repoUri={repoUri} revision={commit.commit.id} fileDiff={file} key={file.path} />
));
return (
<div className="diff">
<SearchBar
repoScope={this.props.repoScope}
query={this.props.query}
onSearchScopeChanged={this.props.onSearchScopeChanged}
/>
{topBar}
<Container>
<EuiText>{commit.commit.message}</EuiText>
</Container>
<Container>
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText>
<Icon type="dataVisualizer" />
Showing
<PrimaryB> {fileCount} Changed files </PrimaryB>
with
<B> {additions} additions</B> and <B>{deletions} deletions </B>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
Committed by
<PrimaryB> {commit.commit.committer} </PrimaryB>
<CommitId>{commit.commit.id.substr(0, COMMIT_ID_LENGTH)}</CommitId>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</Container>
<Container>{diffs}</Container>
<ShortcutsProvider />
</div>
);
}
}
const mapStateToProps = (state: RootState) => ({
commit: state.commit.commit,
query: state.search.query,
repoScope: state.search.searchOptions.repoScope.map(r => r.uri),
});
const mapDispatchToProps = {
onSearchScopeChanged: changeSearchScope,
};
export const Diff = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(DiffPage)
);

View file

@ -0,0 +1,44 @@
/*
* 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 { editor } from 'monaco-editor';
import React from 'react';
import { MonacoDiffEditor } from '../../monaco/monaco_diff_editor';
interface Props {
originCode: string;
modifiedCode: string;
language: string;
renderSideBySide: boolean;
}
export class DiffEditor extends React.Component<Props> {
private diffEditor: MonacoDiffEditor | null = null;
public mountDiffEditor = (container: HTMLDivElement) => {
this.diffEditor = new MonacoDiffEditor(
container,
this.props.originCode,
this.props.modifiedCode,
this.props.language,
this.props.renderSideBySide
);
this.diffEditor.init();
};
public componentDidUpdate(prevProps: Props) {
if (prevProps.renderSideBySide !== this.props.renderSideBySide) {
this.updateLayout(this.props.renderSideBySide);
}
}
public updateLayout(renderSideBySide: boolean) {
this.diffEditor!.diffEditor!.updateOptions({ renderSideBySide } as editor.IDiffEditorOptions);
}
public render() {
return <div id="diffEditor" ref={this.mountDiffEditor} style={{ height: 1000 }} />;
}
}

View file

@ -0,0 +1,236 @@
/*
* 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 { EuiFlexItem } from '@elastic/eui';
import { editor as editorInterfaces } from 'monaco-editor';
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { Hover, Position, TextDocumentPositionParams } from 'vscode-languageserver-protocol';
import { GitBlame } from '../../../common/git_blame';
import { closeReferences, FetchFileResponse, findReferences, hoverResult } from '../../actions';
import { MainRouteParams } from '../../common/types';
import { BlameWidget } from '../../monaco/blame/blame_widget';
import { monaco } from '../../monaco/monaco';
import { MonacoHelper } from '../../monaco/monaco_helper';
import { RootState } from '../../reducers';
import { refUrlSelector } from '../../selectors';
import { history } from '../../utils/url';
import { Modifier, Shortcut } from '../shortcuts';
import { ReferencesPanel } from './references_panel';
import { encodeRevisionString } from '../../utils/url';
export interface EditorActions {
closeReferences(changeUrl: boolean): void;
findReferences(params: TextDocumentPositionParams): void;
hoverResult(hover: Hover): void;
}
interface Props {
file: FetchFileResponse;
revealPosition?: Position;
isReferencesOpen: boolean;
isReferencesLoading: boolean;
references: any[];
referencesTitle: string;
hover?: Hover;
refUrl?: string;
blames: GitBlame[];
showBlame: boolean;
}
type IProps = Props & EditorActions & RouteComponentProps<MainRouteParams>;
export class EditorComponent extends React.Component<IProps> {
public blameWidgets: any;
private container: HTMLElement | undefined;
private monaco: MonacoHelper | undefined;
private editor: editorInterfaces.IStandaloneCodeEditor | undefined;
private lineDecorations: string[] | null = null;
constructor(props: IProps, context: any) {
super(props, context);
}
public componentDidMount(): void {
this.container = document.getElementById('mainEditor') as HTMLElement;
this.monaco = new MonacoHelper(this.container, this.props);
const { file } = this.props;
if (file && file.content) {
const { uri, path, revision } = file.payload;
const qs = this.props.location.search;
this.loadText(file.content, uri, path, file.lang!, revision, qs).then(() => {
if (this.props.revealPosition) {
this.revealPosition(this.props.revealPosition);
}
if (this.props.showBlame) {
this.loadBlame(this.props.blames);
}
});
}
}
public componentDidUpdate(prevProps: IProps) {
const { file } = this.props;
const { uri, path, revision } = file.payload;
const {
resource,
org,
repo,
revision: routeRevision,
path: routePath,
} = this.props.match.params;
const prevContent = prevProps.file && prevProps.file.content;
const qs = this.props.location.search;
if (prevContent !== file.content || qs !== prevProps.location.search) {
this.loadText(file.content!, uri, path, file.lang!, revision, qs).then(() => {
if (this.props.revealPosition) {
this.revealPosition(this.props.revealPosition);
}
});
} else if (
file.payload.uri === `${resource}/${org}/${repo}` &&
file.payload.revision === routeRevision &&
file.payload.path === routePath &&
prevProps.revealPosition !== this.props.revealPosition
) {
this.revealPosition(this.props.revealPosition);
}
if (this.monaco && this.monaco.editor) {
if (prevProps.showBlame !== this.props.showBlame && this.props.showBlame) {
this.loadBlame(this.props.blames);
this.monaco.editor.updateOptions({ lineHeight: 38 });
} else if (!this.props.showBlame) {
this.destroyBlameWidgets();
this.monaco.editor.updateOptions({ lineHeight: 18, lineDecorationsWidth: 16 });
}
if (prevProps.blames !== this.props.blames && this.props.showBlame) {
this.loadBlame(this.props.blames);
this.monaco.editor.updateOptions({ lineHeight: 38, lineDecorationsWidth: 316 });
}
}
}
public componentWillUnmount() {
this.monaco!.destroy();
}
public render() {
return (
<EuiFlexItem data-test-subj="codeSourceViewer" className="codeOverflowHidden" grow={1}>
<Shortcut
keyCode="f"
help="With editor active Find in file"
linuxModifier={[Modifier.ctrl]}
macModifier={[Modifier.meta]}
winModifier={[Modifier.ctrl]}
/>
<div tabIndex={0} className="codeContainer__editor" id="mainEditor" />
{this.renderReferences()}
</EuiFlexItem>
);
}
public loadBlame(blames: GitBlame[]) {
if (this.blameWidgets) {
this.destroyBlameWidgets();
}
this.blameWidgets = blames.map((b, index) => {
return new BlameWidget(b, index === 0, this.monaco!.editor!);
});
if (!this.lineDecorations) {
this.lineDecorations = this.monaco!.editor!.deltaDecorations(
[],
[
{
range: new monaco.Range(1, 1, Infinity, 1),
options: { isWholeLine: true, linesDecorationsClassName: 'code-line-decoration' },
},
]
);
}
}
public destroyBlameWidgets() {
if (this.blameWidgets) {
this.blameWidgets.forEach((bw: BlameWidget) => bw.destroy());
}
if (this.lineDecorations) {
this.monaco!.editor!.deltaDecorations(this.lineDecorations!, []);
this.lineDecorations = null;
}
this.blameWidgets = null;
}
private async loadText(
text: string,
repo: string,
file: string,
lang: string,
revision: string,
qs: string
) {
if (this.monaco) {
this.editor = await this.monaco.loadFile(repo, file, text, lang, revision);
this.editor.onMouseDown((e: editorInterfaces.IEditorMouseEvent) => {
if (e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS) {
const uri = `${repo}/blob/${encodeRevisionString(revision)}/${file}`;
history.push(`/${uri}!L${e.target.position.lineNumber}:0${qs}`);
}
this.monaco!.container.focus();
});
}
}
private revealPosition(pos: Position | undefined) {
if (this.monaco) {
if (pos) {
this.monaco.revealPosition(pos.line, pos.character);
} else {
this.monaco.clearLineSelection();
}
}
}
private renderReferences() {
return (
this.props.isReferencesOpen && (
<ReferencesPanel
onClose={() => this.props.closeReferences(true)}
references={this.props.references}
isLoading={this.props.isReferencesLoading}
title={this.props.referencesTitle}
refUrl={this.props.refUrl}
/>
)
);
}
}
const mapStateToProps = (state: RootState) => ({
file: state.file.file,
isReferencesOpen: state.editor.showing,
isReferencesLoading: state.editor.loading,
references: state.editor.references,
referencesTitle: state.editor.referencesTitle,
hover: state.editor.hover,
refUrl: refUrlSelector(state),
revealPosition: state.editor.revealPosition,
blames: state.blame.blames,
});
const mapDispatchToProps = {
closeReferences,
findReferences,
hoverResult,
};
export const Editor = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(EditorComponent)
);

View file

@ -0,0 +1,33 @@
.code-editor-references-panel {
position: relative;
max-height: 50vh;
display: flex;
flex-direction: column;
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.05), 0px -4px 4px rgba(0, 0, 0, 0.03),
0px -6px 12px rgba(0, 0, 0, 0.05), 0px -12px 24px rgba(0, 0, 0, 0.05);
}
.code-editor-references-panel.expanded {
position: relative;
flex-grow: 10;
max-height: 95%;
height: 95%;
}
.code-editor-reference-accordion-button {
font-size: 13px;
}
.expandButton {
position: absolute;
top: -1 * $euiSize;
right: $euiSize + 1px;
background: $euiColorLightestShade;
border: $euiBorderThin;
border-bottom: 0;
height: 0;
min-height: $euiSize;
padding: 0;
border-radius: $euiSizeXS $euiSizeXS 0 0;
}

View file

@ -0,0 +1,163 @@
/*
* 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 {
EuiAccordion,
EuiButtonIcon,
EuiLoadingKibana,
EuiPanel,
EuiSpacer,
EuiText,
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 { GroupedFileReferences, GroupedRepoReferences } from '../../actions';
import { history } from '../../utils/url';
import { CodeBlock } from '../codeblock/codeblock';
interface Props {
isLoading: boolean;
title: string;
references: GroupedRepoReferences[];
refUrl?: string;
onClose(): void;
}
interface State {
expanded: boolean;
}
export class ReferencesPanel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
expanded: false,
};
}
public close = () => {
this.props.onClose();
};
public toggleExpand = () => {
this.setState({ expanded: !this.state.expanded });
};
public render() {
const body = this.props.isLoading ? <EuiLoadingKibana size="xl" /> : this.renderGroupByRepo();
const styles: any = {};
const expanded = this.state.expanded;
return (
<EuiPanel
grow={false}
className={classname(['code-editor-references-panel', expanded ? 'expanded' : ''])}
style={styles}
>
<EuiButtonIcon
size="s"
onClick={this.toggleExpand}
iconType={expanded ? 'arrowDown' : 'arrowUp'}
aria-label="Next"
className="expandButton"
/>
{!expanded && (
<EuiButtonIcon
className="euiFlyout__closeButton"
size="s"
onClick={this.close}
iconType="cross"
aria-label="Next"
/>
)}
<EuiTitle size="s">
<h3>{this.props.title}</h3>
</EuiTitle>
<EuiSpacer size="m" />
<div className="code-auto-overflow-y">{body}</div>
</EuiPanel>
);
}
private renderGroupByRepo() {
return this.props.references.map((ref: GroupedRepoReferences) => {
return this.renderReferenceRepo(ref);
});
}
private renderReferenceRepo({ repo, files }: GroupedRepoReferences) {
const [org, name] = repo.split('/').slice(1);
const buttonContent = (
<span>
<span>{org}</span>/<b>{name}</b>
</span>
);
return (
<EuiAccordion
id={repo}
key={repo}
buttonContentClassName="code-editor-reference-accordion-button"
buttonContent={buttonContent}
paddingSize="s"
initialIsOpen={true}
>
{files.map(file => this.renderReference(file))}
</EuiAccordion>
);
}
private renderReference(file: GroupedFileReferences) {
const key = `${file.uri}`;
const lineNumberFn = (l: number) => {
return file.lineNumbers[l - 1];
};
const fileComponent = (
<React.Fragment>
<EuiText>
<a href={`#${this.computeUrl(file.uri)}`}>{file.file}</a>
</EuiText>
<EuiSpacer size="s" />
</React.Fragment>
);
return (
<CodeBlock
key={key}
language={file.language}
startLine={0}
code={file.code}
folding={false}
lineNumbersFunc={lineNumberFn}
highlightRanges={file.highlights}
fileComponent={fileComponent}
onClick={this.onCodeClick.bind(this, file.lineNumbers, 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 computeUrl(url: string, line?: number) {
const { uri } = parseSchema(url)!;
let search = history.location.search;
if (search.startsWith('?')) {
search = search.substring(1);
}
const queries = queryString.parse(search);
const query = queryString.stringify({
...queries,
tab: 'references',
refUrl: this.props.refUrl,
});
return line !== undefined ? `${uri}!L${line}:0?${query}` : `${uri}?${query}`;
}
}

View file

@ -0,0 +1,203 @@
{
"node":{
"name":"",
"path":"",
"type":1,
"childrenCount":19,
"children":[
{
"name":"android",
"path":"android",
"sha1":"a2ea0b08e5d3c02cdfd6648b1e6e930eea95c2f2",
"type":1
},
{
"name":"futures",
"path":"futures",
"sha1":"9d34a7a1aeb1c0a158134897d689b45ee3ba10cc",
"type":1
},
{
"name":"guava",
"path":"guava",
"sha1":"17532aab4ac810a06f0f258bffaff50d55e4ee94",
"type":1,
"childrenCount":3,
"children":[
{
"name":"javadoc-link",
"path":"guava/javadoc-link",
"sha1":"c03fe568863a7d437f1f69712583c5381f1225f2",
"type":1
},
{
"name":"pom.xml",
"path":"guava/pom.xml",
"sha1":"845e4b4c9428c26f3403c55eb75ecc2c0f4bb798",
"type":0
},
{
"name":"src",
"path":"guava/src",
"sha1":"c652417027db78632a0458783eb5424eed30ec15",
"type":1,
"childrenCount":1,
"children":[
{
"name":"com",
"path":"guava/src/com",
"sha1":"8aef9b1b3385f9480a8ccc5d3f3f3fe9e225bf30",
"type":1,
"childrenCount":1,
"children":[
{
"name":"google",
"path":"guava/src/com/google",
"sha1":"8ea38b7a4b545ee5e194cdf018601fb802b08173",
"type":1,
"childrenCount":2,
"children":[
{
"name":"common",
"path":"guava/src/com/google/common",
"sha1":"ad31410caf6bd224498690453f7699080a3e0df6",
"type":1
},
{
"name":"thirdparty",
"path":"guava/src/com/google/thirdparty",
"sha1":"935695a8668173410c23e8a4d44871bce3bddb32",
"type":1,
"childrenCount":1,
"children":[
{
"name":"publicsuffix",
"path":"guava/src/com/google/thirdparty/publicsuffix",
"sha1":"ca7a68b3c5a0ebe251364d342e1d9309a63bfd65",
"type":1
}
]
}
]
}
]
}
]
}
]
},
{
"name":"guava-bom",
"path":"guava-bom",
"sha1":"506da491b49b1a9db603ffe96ff748dbe6c666cf",
"type":1,
"childrenCount":1,
"children":[
{
"name":"pom.xml",
"path":"guava-bom/pom.xml",
"sha1":"d32778eaae10d029fab8129a90d942da166b6e29",
"type":0
}
]
},
{
"name":"guava-gwt",
"path":"guava-gwt",
"sha1":"01b81f8d124d4cc4222ec9be4426940dd066dd64",
"type":1
},
{
"name":"guava-testlib",
"path":"guava-testlib",
"sha1":"ba68ef1df869adbc3e2a72372ec96f80d7723d7c",
"type":1
},
{
"name":"guava-tests",
"path":"guava-tests",
"sha1":"c51f5c29a9320903910db537270fbc04039ea3ef",
"type":1
},
{
"name":"refactorings",
"path":"refactorings",
"sha1":"73dc70e964c07e5c4d1dc210e8c95f160fd7e4c1",
"type":1
},
{
"name":"util",
"path":"util",
"sha1":"121ac413ef81b5f3d80d059f024b0d4c2dde31ae",
"type":1
},
{
"name":".gitattributes",
"path":".gitattributes",
"sha1":"1e3b76511d02b52d500387c8d40060a57d109c79",
"type":0
},
{
"name":".gitignore",
"path":".gitignore",
"sha1":"942c3986a9b7a2d5fb5c66f0d404f2665fe89fc0",
"type":0
},
{
"name":".travis.yml",
"path":".travis.yml",
"sha1":"0890618156c2427bb0f75df4b286b8d06e393dd8",
"type":0
},
{
"name":"CONTRIBUTING.md",
"path":"CONTRIBUTING.md",
"sha1":"8acd79c21bb287fe73323cc046ab1157cafb194a",
"type":0
},
{
"name":"CONTRIBUTORS",
"path":"CONTRIBUTORS",
"sha1":"88ecb640d1a2d7af7b1b58657bb0475db0e07d77",
"type":0
},
{
"name":"COPYING",
"path":"COPYING",
"sha1":"d645695673349e3947e8e5ae42332d0ac3164cd7",
"type":0
},
{
"name":"README.md",
"path":"README.md",
"sha1":"dea66d815f15cec63775c536e90cb309af501dec",
"type":0
},
{
"name":"cycle_whitelist.txt",
"path":"cycle_whitelist.txt",
"sha1":"e9c70c3ef79a97f22156f01be7d3768eec8b1405",
"type":0
},
{
"name":"javadoc-stylesheet.css",
"path":"javadoc-stylesheet.css",
"sha1":"64cbb4fbc6ef079832263190e209c27cecad8fff",
"type":0
},
{
"name":"pom.xml",
"path":"pom.xml",
"sha1":"e688d8edf2b4ce5bf847743fee567a34c2f601d2",
"type":0
}
],
"repoUri":"github.com/google/guava"
},
"openedPaths":[
"guava/src/com/google",
"guava",
"guava/src",
"guava/src/com"
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
/*
* 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 { History, Location } from 'history';
import React from 'react';
import { match } from 'react-router-dom';
import renderer from 'react-test-renderer';
import { MainRouteParams, PathTypes } from '../../common/types';
import { createHistory, createLocation, createMatch, mockFunction } from '../../utils/test_utils';
import props from './__fixtures__/props.json';
import { CodeFileTree } from './file_tree';
const location: Location = createLocation({
pathname: '/github.com/google/guava/tree/master/guava/src/com/google',
});
const m: match<MainRouteParams> = createMatch({
path: '/:resource/:org/:repo/:pathType(blob|tree)/:revision/:path*:goto(!.*)?',
url: '/github.com/google/guava/tree/master/guava/src/com/google',
isExact: true,
params: {
resource: 'github.com',
org: 'google',
repo: 'guava',
pathType: PathTypes.tree,
revision: 'master',
path: 'guava/src/com/google',
},
});
const history: History = createHistory({ location, length: 8, action: 'POP' });
test('render correctly', () => {
const tree = renderer
.create(
<CodeFileTree
node={props.node}
openedPaths={props.openedPaths}
history={history}
match={m}
location={location}
closeTreePath={mockFunction}
openTreePath={mockFunction}
fetchRepoTree={mockFunction}
/>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View file

@ -0,0 +1,270 @@
/*
* 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 { EuiIcon, EuiSideNav, EuiText } from '@elastic/eui';
import classes from 'classnames';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { FileTree as Tree, FileTreeItemType } from '../../../model';
import { closeTreePath, fetchRepoTree, FetchRepoTreePayload, openTreePath } from '../../actions';
import { EuiSideNavItem, MainRouteParams, PathTypes } from '../../common/types';
import { RootState } from '../../reducers';
import { encodeRevisionString } from '../../utils/url';
interface Props extends RouteComponentProps<MainRouteParams> {
node?: Tree;
closeTreePath: (paths: string) => void;
openTreePath: (paths: string) => void;
fetchRepoTree: (p: FetchRepoTreePayload) => void;
openedPaths: string[];
treeLoading?: boolean;
}
export class CodeFileTree extends React.Component<Props> {
public componentDidMount(): void {
const { path } = this.props.match.params;
if (path) {
this.props.openTreePath(path);
}
}
public fetchTree(path = '', isDir: boolean) {
const { resource, org, repo, revision } = this.props.match.params;
this.props.fetchRepoTree({
uri: `${resource}/${org}/${repo}`,
revision,
path: path || '',
isDir,
});
}
public onClick = (node: Tree) => {
const { resource, org, repo, revision, path } = this.props.match.params;
if (!(path === node.path)) {
let pathType: PathTypes;
if (node.type === FileTreeItemType.Link || node.type === FileTreeItemType.File) {
pathType = PathTypes.blob;
} else {
pathType = PathTypes.tree;
}
this.props.history.push(
`/${resource}/${org}/${repo}/${pathType}/${encodeRevisionString(revision)}/${node.path}`
);
}
};
public toggleTree = (path: string) => {
if (this.isPathOpen(path)) {
this.props.closeTreePath(path);
} else {
this.props.openTreePath(path);
}
};
public flattenDirectory: (node: Tree) => Tree[] = (node: Tree) => {
if (node.childrenCount === 1 && node.children![0].type === FileTreeItemType.Directory) {
return [node, ...this.flattenDirectory(node.children![0])];
} else {
return [node];
}
};
public scrollIntoView(el: any) {
if (el) {
const rect = el.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = elemTop >= 0 && elemBottom <= window.innerHeight;
if (!isVisible) {
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
}
}
}
public getItemRenderer = (node: Tree, forceOpen: boolean, flattenFrom?: Tree) => () => {
const className = 'codeFileTree__item kbn-resetFocusState';
let bg = null;
if (this.props.match.params.path === node.path) {
bg = <div ref={el => this.scrollIntoView(el)} className="codeFileTree__node--fullWidth" />;
}
const onClick = () => {
const path = flattenFrom ? flattenFrom.path! : node.path!;
this.toggleTree(path);
this.onClick(node);
};
switch (node.type) {
case FileTreeItemType.Directory: {
return (
<div className="codeFileTree__node">
<div
data-test-subj={`codeFileTreeNode-Directory-${node.path}`}
className={className}
role="button"
tabIndex={0}
onKeyDown={onClick}
onClick={onClick}
>
{forceOpen ? (
<EuiIcon type="arrowDown" size="s" className="codeFileTree__icon" />
) : (
<EuiIcon type="arrowRight" size="s" className="codeFileTree__icon" />
)}
<EuiIcon
type={forceOpen ? 'folderOpen' : 'folderClosed'}
data-test-subj={`codeFileTreeNode-Directory-Icon-${node.path}-${
forceOpen ? 'open' : 'closed'
}`}
/>
<span className="codeFileTree__directory">
<EuiText size="s" grow={false} className="eui-displayInlineBlock">
{`${node.name}/`}
</EuiText>
</span>
</div>
{bg}
</div>
);
}
case FileTreeItemType.Submodule: {
return (
<div className="codeFileTree__node">
<div
data-test-subj={`codeFileTreeNode-Submodule-${node.path}`}
tabIndex={0}
onKeyDown={onClick}
onClick={onClick}
className={classes(className, 'codeFileTree__file')}
role="button"
>
<EuiIcon type="submodule" />
<span className="codeFileTree__directory">
<EuiText size="s" grow={false} color="default" className="eui-displayInlineBlock">
{node.name}
</EuiText>
</span>
</div>
{bg}
</div>
);
}
case FileTreeItemType.Link: {
return (
<div className="codeFileTree__node">
<div
data-test-subj={`codeFileTreeNode-Link-${node.path}`}
tabIndex={0}
onKeyDown={onClick}
onClick={onClick}
className={classes(className, 'codeFileTree__file')}
role="button"
>
<EuiIcon type="symlink" />
<span className="codeFileTree__directory">
<EuiText size="s" grow={false} color="default" className="eui-displayInlineBlock">
{node.name}
</EuiText>
</span>
</div>
{bg}
</div>
);
}
case FileTreeItemType.File: {
return (
<div className="codeFileTree__node">
<div
data-test-subj={`codeFileTreeNode-File-${node.path}`}
tabIndex={0}
onKeyDown={onClick}
onClick={onClick}
className={classes(className, 'codeFileTree__file')}
role="button"
>
<EuiIcon type="document" />
<span className="codeFileTree__directory">
<EuiText size="s" grow={false} color="default" className="eui-displayInlineBlock">
{node.name}
</EuiText>
</span>
</div>
{bg}
</div>
);
}
}
};
public treeToItems = (node: Tree): EuiSideNavItem => {
const forceOpen =
node.type === FileTreeItemType.Directory ? this.isPathOpen(node.path!) : false;
const data: EuiSideNavItem = {
id: node.name,
name: node.name,
isSelected: false,
renderItem: this.getItemRenderer(node, forceOpen),
forceOpen,
onClick: () => void 0,
};
if (node.type === FileTreeItemType.Directory && Number(node.childrenCount) > 0) {
const nodes = this.flattenDirectory(node);
const length = nodes.length;
if (length > 1 && !(this.props.match.params.path === node.path)) {
data.name = nodes.map(n => n.name).join('/');
data.id = data.name;
const lastNode = nodes[length - 1];
const flattenNode = {
...lastNode,
name: data.name,
id: data.id,
};
data.forceOpen = this.isPathOpen(node.path!);
data.renderItem = this.getItemRenderer(flattenNode, data.forceOpen, node);
if (data.forceOpen && Number(flattenNode.childrenCount) > 0) {
data.items = flattenNode.children!.map(this.treeToItems);
}
} else if (forceOpen && node.children) {
data.items = node.children.map(this.treeToItems);
}
}
return data;
};
public render() {
const items = [
{
name: '',
id: '',
items: (this.props.node!.children || []).map(this.treeToItems),
},
];
return this.props.node && <EuiSideNav items={items} isOpenOnMobile={true} />;
}
private isPathOpen(path: string) {
return this.props.openedPaths.includes(path);
}
}
const mapStateToProps = (state: RootState) => ({
node: state.file.tree,
openedPaths: state.file.openedPaths,
treeLoading: state.file.loading,
});
const mapDispatchToProps = {
fetchRepoTree,
closeTreePath,
openTreePath,
};
export const FileTree = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(CodeFileTree)
);

View file

@ -0,0 +1,32 @@
/*
* 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 chrome from 'ui/chrome';
import { EuiButton, EuiHorizontalRule, EuiText, EuiSpacer } from '@elastic/eui';
import { documentationLinks } from '../../lib/documentation_links';
export class HelpMenu extends React.PureComponent {
public render() {
return (
<React.Fragment>
<EuiHorizontalRule margin="none" />
<EuiSpacer />
<EuiText size="s">
<p>For Code specific information</p>
</EuiText>
<EuiSpacer />
<EuiButton fill iconType="popout" href={chrome.addBasePath('/app/code#/setup-guide')}>
Setup Guide
</EuiButton>
<EuiSpacer />
<EuiButton fill iconType="popout" href={documentationLinks.code} target="_blank">
Code documentation
</EuiButton>
</React.Fragment>
);
}
}

View file

@ -0,0 +1,7 @@
/*
* 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 { HelpMenu } from './help_menu';

View file

@ -0,0 +1,46 @@
/*
* 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 { EuiButton, EuiFlexGroup } from '@elastic/eui';
// @ts-ignore
import { renderMarkdown } from 'monaco-editor/esm/vs/base/browser/htmlContentRenderer';
// @ts-ignore
import { tokenizeToString } from 'monaco-editor/esm/vs/editor/common/modes/textToHtmlTokenizer';
import React from 'react';
import { HoverState } from './hover_widget';
export interface HoverButtonProps {
state: HoverState;
gotoDefinition: () => void;
findReferences: () => void;
}
export class HoverButtons extends React.PureComponent<HoverButtonProps> {
public render() {
return (
<React.Fragment>
<EuiFlexGroup className="button-group euiFlexGroup" gutterSize="none" responsive={true}>
<EuiButton
size="s"
isDisabled={this.props.state !== HoverState.READY}
onClick={this.props.gotoDefinition}
data-test-subj="codeGoToDefinitionButton"
>
Goto Definition
</EuiButton>
<EuiButton
size="s"
isDisabled={this.props.state !== HoverState.READY}
onClick={this.props.findReferences}
data-test-subj="codeFindReferenceButton"
>
Find Reference
</EuiButton>
</EuiFlexGroup>
</React.Fragment>
);
}
}

View file

@ -0,0 +1,101 @@
/*
* 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 { EuiText } from '@elastic/eui';
// @ts-ignore
import { renderMarkdown } from 'monaco-editor/esm/vs/base/browser/htmlContentRenderer';
// @ts-ignore
import { tokenizeToString } from 'monaco-editor/esm/vs/editor/common/modes/textToHtmlTokenizer';
import React from 'react';
import { MarkedString } from 'vscode-languageserver-types';
export interface HoverWidgetProps {
state: HoverState;
contents?: MarkedString[];
gotoDefinition: () => void;
findReferences: () => void;
}
export enum HoverState {
LOADING,
INITIALIZING,
READY,
}
export class HoverWidget extends React.PureComponent<HoverWidgetProps> {
public render() {
let contents;
switch (this.props.state) {
case HoverState.READY:
contents = this.renderContents();
break;
case HoverState.INITIALIZING:
contents = this.renderInitialting();
break;
case HoverState.LOADING:
default:
contents = this.renderLoading();
}
return <React.Fragment>{contents}</React.Fragment>;
}
private renderLoading() {
return (
<div className="hover-row">
<div className="text-placeholder gradient" />
<div className="text-placeholder gradient" style={{ width: '90%' }} />
<div className="text-placeholder gradient" style={{ width: '75%' }} />
</div>
);
}
private renderContents() {
return this.props
.contents!.filter(content => !!content)
.map((markedString, idx) => {
let markdown: string;
if (typeof markedString === 'string') {
markdown = markedString;
} else if (markedString.language) {
markdown = '```' + markedString.language + '\n' + markedString.value + '\n```';
} else {
markdown = markedString.value;
}
const renderedContents: string = renderMarkdown(
{ value: markdown },
{
codeBlockRenderer: (language: string, value: string) => {
const code = tokenizeToString(value, language);
return `<span>${code}</span>`;
},
}
).innerHTML;
return (
<div
className="hover-row"
key={`hover_${idx}`}
dangerouslySetInnerHTML={{ __html: renderedContents }}
/>
);
});
}
private renderInitialting() {
return (
<div className="hover-row">
{/*
// @ts-ignore */}
<EuiText textAlign="center">
<h4>Language Server is initializing</h4>
<EuiText size="xs" color="subdued">
<p>Depending on the size of your repo, this could take a few minutes.</p>
</EuiText>
</EuiText>
</div>
);
}
}

View file

@ -0,0 +1,48 @@
/*
* 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 { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor } from '@elastic/eui';
import _ from 'lodash';
import moment from 'moment';
import React from 'react';
import { GitBlame } from '../../../common/git_blame';
export class Blame extends React.PureComponent<{ blame: GitBlame; isFirstLine: boolean }> {
public render(): React.ReactNode {
const { blame, isFirstLine } = this.props;
return (
<EuiFlexGroup
className={isFirstLine ? 'codeBlame__item codeBlame__item--first ' : 'codeBlame__item'}
gutterSize="none"
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
<EuiAvatar
size="s"
type="space"
className="codeAvatar"
name={blame.committer.name}
initialsLength={1}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" className="codeText__blameMessage eui-textTruncate">
{blame.commit.message}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false} className="eui-textTruncate">
<EuiText size="xs" className="eui-textTruncate code-auto-margin">
<EuiTextColor color="subdued">{moment(blame.commit.date).fromNow()}</EuiTextColor>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.
*/
// @ts-ignore
import { EuiBreadcrumbs } from '@elastic/eui';
import React from 'react';
import { MainRouteParams } from '../../common/types';
import { encodeRevisionString } from '../../utils/url';
interface Props {
routeParams: MainRouteParams;
}
export class Breadcrumb extends React.PureComponent<Props> {
public render() {
const { resource, org, repo, revision, path } = this.props.routeParams;
const repoUri = `${resource}/${org}/${repo}`;
const breadcrumbs: Array<{ text: string; href: string; className?: string }> = [];
const pathSegments = path ? path.split('/') : [];
pathSegments.forEach((p, index) => {
const paths = pathSegments.slice(0, index + 1);
const href = `#${repoUri}/tree/${encodeRevisionString(revision)}/${paths.join('/')}`;
breadcrumbs.push({
text: p,
href,
className: 'codeNoMinWidth',
});
});
return <EuiBreadcrumbs max={Number.MAX_VALUE} breadcrumbs={breadcrumbs} />;
}
}

View file

@ -0,0 +1,64 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
import { CloneProgress } from '../../../model';
interface Props {
repoName: string;
progress: number;
cloneProgress: CloneProgress;
}
export const CloneStatus = (props: Props) => {
const { progress: progressRate, cloneProgress, repoName } = props;
let progress = `Receiving objects: ${progressRate.toFixed(2)}%`;
if (progressRate < 0) {
progress = 'Clone Failed';
} else if (cloneProgress) {
const { receivedObjects, totalObjects, indexedObjects } = cloneProgress;
if (receivedObjects === totalObjects) {
progress = `Indexing objects: ${progressRate.toFixed(
2
)}% (${indexedObjects}/${totalObjects})`;
} else {
progress = `Receiving objects: ${progressRate.toFixed(
2
)}% (${receivedObjects}/${totalObjects})`;
}
}
return (
<EuiFlexGroup direction="column" alignItems="center">
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
<EuiFlexItem grow={false}>
<EuiText style={{ fontSize: theme.euiSizeXXL, color: '#1A1A1A' }}>
{repoName} is cloning
</EuiText>
</EuiFlexItem>
<EuiSpacer size="s" />
<EuiFlexItem grow={false}>
<EuiText style={{ fontSize: theme.euiSizeM, color: '#69707D' }}>
Your project will be available when this process is complete
</EuiText>
</EuiFlexItem>
<EuiSpacer size="xl" />
<EuiFlexItem grow={false}>
<div>
<EuiText size="m" color="subdued">
{progress}
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ minWidth: 640 }}>
<EuiProgress color="primary" size="s" max={100} value={progressRate} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,152 @@
/*
* 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 {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiPanel,
EuiText,
EuiTextColor,
} from '@elastic/eui';
import _ from 'lodash';
import moment from 'moment';
import React from 'react';
import { connect } from 'react-redux';
import { CommitInfo } from '../../../model/commit';
import { CommitLink } from '../diff_page/commit_link';
import { RootState } from '../../reducers';
import { hasMoreCommitsSelector, treeCommitsSelector } from '../../selectors';
import { fetchMoreCommits } from '../../actions';
const COMMIT_ID_LENGTH = 8;
const Commit = (props: { commit: CommitInfo; date: string; repoUri: string }) => {
const { date, commit } = props;
const { message, committer, id } = commit;
const commitId = id
.split('')
.slice(0, COMMIT_ID_LENGTH)
.join('');
return (
<EuiPanel className="code-timeline__commit--root">
<div className="eui-textTruncate">
<EuiText size="s">
<p className="eui-textTruncate">{message}</p>
</EuiText>
<EuiText size="xs">
<EuiTextColor color="subdued">
{committer} · {date}
</EuiTextColor>
</EuiText>
</div>
<div className="code-commit-id">
<CommitLink repoUri={props.repoUri} commit={commitId} />
</div>
</EuiPanel>
);
};
const CommitGroup = (props: { commits: CommitInfo[]; date: string; repoUri: string }) => {
const commitList = props.commits.map(commit => (
<Commit commit={commit} key={commit.id} date={props.date} repoUri={props.repoUri} />
));
return (
<div className="code-timeline__commit-container">
<EuiFlexGroup justifyContent="flexStart" gutterSize="s">
<EuiFlexItem grow={false}>
<div className="code-timeline__marker" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<h4>
<EuiTextColor color="subdued">Commits on {props.date}</EuiTextColor>
</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<div className="code-timeline">{commitList}</div>
</div>
);
};
export const CommitHistoryLoading = () => (
<div className="codeLoader">
<EuiLoadingSpinner size="xl" />
</div>
);
export const PageButtons = (props: {
loading?: boolean;
disabled: boolean;
onClick: () => void;
}) => (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiButton
onClick={props.onClick}
iconType="arrowDown"
isLoading={props.loading}
isDisabled={props.disabled}
size="s"
>
More
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
export const CommitHistoryComponent = (props: {
commits: CommitInfo[];
repoUri: string;
header: React.ReactNode;
loadingCommits?: boolean;
showPagination?: boolean;
hasMoreCommit?: boolean;
fetchMoreCommits: any;
}) => {
const commits = _.groupBy(props.commits, commit => moment(commit.updated).format('YYYYMMDD'));
const commitDates = Object.keys(commits).sort((a, b) => b.localeCompare(a)); // sort desc
const commitList = commitDates.map(cd => (
<CommitGroup
commits={commits[cd]}
date={moment(cd).format('MMMM Do, YYYY')}
key={cd}
repoUri={props.repoUri}
/>
));
return (
<div className="codeContainer__commitMessages">
<div className="codeHeader__commit">{props.header}</div>
{commitList}
{!props.showPagination && props.loadingCommits && <CommitHistoryLoading />}
{props.showPagination && (
<PageButtons
disabled={!props.hasMoreCommit || props.commits.length < 10}
onClick={() => props.fetchMoreCommits(props.repoUri)}
loading={props.loadingCommits}
/>
)}
</div>
);
};
const mapStateToProps = (state: RootState) => ({
file: state.file.file,
commits: treeCommitsSelector(state) || [],
loadingCommits: state.file.loadingCommits,
hasMoreCommit: hasMoreCommitsSelector(state),
});
const mapDispatchToProps = {
fetchMoreCommits,
};
export const CommitHistory = connect(
mapStateToProps,
mapDispatchToProps
// @ts-ignore
)(CommitHistoryComponent);

View file

@ -0,0 +1,395 @@
/*
* 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 { EuiButton, EuiButtonGroup, EuiFlexGroup, EuiTitle } from '@elastic/eui';
import 'github-markdown-css/github-markdown.css';
import React from 'react';
import Markdown from 'react-markdown';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import chrome from 'ui/chrome';
import { RepositoryUtils } from '../../../common/repository_utils';
import {
FileTree,
FileTreeItemType,
SearchScope,
WorkerReservedProgress,
Repository,
} from '../../../model';
import { CommitInfo, ReferenceInfo } from '../../../model/commit';
import { changeSearchScope, FetchFileResponse, SearchOptions } from '../../actions';
import { MainRouteParams, PathTypes } from '../../common/types';
import { RepoState, RepoStatus, RootState } from '../../reducers';
import {
currentTreeSelector,
hasMoreCommitsSelector,
repoUriSelector,
statusSelector,
} from '../../selectors';
import { encodeRevisionString, history } from '../../utils/url';
import { Editor } from '../editor/editor';
import { CloneStatus } from './clone_status';
import { CommitHistory } from './commit_history';
import { Directory } from './directory';
import { ErrorPanel } from './error_panel';
import { NotFound } from './not_found';
import { TopBar } from './top_bar';
interface Props extends RouteComponentProps<MainRouteParams> {
isNotFound: boolean;
repoStatus?: RepoStatus;
tree: FileTree;
file: FetchFileResponse | undefined;
currentTree: FileTree | undefined;
commits: CommitInfo[];
branches: ReferenceInfo[];
hasMoreCommits: boolean;
loadingCommits: boolean;
onSearchScopeChanged: (s: SearchScope) => void;
repoScope: string[];
searchOptions: SearchOptions;
currentRepository?: Repository;
}
const LANG_MD = 'markdown';
enum ButtonOption {
Code = 'Code',
Blame = 'Blame',
History = 'History',
Folder = 'Directory',
}
enum ButtonLabel {
Code = 'Code',
Content = 'Content',
Download = 'Download',
Raw = 'Raw',
}
class CodeContent extends React.PureComponent<Props> {
public findNode = (pathSegments: string[], node: FileTree): FileTree | undefined => {
if (!node) {
return undefined;
} else if (pathSegments.length === 0) {
return node;
} else if (pathSegments.length === 1) {
return (node.children || []).find(n => n.name === pathSegments[0]);
} else {
const currentFolder = pathSegments.shift();
const nextNode = (node.children || []).find(n => n.name === currentFolder);
if (nextNode) {
return this.findNode(pathSegments, nextNode);
} else {
return undefined;
}
}
};
public switchButton = (id: string) => {
const { path, resource, org, repo, revision } = this.props.match.params;
const repoUri = `${resource}/${org}/${repo}`;
switch (id) {
case ButtonOption.Code:
history.push(
`/${repoUri}/${PathTypes.blob}/${encodeRevisionString(revision)}/${path || ''}`
);
break;
case ButtonOption.Folder:
history.push(
`/${repoUri}/${PathTypes.tree}/${encodeRevisionString(revision)}/${path || ''}`
);
break;
case ButtonOption.Blame:
history.push(
`/${repoUri}/${PathTypes.blame}/${encodeRevisionString(revision)}/${path || ''}`
);
break;
case ButtonOption.History:
history.push(
`/${repoUri}/${PathTypes.commits}/${encodeRevisionString(revision)}/${path || ''}`
);
break;
}
};
public openRawFile = () => {
const { path, resource, org, repo, revision } = this.props.match.params;
const repoUri = `${resource}/${org}/${repo}`;
window.open(
chrome.addBasePath(`/app/code/repo/${repoUri}/raw/${encodeRevisionString(revision)}/${path}`)
);
};
public renderButtons = () => {
let buttonId: string | undefined;
switch (this.props.match.params.pathType) {
case PathTypes.blame:
buttonId = ButtonOption.Blame;
break;
case PathTypes.blob:
buttonId = ButtonOption.Code;
break;
case PathTypes.tree:
buttonId = ButtonOption.Folder;
break;
case PathTypes.commits:
buttonId = ButtonOption.History;
break;
default:
break;
}
const currentTree = this.props.currentTree;
if (
this.props.file &&
currentTree &&
(currentTree.type === FileTreeItemType.File || currentTree.type === FileTreeItemType.Link)
) {
const { isUnsupported, isOversize, isImage, lang } = this.props.file;
const isMarkdown = lang === LANG_MD;
const isText = !isUnsupported && !isOversize && !isImage;
const buttonOptions = [
{
id: ButtonOption.Code,
label: isText && !isMarkdown ? ButtonLabel.Code : ButtonLabel.Content,
},
{
id: ButtonOption.Blame,
label: ButtonOption.Blame,
isDisabled: isUnsupported || isImage || isOversize,
},
{
id: ButtonOption.History,
label: ButtonOption.History,
},
];
const rawButtonOptions = [
{ id: 'Raw', label: isText ? ButtonLabel.Raw : ButtonLabel.Download },
];
return (
<EuiFlexGroup direction="row" alignItems="center" gutterSize="none">
<EuiButtonGroup
buttonSize="s"
color="primary"
options={buttonOptions}
type="single"
idSelected={buttonId}
onChange={this.switchButton}
className="codeButtonGroup"
/>
<EuiButtonGroup
buttonSize="s"
color="primary"
options={rawButtonOptions}
type="single"
idSelected={''}
onChange={this.openRawFile}
className="codeButtonGroup"
/>
</EuiFlexGroup>
);
} else {
return (
<EuiFlexGroup direction="row" alignItems="center" gutterSize="none">
<EuiButtonGroup
buttonSize="s"
color="primary"
options={[
{
id: ButtonOption.Folder,
label: ButtonOption.Folder,
},
{
id: ButtonOption.History,
label: ButtonOption.History,
},
]}
type="single"
idSelected={buttonId}
onChange={this.switchButton}
/>
</EuiFlexGroup>
);
}
};
public render() {
return (
<div className="codeContainer__main">
<TopBar
defaultSearchScope={this.props.currentRepository}
routeParams={this.props.match.params}
onSearchScopeChanged={this.props.onSearchScopeChanged}
buttons={this.renderButtons()}
searchOptions={this.props.searchOptions}
branches={this.props.branches}
/>
{this.renderContent()}
</div>
);
}
public shouldRenderProgress() {
if (!this.props.repoStatus) {
return false;
}
const { progress, cloneProgress, state } = this.props.repoStatus;
return (
!!progress &&
state === RepoState.CLONING &&
progress < WorkerReservedProgress.COMPLETED &&
!RepositoryUtils.hasFullyCloned(cloneProgress)
);
}
public renderProgress() {
if (!this.props.repoStatus) {
return null;
}
const { progress, cloneProgress } = this.props.repoStatus;
const { org, repo } = this.props.match.params;
return (
<CloneStatus
repoName={`${org}/${repo}`}
progress={progress ? progress : 0}
cloneProgress={cloneProgress}
/>
);
}
public renderContent() {
if (this.props.isNotFound) {
return <NotFound />;
}
if (this.shouldRenderProgress()) {
return this.renderProgress();
}
const { file, match, tree } = this.props;
const { path, pathType, resource, org, repo, revision } = match.params;
const repoUri = `${resource}/${org}/${repo}`;
switch (pathType) {
case PathTypes.tree:
const node = this.findNode(path ? path.split('/') : [], tree);
return (
<div className="codeContainer__directoryView">
<Directory node={node} />
<CommitHistory
repoUri={repoUri}
header={
<React.Fragment>
<EuiTitle size="s" className="codeMargin__title">
<h3>Recent Commits</h3>
</EuiTitle>
<EuiButton
href={`#/${resource}/${org}/${repo}/${PathTypes.commits}/${encodeRevisionString(
revision
)}/${path || ''}`}
>
View All
</EuiButton>
</React.Fragment>
}
/>
</div>
);
case PathTypes.blob:
if (!file) {
return null;
}
const {
lang: fileLanguage,
content: fileContent,
isUnsupported,
isOversize,
isImage,
} = file;
if (isUnsupported) {
return (
<ErrorPanel
title={<h2>Unsupported File</h2>}
content="Unfortunately thats an unsupported file type and were unable to render it here."
/>
);
}
if (isOversize) {
return (
<ErrorPanel
title={<h2>File is too big</h2>}
content="Sorry about that, but we cant show files that are this big right now."
/>
);
}
if (fileLanguage === LANG_MD) {
return (
<div className="markdown-body code-markdown-container kbnMarkdown__body">
<Markdown source={fileContent} escapeHtml={true} skipHtml={true} />
</div>
);
} else if (isImage) {
const rawUrl = chrome.addBasePath(`/app/code/repo/${repoUri}/raw/${revision}/${path}`);
return (
<div className="code-auto-margin">
<img src={rawUrl} alt={rawUrl} />
</div>
);
}
return (
<EuiFlexGroup direction="row" className="codeContainer__blame">
<Editor showBlame={false} />
</EuiFlexGroup>
);
case PathTypes.blame:
return (
<EuiFlexGroup direction="row" className="codeContainer__blame">
<Editor showBlame={true} />
</EuiFlexGroup>
);
case PathTypes.commits:
return (
<div className="codeContainer__history">
<CommitHistory
repoUri={repoUri}
header={
<EuiTitle className="codeMargin__title">
<h3>Commit History</h3>
</EuiTitle>
}
showPagination={true}
/>
</div>
);
}
}
}
const mapStateToProps = (state: RootState) => ({
isNotFound: state.file.isNotFound,
file: state.file.file,
tree: state.file.tree,
currentTree: currentTreeSelector(state),
branches: state.file.branches,
hasMoreCommits: hasMoreCommitsSelector(state),
loadingCommits: state.file.loadingCommits,
repoStatus: statusSelector(state, repoUriSelector(state)),
searchOptions: state.search.searchOptions,
currentRepository: state.repository.currentRepository,
});
const mapDispatchToProps = {
onSearchScopeChanged: changeSearchScope,
};
export const Content = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
// @ts-ignore
)(CodeContent)
);

View file

@ -0,0 +1,87 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTitle, IconType } from '@elastic/eui';
import React from 'react';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
import { FileTree, FileTreeItemType } from '../../../model';
import { MainRouteParams, PathTypes } from '../../common/types';
import { encodeRevisionString } from '../../utils/url';
interface DirectoryNodesProps {
title: string;
nodes: FileTree[];
getUrl: (path: string) => string;
}
const DirectoryNodes = (props: DirectoryNodesProps) => {
const typeIconMap: { [k: number]: IconType } = {
[FileTreeItemType.File]: 'document',
[FileTreeItemType.Directory]: 'folderClosed',
[FileTreeItemType.Link]: 'symlink',
[FileTreeItemType.Submodule]: 'submodule',
};
const nodes = props.nodes.map(n => (
<EuiFlexItem key={n.path} grow={false}>
<Link
className="code-link"
to={props.getUrl(n.path!)}
data-test-subj={`codeFileExplorerNode-${n.name}`}
>
<div className="code-directory__node">
<EuiIcon type={typeIconMap[n.type]} />
<EuiText size="s" className="code-fileNodeName eui-textTruncate">
{n.name}
</EuiText>
</div>
</Link>
</EuiFlexItem>
));
return (
<EuiFlexItem className="codeContainer__directoryList">
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiTitle size="s">
<h3>{props.title}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexGroup wrap direction="row" gutterSize="none" justifyContent="flexStart">
{nodes}
</EuiFlexGroup>
</EuiFlexGroup>
</EuiFlexItem>
);
};
interface Props extends RouteComponentProps<MainRouteParams> {
node?: FileTree;
}
export const Directory = withRouter((props: Props) => {
let files: FileTree[] = [];
let folders: FileTree[] = [];
if (props.node && props.node.children) {
files = props.node.children.filter(
n => n.type === FileTreeItemType.File || n.type === FileTreeItemType.Link
);
folders = props.node.children.filter(
n => n.type === FileTreeItemType.Directory || n.type === FileTreeItemType.Submodule
);
}
const { resource, org, repo, revision } = props.match.params;
const getUrl = (pathType: PathTypes) => (path: string) =>
`/${resource}/${org}/${repo}/${pathType}/${encodeRevisionString(revision)}/${path}`;
const fileList = <DirectoryNodes nodes={files} title="Files" getUrl={getUrl(PathTypes.blob)} />;
const folderList = (
<DirectoryNodes nodes={folders} title="Directories" getUrl={getUrl(PathTypes.tree)} />
);
return (
<EuiFlexGroup direction="column">
{files.length > 0 && fileList}
{folders.length > 0 && folderList}
</EuiFlexGroup>
);
});

View file

@ -0,0 +1,38 @@
/*
* 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 { EuiButton, EuiPanel, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { history } from '../../utils/url';
import { ErrorIcon } from '../shared/icons';
export const ErrorPanel = (props: { title: ReactNode; content: string }) => {
return (
<div className="codePanel__error">
<EuiPanel>
<EuiSpacer />
<EuiText textAlign="center">
<ErrorIcon />
</EuiText>
<EuiText textAlign="center">{props.title}</EuiText>
<EuiSpacer />
<EuiText textAlign="center">
<EuiTextColor>{props.content}</EuiTextColor>
</EuiText>
<EuiSpacer />
<EuiSpacer />
<EuiText textAlign="center">
<EuiButton fill={true} onClick={history.goBack}>
Go Back
</EuiButton>
</EuiText>
<EuiSpacer />
<EuiSpacer />
<EuiSpacer />
</EuiPanel>
</div>
);
};

View file

@ -0,0 +1,281 @@
.code-auto-overflow {
overflow: auto;
}
.code-auto-overflow-y {
overflow-x: hidden;
overflow-y: auto;
}
.codeOverflowHidden {
overflow: hidden;
}
.code-markdown-container {
padding: $euiSizeXL;
overflow: auto;
}
.code-auto-margin {
margin: auto;
}
.code-navigation__sidebar {
background-color: $euiColorLightestShade;
width: 16rem;
border-right: $euiBorderThin;
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-direction: column;
height: 100%;
div[role="tablist"] {
flex-grow: 0;
flex-shrink: 0;
}
div[role="tabpanel"] {
@include euiScrollBar;
width: 100%;
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
}
}
.codeFileTree--container {
flex-grow: 1;
flex-shrink: 1;
padding: $euiSizeM;
background-color: $euiColorLightestShade;
position: relative;
display: inline-block;
min-width: 100%;
height: 100%;
}
.codeFileTree__icon {
margin-right: $euiSizeS;
}
.code-directory__node {
width: calc(200rem / 14);
padding: 0 $euiSizeS;
border-radius: $euiBorderRadius;
white-space: nowrap;
color: $euiColorFullShade;
&:hover {
background-color: rgba($euiColorGhost, .10);
cursor: pointer;
}
}
.code-fileNodeName {
display: inline-block;
vertical-align: middle;
margin-left: $euiSizeS;
}
.code-timeline {
border-left: $euiBorderThick;
margin-left: $euiSizeXS;
padding: $euiSizeS 0 $euiSizeS $euiSizeS;
}
.code-timeline__marker {
width: $euiSizeS;
height: $euiSizeS;
border-radius: $euiSizeS / 2;
background-color: $euiBorderColor;
margin: auto;
}
.code-timeline__commit-container {
margin: 0 0 $euiSizeXS $euiSizeM;
.euiPanel:not(:first-of-type), .euiPanel:not(:last-of-type) {
border-radius: 0;
}
}
.euiPanel.code-timeline__commit--root {
display: flex;
flex-direction: row;
justify-content: space-between;
&:not(:first-child) {
border-top: none;
}
&:not(:first-child):not(:last-child) {
border-radius: 0;
}
&:first-child {
border-radius: $euiSizeXS $euiSizeXS 0 0;
}
&:last-child {
border-radius: 0 0 $euiSizeXS $euiSizeXS;
}
&:only-child{
border-radius: $euiSizeXS
}
}
.code-top-bar__container {
box-sizing: content-box;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: $euiSizeS;
min-height: 80px;
border-bottom: $euiBorderThin;
nav {
a {
display: inline;
}
div {
vertical-align: baseline;
}
}
}
.codeSearch__suggestion-item {
height: 3rem;
margin: 0 $euiSize;
border-radius: $euiSizeXS;
cursor: pointer;
width: calc(100% - $euiSizeXL);
&:hover {
background: $euiFocusBackgroundColor;
}
}
.codeSearch__suggestion-item--active {
background: $euiFocusBackgroundColor;
}
.codeSearch-suggestion--inner {
display: flex;
align-items: stretch;
flex-grow: 1;
align-items: center;
white-space: nowrap;
}
.codeSearch__suggestion-text {
color: $euiColorFullShade;
display: flex;
flex-direction: column;
flex-grow: 0;
flex-basis: auto;
font-family: $euiCodeFontFamily;
margin-right: $euiSizeXL;
width: auto;
overflow: hidden;
text-overflow: ellipsis;
padding: $euiSizeXS $euiSizeS;
font-size: $euiFontSizeS;
}
.codeSearch__full-text-button {
border-top: $euiBorderWidthThin solid $euiBorderColor;
padding: $euiSizeS;
text-align: center;
font-weight: bold;
background: $euiColorLightShade;
margin: $euiSizeS;
padding: $euiSizeS;
font-size: $euiFontSizeS;
}
.kbnTypeahead .kbnTypeahead__popover .kbnTypeahead__items {
overflow-x: hidden;
}
.codeSearch-suggestion__group {
border-top: $euiBorderThin;
}
.codeSearch-suggestion__group-header {
padding: $euiSizeL;
}
.codeSearch-suggestion__group-title {
font-weight: bold;
margin-left: $euiSizeS;
display: inline-block;
}
.codeSearch-suggestion__group-result {
color: $euiColorDarkShade;
font-size: $euiFontSizeXS;
}
.codeSearch-suggestion__link {
height: $euiSize;
line-height: $euiSize;
text-align: center;
font-size: $euiFontSizeXS;
margin: $euiSizeS;
}
.codeSearch-suggestion__description {
flex-grow: 1;
flex-basis: 0%;
display: flex;
flex-direction: column;
color: $euiColorDarkShade;
overflow: hidden;
text-overflow: ellipsis;
font-size: $euiFontSizeXS;
padding: $euiSizeXS $euiSizeS;
}
.codeSearch-suggestion__token {
color: $euiColorFullShade;
box-sizing: border-box;
flex-grow: 0;
flex-basis: auto;
width: $euiSizeXL;
height: $euiSizeXL;
text-align: center;
overflow: hidden;
padding: $euiSizeXS;
justify-content: center;
align-items: center;
margin-left: $euiSizeXS;
}
.code-link {
margin: 0 $euiSizeS $euiSizeS;
border-radius: $euiBorderRadius;
&:focus {
text-decoration: underline;
}
}
.codeBlame__item {
padding: $euiSizeXS $euiSizeS;
border-top: $euiBorderThin;
&.codeBlame__item--first{
border-top: none;
}
}
.codeIcon__language {
fill: $euiColorDarkestShade;
}
.codeNoMinWidth {
min-width: unset !important;
}
.code-commit-id {
@include euiCodeFont;
height: calc(20rem / 14);
margin: auto 0 auto $euiSizeM;
text-align: center;
flex-shrink: 0;
}
.code-line-decoration {
border-right: $euiBorderThick;
width: 316px !important;
}

View file

@ -0,0 +1,70 @@
/*
* 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 { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import chrome from 'ui/chrome';
import { MainRouteParams } from '../../common/types';
import { ShortcutsProvider } from '../shortcuts';
import { Content } from './content';
import { SideTabs } from './side_tabs';
import { structureSelector } from '../../selectors';
import { RootState } from '../../reducers';
interface Props extends RouteComponentProps<MainRouteParams> {
loadingFileTree: boolean;
loadingStructureTree: boolean;
hasStructure: boolean;
}
class CodeMain extends React.Component<Props> {
public componentDidMount() {
this.setBreadcrumbs();
}
public componentDidUpdate() {
chrome.breadcrumbs.pop();
this.setBreadcrumbs();
}
public setBreadcrumbs() {
const { org, repo } = this.props.match.params;
chrome.breadcrumbs.push({ text: `${org}${repo}` });
}
public componentWillUnmount() {
chrome.breadcrumbs.pop();
}
public render() {
const { loadingFileTree, loadingStructureTree, hasStructure } = this.props;
return (
<div className="codeContainer__root">
<div className="codeContainer__rootInner">
<React.Fragment>
<SideTabs
loadingFileTree={loadingFileTree}
loadingStructureTree={loadingStructureTree}
hasStructure={hasStructure}
/>
<Content />
</React.Fragment>
</div>
<ShortcutsProvider />
</div>
);
}
}
const mapStateToProps = (state: RootState) => ({
loadingFileTree: state.file.loading,
loadingStructureTree: state.symbol.loading,
hasStructure: structureSelector(state).length > 0 && !state.symbol.error,
});
export const Main = connect(mapStateToProps)(CodeMain);

View file

@ -0,0 +1,18 @@
/*
* 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 { EuiFlexGroup } from '@elastic/eui';
import React from 'react';
import { ErrorPanel } from './error_panel';
export const NotFound = () => (
<EuiFlexGroup alignItems="center" justifyContent="flexStart">
<ErrorPanel
title={<h2>404</h2>}
content="Unfortunately that page doesnt exist. You can try searching to find what youre looking for."
/>
</EuiFlexGroup>
);

View file

@ -0,0 +1,127 @@
/*
* 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 { ParsedUrlQuery } from 'querystring';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import url from 'url';
import { unique } from 'lodash';
import { SearchScope, Repository } from '../../../model';
import { MainRouteParams, SearchScopeText } from '../../common/types';
import {
AutocompleteSuggestion,
FileSuggestionsProvider,
QueryBar,
RepositorySuggestionsProvider,
SymbolSuggestionsProvider,
} from '../query_bar';
import { Shortcut } from '../shortcuts';
import { SearchOptions } from '../../actions';
interface Props extends RouteComponentProps<MainRouteParams> {
onSearchScopeChanged: (s: SearchScope) => void;
searchOptions: SearchOptions;
defaultSearchScope?: Repository;
}
export class CodeSearchBar extends React.Component<Props> {
public state = {
searchScope: SearchScope.DEFAULT,
};
public queryBar: any | null = null;
public suggestionProviders = [
new SymbolSuggestionsProvider(),
new FileSuggestionsProvider(),
new RepositorySuggestionsProvider(),
];
public onSubmit = (queryString: string) => {
const { history } = this.props;
if (queryString.trim().length === 0) {
return;
}
const query: ParsedUrlQuery = {
q: queryString,
};
if (this.props.searchOptions.repoScope) {
// search from a repo page may have a default scope of this repo
if (this.props.searchOptions.defaultRepoScopeOn && this.props.defaultSearchScope) {
query.repoScope = unique([
...this.props.searchOptions.repoScope.map(r => r.uri),
this.props.defaultSearchScope.uri,
]).join(',');
} else {
query.repoScope = this.props.searchOptions.repoScope.map(r => r.uri).join(',');
}
}
if (this.state.searchScope === SearchScope.REPOSITORY) {
query.scope = SearchScope.REPOSITORY;
}
history.push(url.format({ pathname: '/search', query }));
};
public onSelect = (item: AutocompleteSuggestion) => {
this.props.history.push(item.selectUrl);
};
public render() {
return (
<div className="codeContainer__searchBar">
<Shortcut
keyCode="p"
help={SearchScopeText[SearchScope.REPOSITORY]}
onPress={() => {
this.props.onSearchScopeChanged(SearchScope.REPOSITORY);
if (this.queryBar) {
this.queryBar.focusInput();
}
}}
/>
<Shortcut
keyCode="y"
help={SearchScopeText[SearchScope.SYMBOL]}
onPress={() => {
this.props.onSearchScopeChanged(SearchScope.SYMBOL);
if (this.queryBar) {
this.queryBar.focusInput();
}
}}
/>
<Shortcut
keyCode="s"
help={SearchScopeText[SearchScope.DEFAULT]}
onPress={() => {
this.props.onSearchScopeChanged(SearchScope.DEFAULT);
if (this.queryBar) {
this.queryBar.focusInput();
}
}}
/>
<QueryBar
query=""
onSubmit={this.onSubmit}
onSelect={this.onSelect}
appName="code"
disableAutoFocus={true}
suggestionProviders={this.suggestionProviders}
enableSubmitWhenOptionsChanged={false}
onSearchScopeChanged={this.props.onSearchScopeChanged}
ref={instance => {
if (instance) {
// @ts-ignore
this.queryBar = instance.getWrappedInstance();
}
}}
/>
</div>
);
}
}
export const SearchBar = withRouter(CodeSearchBar);

View file

@ -0,0 +1,119 @@
/*
* 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 { EuiLoadingSpinner, EuiSpacer, EuiTabbedContent, EuiText } from '@elastic/eui';
import { parse as parseQuery } from 'querystring';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { QueryString } from 'ui/utils/query_string';
import { MainRouteParams, PathTypes } from '../../common/types';
import { FileTree } from '../file_tree/file_tree';
import { Shortcut } from '../shortcuts';
import { SymbolTree } from '../symbol_tree/symbol_tree';
enum Tabs {
file = 'file',
structure = 'structure',
}
interface Props extends RouteComponentProps<MainRouteParams> {
loadingFileTree: boolean;
loadingStructureTree: boolean;
hasStructure: boolean;
}
class CodeSideTabs extends React.PureComponent<Props> {
public get sideTab(): Tabs {
const { search } = this.props.location;
let qs = search;
if (search.charAt(0) === '?') {
qs = search.substr(1);
}
const tab = parseQuery(qs).tab;
return tab === Tabs.structure ? Tabs.structure : Tabs.file;
}
public renderLoadingSpinner(text: string) {
return (
<div>
<EuiSpacer size="xl" />
<EuiSpacer size="xl" />
<EuiText textAlign="center">Loading {text} tree</EuiText>
<EuiSpacer size="m" />
<EuiText textAlign="center">
<EuiLoadingSpinner size="xl" />
</EuiText>
</div>
);
}
public get tabs() {
const fileTabContent = this.props.loadingFileTree ? (
this.renderLoadingSpinner('file')
) : (
<div className="codeFileTree__container">{<FileTree />}</div>
);
const structureTabContent = this.props.loadingStructureTree ? (
this.renderLoadingSpinner('structure')
) : (
<SymbolTree />
);
return [
{
id: Tabs.file,
name: 'File',
content: fileTabContent,
isSelected: Tabs.file === this.sideTab,
'data-test-subj': 'codeFileTreeTab',
},
{
id: Tabs.structure,
name: 'Structure',
content: structureTabContent,
isSelected: Tabs.structure === this.sideTab,
disabled: this.props.match.params.pathType === PathTypes.tree || !this.props.hasStructure,
'data-test-subj': 'codeStructureTreeTab',
},
];
}
public switchTab = (tab: Tabs) => {
const { history } = this.props;
const { pathname, search } = history.location;
// @ts-ignore
history.push(QueryString.replaceParamInUrl(`${pathname}${search}`, 'tab', tab));
};
public render() {
return (
<div>
<EuiTabbedContent
className="code-navigation__sidebar"
tabs={this.tabs}
initialSelectedTab={this.tabs.find(t => t.id === this.sideTab)}
onTabClick={tab => this.switchTab(tab.id as Tabs)}
expand={true}
selectedTab={this.tabs.find(t => t.id === this.sideTab)}
/>
<Shortcut
keyCode="t"
help="Toggle tree and symbol view in sidebar"
onPress={this.toggleTab}
/>
</div>
);
}
private toggleTab = () => {
const currentTab = this.sideTab;
if (currentTab === Tabs.file) {
this.switchTab(Tabs.structure);
} else {
this.switchTab(Tabs.file);
}
};
}
export const SideTabs = withRouter(CodeSideTabs);

View file

@ -0,0 +1,72 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
import React, { ChangeEvent } from 'react';
import { SearchScope, Repository } from '../../../model';
import { ReferenceInfo } from '../../../model/commit';
import { MainRouteParams } from '../../common/types';
import { encodeRevisionString } from '../../utils/url';
import { history } from '../../utils/url';
import { Breadcrumb } from './breadcrumb';
import { SearchBar } from './search_bar';
import { SearchOptions } from '../../actions';
interface Props {
routeParams: MainRouteParams;
onSearchScopeChanged: (s: SearchScope) => void;
buttons: React.ReactNode;
searchOptions: SearchOptions;
branches: ReferenceInfo[];
defaultSearchScope?: Repository;
}
export class TopBar extends React.Component<Props, { value: string }> {
public state = {
value: 'master',
};
public onChange = (e: ChangeEvent<HTMLSelectElement>) => {
const { resource, org, repo, path = '', pathType } = this.props.routeParams;
this.setState({
value: e.target.value,
});
const revision = this.props.branches.find(b => b.name === e.target.value)!.commit.id;
history.push(
`/${resource}/${org}/${repo}/${pathType}/${encodeRevisionString(revision)}/${path}`
);
};
public render() {
return (
<div className="code-top-bar__container">
<SearchBar
defaultSearchScope={this.props.defaultSearchScope}
onSearchScopeChanged={this.props.onSearchScopeChanged}
searchOptions={this.props.searchOptions}
/>
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem
className="codeContainer__select"
grow={false}
style={{ display: 'none' }}
>
<EuiSelect
options={this.props.branches.map(b => ({ value: b.name, text: b.name }))}
onChange={this.onChange}
/>
</EuiFlexItem>
<Breadcrumb routeParams={this.props.routeParams} />
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>{this.props.buttons}</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}
}

View file

@ -0,0 +1,55 @@
{
"file": {
"type": "file",
"total": 1,
"hasMore": false,
"suggestions": [
{
"description": "This is a file",
"end": 10,
"start": 1,
"text": "src/foo/bar.java",
"tokenType": "",
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java"
}
]
},
"repository": {
"type": "repository",
"total": 2,
"hasMore": true,
"suggestions": [
{
"description": "",
"end": 10,
"start": 1,
"text": "elastic/kibana",
"tokenType": "",
"selectUrl": "http://github.com/elastic/kibana"
},
{
"description": "",
"end": 10,
"start": 1,
"text": "elastic/elasticsearch",
"tokenType": "",
"selectUrl": "http://github.com/elastic/elasticsearch"
}
]
},
"symbol": {
"type": "symbol",
"total": 1,
"hasMore": false,
"suggestions": [
{
"description": "elastic/elasticsearch > src/foo/bar.java",
"end": 10,
"start": 1,
"text": "java.lang.String",
"tokenType": "tokenClass",
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java"
}
]
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
/*
* 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 { QueryBar } from './query_bar';

View file

@ -0,0 +1,226 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiComboBox,
EuiFlexGroup,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiPanel,
EuiSpacer,
EuiText,
EuiTextColor,
EuiTitle,
EuiNotificationBadge,
} from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import { unique } from 'lodash';
import React, { Component } from 'react';
import { Repository } from '../../../../model';
import { SearchOptions as ISearchOptions } from '../../../actions';
interface State {
isFlyoutOpen: boolean;
repoScope: Repository[];
query: string;
defaultRepoScopeOn: boolean;
}
interface Props {
repositorySearch: (p: { query: string }) => void;
saveSearchOptions: (searchOptions: ISearchOptions) => void;
repoSearchResults: any[];
searchLoading: boolean;
searchOptions: ISearchOptions;
defaultRepoOptions: Repository[];
defaultSearchScope?: Repository;
}
export class SearchOptions extends Component<Props, State> {
public state: State = {
query: '',
isFlyoutOpen: false,
repoScope: this.props.searchOptions.repoScope,
defaultRepoScopeOn: this.props.searchOptions.defaultRepoScopeOn,
};
componentDidUpdate(prevProps: Props) {
if (
this.props.searchOptions.defaultRepoScopeOn &&
!prevProps.searchOptions.defaultRepoScopeOn
) {
this.setState({ defaultRepoScopeOn: this.props.searchOptions.defaultRepoScopeOn });
}
}
public applyAndClose = () => {
if (this.state.defaultRepoScopeOn && this.props.defaultSearchScope) {
this.props.saveSearchOptions({
repoScope: unique([...this.state.repoScope, this.props.defaultSearchScope], r => r.uri),
defaultRepoScopeOn: this.state.defaultRepoScopeOn,
});
} else {
this.props.saveSearchOptions({
repoScope: this.state.repoScope,
defaultRepoScopeOn: this.state.defaultRepoScopeOn,
});
}
this.setState({ isFlyoutOpen: false });
};
public removeRepoScope = (r: string) => () => {
this.setState(prevState => {
const nextState: any = {
repoScope: prevState.repoScope.filter(rs => rs.uri !== r),
};
if (this.props.defaultSearchScope && r === this.props.defaultSearchScope.uri) {
nextState.defaultRepoScopeOn = false;
}
return nextState;
});
};
public render() {
let optionsFlyout;
const repoScope =
this.state.defaultRepoScopeOn && this.props.defaultSearchScope
? unique([...this.state.repoScope, this.props.defaultSearchScope], r => r.uri)
: this.state.repoScope;
if (this.state.isFlyoutOpen) {
const selectedRepos = repoScope.map(r => {
return (
<div key={r.uri}>
<EuiPanel paddingSize="s">
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween" alignItems="center">
<div className="codeQueryBar">
<EuiText>
<EuiTextColor color="subdued">{r.org}/</EuiTextColor>
<b>{r.name}</b>
</EuiText>
</div>
<EuiIcon
className="codeUtility__cursor--pointer"
type="cross"
onClick={this.removeRepoScope(r.uri)}
/>
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="s" />
</div>
);
});
optionsFlyout = (
<EuiFlyout
onClose={this.closeOptionsFlyout}
size="s"
aria-labelledby="flyoutSmallTitle"
className="codeSearchSettings__flyout"
>
<EuiFlyoutHeader>
<EuiTitle size="s">
<h2 id="flyoutSmallTitle" className="">
<EuiNotificationBadge size="m" className="code-notification-badge">
{repoScope.length}
</EuiNotificationBadge>
<EuiTextColor color="secondary" className="code-flyout-title">
{' '}
Search Filters{' '}
</EuiTextColor>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiTitle size="xs">
<h3>Repo Scope</h3>
</EuiTitle>
<EuiText size="xs">Add indexed repos to your search scope</EuiText>
<EuiSpacer size="m" />
<EuiComboBox
placeholder="Search to add repos"
async={true}
options={
this.state.query
? this.props.repoSearchResults.map(repo => ({
label: repo.name,
}))
: this.props.defaultRepoOptions.map(repo => ({
label: repo.name,
}))
}
selectedOptions={[]}
isLoading={this.props.searchLoading}
onChange={this.onRepoChange}
onSearchChange={this.onRepoSearchChange}
isClearable={true}
/>
<EuiSpacer size="m" />
{selectedRepos}
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="flexEnd" gutterSize="none">
<EuiButton onClick={this.applyAndClose} fill={true} iconSide="right">
Apply and Close
</EuiButton>
</EuiFlexGroup>
</EuiFlyoutBody>
</EuiFlyout>
);
}
return (
<div>
<div className="kuiLocalSearchAssistedInput__assistance">
<EuiButtonEmpty size="xs" onClick={this.toggleOptionsFlyout}>
<EuiNotificationBadge size="m" className="code-notification-badge">
{repoScope.length}
</EuiNotificationBadge>
<EuiTextColor color="secondary"> Search Filters </EuiTextColor>
</EuiButtonEmpty>
</div>
{optionsFlyout}
</div>
);
}
private onRepoSearchChange = (searchValue: string) => {
this.setState({ query: searchValue });
if (searchValue) {
this.props.repositorySearch({
query: searchValue,
});
}
};
private onRepoChange = (repos: any) => {
this.setState(prevState => ({
repoScope: unique([
...prevState.repoScope,
...repos.map((r: any) =>
[...this.props.repoSearchResults, ...this.props.defaultRepoOptions].find(
rs => rs.name === r.label
)
),
]),
}));
};
private toggleOptionsFlyout = () => {
this.setState({
isFlyoutOpen: !this.state.isFlyoutOpen,
});
};
private closeOptionsFlyout = () => {
this.setState({
isFlyoutOpen: false,
repoScope: this.props.searchOptions.repoScope,
defaultRepoScopeOn: this.props.searchOptions.defaultRepoScopeOn,
});
};
}

View file

@ -0,0 +1,131 @@
/*
* 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 { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import sinon from 'sinon';
import { SearchScope } from '../../../../model';
import { AutocompleteSuggestionType } from '../suggestions';
import props from './__fixtures__/props.json';
import { CodeQueryBar } from './query_bar';
// Injest a mock random function to fixiate the output for generating component id.
const mockMath = Object.create(global.Math);
mockMath.random = () => 0.5;
global.Math = mockMath;
test('render correctly with empty query string', () => {
const emptyFn = () => {
return;
};
const queryBarComp = mount(
<CodeQueryBar
repositorySearch={emptyFn}
saveSearchOptions={emptyFn}
repoSearchResults={[]}
searchLoading={false}
searchOptions={{ repoScope: [], defaultRepoScopeOn: false }}
query=""
disableAutoFocus={false}
appName="mockapp"
suggestionProviders={[]}
enableSubmitWhenOptionsChanged={false}
onSubmit={emptyFn}
onSelect={emptyFn}
onSearchScopeChanged={emptyFn}
searchScope={SearchScope.DEFAULT}
defaultRepoOptions={[]}
/>
);
expect(toJson(queryBarComp)).toMatchSnapshot();
});
test('render correctly with input query string changed', done => {
const emptyFn = () => {
return;
};
const emptyAsyncFn = (query: string): Promise<any> => {
return Promise.resolve();
};
const fileSuggestionsSpy = sinon.fake.returns(
Promise.resolve(props[AutocompleteSuggestionType.FILE])
);
const symbolSuggestionsSpy = sinon.fake.returns(
Promise.resolve(props[AutocompleteSuggestionType.SYMBOL])
);
const repoSuggestionsSpy = sinon.fake.returns(
Promise.resolve(props[AutocompleteSuggestionType.REPOSITORY])
);
const mockFileSuggestionsProvider = {
getSuggestions: emptyAsyncFn,
};
mockFileSuggestionsProvider.getSuggestions = fileSuggestionsSpy;
const mockSymbolSuggestionsProvider = {
getSuggestions: emptyAsyncFn,
};
mockSymbolSuggestionsProvider.getSuggestions = symbolSuggestionsSpy;
const mockRepositorySuggestionsProvider = {
getSuggestions: emptyAsyncFn,
};
mockRepositorySuggestionsProvider.getSuggestions = repoSuggestionsSpy;
const submitSpy = sinon.spy();
const queryBarComp = mount(
<MemoryRouter initialEntries={[{ pathname: '/', key: 'testKey' }]}>
<CodeQueryBar
repositorySearch={emptyFn}
saveSearchOptions={emptyFn}
repoSearchResults={[]}
searchLoading={false}
searchOptions={{ repoScope: [], defaultRepoScopeOn: false }}
query="mockquery"
disableAutoFocus={false}
appName="mockapp"
suggestionProviders={[
mockFileSuggestionsProvider,
mockSymbolSuggestionsProvider,
mockRepositorySuggestionsProvider,
]}
enableSubmitWhenOptionsChanged={false}
onSubmit={submitSpy}
onSelect={emptyFn}
onSearchScopeChanged={emptyFn}
searchScope={SearchScope.DEFAULT}
defaultRepoOptions={[]}
/>
</MemoryRouter>
);
// Input 'mockquery' in the query bar.
queryBarComp
.find('input[type="text"]')
.at(0)
.simulate('change', { target: { value: 'mockquery' } });
// Wait for 101ms to make sure the getSuggestions has been triggered.
setTimeout(() => {
expect(toJson(queryBarComp)).toMatchSnapshot();
expect(fileSuggestionsSpy.calledOnce).toBeTruthy();
expect(symbolSuggestionsSpy.calledOnce).toBeTruthy();
expect(repoSuggestionsSpy.calledOnce).toBeTruthy();
// Hit enter
queryBarComp
.find('input[type="text"]')
.at(0)
.simulate('keyDown', { keyCode: 13, key: 'Enter', metaKey: true });
expect(submitSpy.calledOnce).toBeTruthy();
done();
}, 1000);
});

View file

@ -0,0 +1,515 @@
/*
* 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 { debounce, isEqual } from 'lodash';
import React, { Component } from 'react';
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui';
import { connect } from 'react-redux';
import {
saveSearchOptions,
SearchOptions as ISearchOptions,
searchReposForScope,
} from '../../../actions';
import { matchPairs } from '../lib/match_pairs';
import { SuggestionsComponent } from './typeahead/suggestions_component';
import { SearchScope, Repository } from '../../../../model';
import { SearchScopePlaceholderText } from '../../../common/types';
import { RootState } from '../../../reducers';
import {
AutocompleteSuggestion,
AutocompleteSuggestionGroup,
SuggestionsProvider,
} from '../suggestions';
import { SearchOptions } from './options';
import { ScopeSelector } from './scope_selector';
const KEY_CODES = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
ENTER: 13,
ESC: 27,
TAB: 9,
HOME: 36,
END: 35,
};
interface Props {
query: string;
onSubmit: (query: string) => void;
onSelect: (item: AutocompleteSuggestion) => void;
disableAutoFocus?: boolean;
appName: string;
suggestionProviders: SuggestionsProvider[];
repositorySearch: (p: { query: string }) => void;
saveSearchOptions: (searchOptions: ISearchOptions) => void;
enableSubmitWhenOptionsChanged: boolean;
onSearchScopeChanged: (s: SearchScope) => void;
repoSearchResults: any[];
searchLoading: boolean;
searchScope: SearchScope;
searchOptions: ISearchOptions;
defaultRepoOptions: Repository[];
currentRepository?: Repository;
}
interface State {
query: string;
inputIsPristine: boolean;
isSuggestionsVisible: boolean;
groupIndex: number | null;
itemIndex: number | null;
suggestionGroups: AutocompleteSuggestionGroup[];
currentProps?: Props;
}
export class CodeQueryBar extends Component<Props, State> {
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (isEqual(prevState.currentProps, nextProps)) {
return null;
}
const nextState: any = {
currentProps: nextProps,
};
if (nextProps.query !== prevState.query) {
nextState.query = nextProps.query;
}
return nextState;
}
/*
Keep the "draft" value in local state until the user actually submits the query. There are a couple advantages:
1. Each app doesn't have to maintain its own "draft" value if it wants to put off updating the query in app state
until the user manually submits their changes. Most apps have watches on the query value in app state so we don't
want to trigger those on every keypress. Also, some apps (e.g. dashboard) already juggle multiple query values,
each with slightly different semantics and I'd rather not add yet another variable to the mix.
2. Changes to the local component state won't trigger an Angular digest cycle. Triggering digest cycles on every
keypress has been a major source of performance issues for us in previous implementations of the query bar.
See https://github.com/elastic/kibana/issues/14086
*/
public state = {
query: this.props.query,
inputIsPristine: true,
isSuggestionsVisible: false,
groupIndex: null,
itemIndex: null,
suggestionGroups: [],
showOptions: false,
};
public updateSuggestions = debounce(async () => {
const suggestionGroups = (await this.getSuggestions()) || [];
if (!this.componentIsUnmounting) {
this.setState({ suggestionGroups });
}
}, 100);
public inputRef: HTMLInputElement | null = null;
public optionFlyout: any | null = null;
private componentIsUnmounting = false;
public isDirty = () => {
return this.state.query !== this.props.query;
};
public loadMore = () => {
// TODO(mengwei): Add action for load more.
};
public incrementIndex = (currGroupIndex: number, currItemIndex: number) => {
let nextItemIndex = currItemIndex + 1;
if (currGroupIndex === null) {
currGroupIndex = 0;
}
let nextGroupIndex = currGroupIndex;
const group: AutocompleteSuggestionGroup = this.state.suggestionGroups[currGroupIndex];
if (currItemIndex === null || nextItemIndex >= group.suggestions.length) {
nextItemIndex = 0;
nextGroupIndex = currGroupIndex + 1;
if (nextGroupIndex >= this.state.suggestionGroups.length) {
nextGroupIndex = 0;
}
}
this.setState({
groupIndex: nextGroupIndex,
itemIndex: nextItemIndex,
});
};
public decrementIndex = (currGroupIndex: number, currItemIndex: number) => {
let prevItemIndex = currItemIndex - 1;
if (currGroupIndex === null) {
currGroupIndex = this.state.suggestionGroups.length - 1;
}
let prevGroupIndex = currGroupIndex;
if (currItemIndex === null || prevItemIndex < 0) {
prevGroupIndex = currGroupIndex - 1;
if (prevGroupIndex < 0) {
prevGroupIndex = this.state.suggestionGroups.length - 1;
}
const group: AutocompleteSuggestionGroup = this.state.suggestionGroups[prevGroupIndex];
prevItemIndex = group.suggestions.length - 1;
}
this.setState({
groupIndex: prevGroupIndex,
itemIndex: prevItemIndex,
});
};
public getSuggestions = async () => {
if (!this.inputRef) {
return;
}
const { query } = this.state;
if (query.length === 0) {
return [];
}
if (!this.props.suggestionProviders || this.props.suggestionProviders.length === 0) {
return [];
}
const { selectionStart, selectionEnd } = this.inputRef;
if (selectionStart === null || selectionEnd === null) {
return;
}
const res = await Promise.all(
this.props.suggestionProviders.map((provider: SuggestionsProvider) => {
return provider.getSuggestions(
query,
this.props.searchScope,
this.props.searchOptions.repoScope.map(repo => repo.uri)
);
})
);
return res.filter((group: AutocompleteSuggestionGroup) => group.suggestions.length > 0);
};
public selectSuggestion = (item: AutocompleteSuggestion) => {
if (!this.inputRef) {
return;
}
const { selectionStart, selectionEnd } = this.inputRef;
if (selectionStart === null || selectionEnd === null) {
return;
}
this.setState(
{
query: '',
groupIndex: null,
itemIndex: null,
isSuggestionsVisible: false,
},
() => {
if (item) {
this.props.onSelect(item);
}
}
);
};
public onOutsideClick = () => {
this.setState({ isSuggestionsVisible: false, groupIndex: null, itemIndex: null });
};
public onClickInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target instanceof HTMLInputElement) {
this.onInputChange(event.target.value);
}
};
public onClickSubmitButton = (event: React.MouseEvent<HTMLButtonElement>) => {
this.onSubmit(() => event.preventDefault());
};
public onClickSuggestion = (suggestion: AutocompleteSuggestion) => {
if (!this.inputRef) {
return;
}
this.selectSuggestion(suggestion);
this.inputRef.focus();
};
public onMouseEnterSuggestion = (groupIndex: number, itemIndex: number) => {
this.setState({ groupIndex, itemIndex });
};
public onInputChange = (value: string) => {
const hasValue = Boolean(value.trim());
this.setState({
query: value,
inputIsPristine: false,
isSuggestionsVisible: hasValue,
groupIndex: null,
itemIndex: null,
});
};
public onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.updateSuggestions();
this.onInputChange(event.target.value);
};
public onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) {
this.setState({ isSuggestionsVisible: true });
if (event.target instanceof HTMLInputElement) {
this.onInputChange(event.target.value);
}
}
};
public onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.target instanceof HTMLInputElement) {
const { isSuggestionsVisible, groupIndex, itemIndex } = this.state;
const preventDefault = event.preventDefault.bind(event);
const { target, key, metaKey } = event;
const { value, selectionStart, selectionEnd } = target;
const updateQuery = (query: string, newSelectionStart: number, newSelectionEnd: number) => {
this.setState(
{
query,
},
() => {
target.setSelectionRange(newSelectionStart, newSelectionEnd);
}
);
};
switch (event.keyCode) {
case KEY_CODES.DOWN:
event.preventDefault();
if (isSuggestionsVisible && groupIndex !== null && itemIndex !== null) {
this.incrementIndex(groupIndex, itemIndex);
} else {
this.setState({ isSuggestionsVisible: true, groupIndex: 0, itemIndex: 0 });
}
break;
case KEY_CODES.UP:
event.preventDefault();
if (isSuggestionsVisible && groupIndex !== null && itemIndex !== null) {
this.decrementIndex(groupIndex, itemIndex);
} else {
const lastGroupIndex = this.state.suggestionGroups.length - 1;
const group: AutocompleteSuggestionGroup = this.state.suggestionGroups[lastGroupIndex];
if (group !== null) {
const lastItemIndex = group.suggestions.length - 1;
this.setState({
isSuggestionsVisible: true,
groupIndex: lastGroupIndex,
itemIndex: lastItemIndex,
});
}
}
break;
case KEY_CODES.ENTER:
event.preventDefault();
if (
isSuggestionsVisible &&
groupIndex !== null &&
itemIndex !== null &&
this.state.suggestionGroups[groupIndex]
) {
const group: AutocompleteSuggestionGroup = this.state.suggestionGroups[groupIndex];
this.selectSuggestion(group.suggestions[itemIndex]);
} else {
this.onSubmit(() => event.preventDefault());
}
break;
case KEY_CODES.ESC:
event.preventDefault();
this.setState({ isSuggestionsVisible: false, groupIndex: null, itemIndex: null });
break;
case KEY_CODES.TAB:
this.setState({ isSuggestionsVisible: false, groupIndex: null, itemIndex: null });
break;
default:
if (selectionStart !== null && selectionEnd !== null) {
matchPairs({
value,
selectionStart,
selectionEnd,
key,
metaKey,
updateQuery,
preventDefault,
});
}
break;
}
}
};
public onSubmit = (preventDefault?: () => void) => {
if (preventDefault) {
preventDefault();
}
this.props.onSubmit(this.state.query);
this.setState({ isSuggestionsVisible: false });
};
public componentDidMount() {
this.updateSuggestions();
}
public componentDidUpdate(prevProps: Props) {
if (prevProps.query !== this.props.query) {
this.updateSuggestions();
}
// When search options (e.g. repository scopes) change,
// submit the search query again to refresh the search result.
if (
this.props.enableSubmitWhenOptionsChanged &&
!_.isEqual(prevProps.searchOptions, this.props.searchOptions)
) {
this.onSubmit();
}
}
public componentWillUnmount() {
this.updateSuggestions.cancel();
this.componentIsUnmounting = true;
}
public focusInput() {
if (this.inputRef) {
this.inputRef.focus();
}
}
public toggleOptionsFlyout() {
if (this.optionFlyout) {
this.optionFlyout.toggleOptionsFlyout();
}
}
public render() {
const inputRef = (node: HTMLInputElement | null) => {
if (node) {
this.inputRef = node;
}
};
const activeDescendant = this.state.isSuggestionsVisible
? `suggestion-${this.state.groupIndex}-${this.state.itemIndex}`
: '';
return (
<EuiFlexGroup responsive={false} gutterSize="none">
<EuiFlexItem grow={false}>
<ScopeSelector
scope={this.props.searchScope}
onScopeChanged={this.props.onSearchScopeChanged}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
{/* position:relative required on container so the suggestions appear under the query bar*/}
<div
style={{ position: 'relative' }}
role="combobox"
aria-haspopup="true"
aria-expanded={this.state.isSuggestionsVisible}
aria-owns="typeahead-items"
aria-controls="typeahead-items"
>
<form name="queryBarForm">
<div className="kuiLocalSearch" role="search">
<div className="kuiLocalSearchAssistedInput">
<EuiFieldText
className="kuiLocalSearchAssistedInput__input"
placeholder={SearchScopePlaceholderText[this.props.searchScope]}
value={this.state.query}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
onChange={this.onChange}
onClick={this.onClickInput}
fullWidth={true}
autoFocus={!this.props.disableAutoFocus}
inputRef={inputRef}
autoComplete="off"
spellCheck={false}
aria-label="Search input"
type="text"
data-test-subj="queryInput"
aria-autocomplete="list"
aria-controls="typeahead-items"
aria-activedescendant={activeDescendant}
role="textbox"
/>
<SearchOptions
defaultRepoOptions={this.props.defaultRepoOptions}
defaultSearchScope={this.props.currentRepository}
repositorySearch={this.props.repositorySearch}
saveSearchOptions={this.props.saveSearchOptions}
repoSearchResults={this.props.repoSearchResults}
searchLoading={this.props.searchLoading}
searchOptions={this.props.searchOptions}
ref={element => (this.optionFlyout = element)}
/>
</div>
</div>
</form>
<SuggestionsComponent
query={this.state.query}
show={this.state.isSuggestionsVisible}
suggestionGroups={this.state.suggestionGroups}
groupIndex={this.state.groupIndex}
itemIndex={this.state.itemIndex}
onClick={this.onClickSuggestion}
onMouseEnter={this.onMouseEnterSuggestion}
loadMore={this.loadMore}
/>
</div>
</EuiOutsideClickDetector>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}
const mapStateToProps = (state: RootState) => ({
repoSearchResults: state.search.scopeSearchResults.repositories,
searchLoading: state.search.isScopeSearchLoading,
searchScope: state.search.scope,
searchOptions: state.search.searchOptions,
defaultRepoOptions: state.repository.repositories.slice(0, 5),
currentRepository: state.repository.currentRepository,
});
const mapDispatchToProps = {
repositorySearch: searchReposForScope,
saveSearchOptions,
};
export const QueryBar = connect(
mapStateToProps,
mapDispatchToProps,
null,
{ withRef: true }
)(CodeQueryBar);

View file

@ -0,0 +1,72 @@
/*
* 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 {
EuiIcon,
// @ts-ignore
EuiSuperSelect,
EuiText,
} from '@elastic/eui';
import React, { Component } from 'react';
import { SearchScope } from '../../../../model';
import { SearchScopeText } from '../../../common/types';
import { pxToRem } from '../../../style/variables';
interface Props {
scope: SearchScope;
onScopeChanged: (s: SearchScope) => void;
}
export class ScopeSelector extends Component<Props> {
public scopeOptions = [
{
value: SearchScope.DEFAULT,
inputDisplay: (
<div>
<EuiText size="s">
<EuiIcon type="bullseye" /> {SearchScopeText[SearchScope.DEFAULT]}
</EuiText>
</div>
),
},
{
value: SearchScope.SYMBOL,
inputDisplay: (
<EuiText size="s">
<EuiIcon type="crosshairs" /> {SearchScopeText[SearchScope.SYMBOL]}
</EuiText>
),
},
{
value: SearchScope.REPOSITORY,
inputDisplay: (
<EuiText size="s">
<EuiIcon type="branch" /> {SearchScopeText[SearchScope.REPOSITORY]}
</EuiText>
),
},
{
value: SearchScope.FILE,
inputDisplay: (
<EuiText size="s">
<EuiIcon type="document" /> {SearchScopeText[SearchScope.FILE]}
</EuiText>
),
},
];
public render() {
return (
<EuiSuperSelect
style={{ width: pxToRem(200) }}
options={this.scopeOptions}
valueOfSelected={this.props.scope}
onChange={this.props.onScopeChanged}
/>
);
}
}

View file

@ -0,0 +1,201 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render file item 1`] = `
<SuggestionComponent
ariaId="suggestion-1-1"
innerRef={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
query="bar"
selected={false}
suggestion={
Object {
"description": "This is a file",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java",
"start": 1,
"text": "src/foo/bar.java",
"tokenType": "",
}
}
>
<div
aria-selected={false}
className="codeSearch__suggestion-item "
id="suggestion-1-1"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
<span>
src/foo/
<strong>
bar
</strong>
.java
</span>
</div>
<div
className="codeSearch-suggestion__description"
>
This is a file
</div>
</div>
</div>
</div>
</SuggestionComponent>
`;
exports[`render repository item 1`] = `
<SuggestionComponent
ariaId="suggestion-1-1"
innerRef={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
query="kibana"
selected={false}
suggestion={
Object {
"description": "",
"end": 10,
"selectUrl": "http://github.com/elastic/kibana",
"start": 1,
"text": "elastic/kibana",
"tokenType": "",
}
}
>
<div
aria-selected={false}
className="codeSearch__suggestion-item "
id="suggestion-1-1"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
<span>
elastic/
<strong>
kibana
</strong>
</span>
</div>
<div
className="codeSearch-suggestion__description"
/>
</div>
</div>
</div>
</SuggestionComponent>
`;
exports[`render symbol item 1`] = `
<SuggestionComponent
ariaId="suggestion-1-1"
innerRef={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
selected={false}
suggestion={
Object {
"description": "elastic/elasticsearch > src/foo/bar.java",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java",
"start": 1,
"text": "java.lang.String",
"tokenType": "tokenClass",
}
}
>
<div
aria-selected={false}
className="codeSearch__suggestion-item "
id="suggestion-1-1"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div
className="codeSearch-suggestion__token"
>
<EuiToken
displayOptions={Object {}}
iconType="tokenClass"
size="s"
>
<div
className="euiToken euiToken--tokenTint01 euiToken--circle euiToken--small"
>
<EuiIcon
type="tokenClass"
>
<tokenClass
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.333 7.027H9.375c-.056-.708-.48-1.187-1.222-1.187-.972 0-1.5.806-1.5 2.16 0 1.43.545 2.16 1.486 2.16.708 0 1.139-.415 1.236-1.08l1.958.015C11.236 10.418 10.181 12 8.097 12c-1.958 0-3.43-1.41-3.43-4 0-2.6 1.514-4 3.43-4 1.792 0 3.084 1.095 3.236 3.027z"
fillRule="evenodd"
/>
</svg>
</tokenClass>
</EuiIcon>
</div>
</EuiToken>
</div>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
<span>
java.lang.
<strong>
String
</strong>
</span>
</div>
<div
className="codeSearch-suggestion__description"
>
elastic/elasticsearch &gt; src/foo/bar.java
</div>
</div>
</div>
</div>
</SuggestionComponent>
`;

View file

@ -0,0 +1,639 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render empty suggestions component 1`] = `
<SuggestionsComponent
groupIndex={0}
itemIndex={0}
loadMore={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
show={true}
suggestionGroups={Array []}
/>
`;
exports[`render full suggestions component 1`] = `
<MemoryRouter
initialEntries={
Array [
Object {
"key": "testKey",
"pathname": "/",
},
]
}
>
<Router
history={
Object {
"action": "POP",
"block": [Function],
"canGo": [Function],
"createHref": [Function],
"entries": Array [
Object {
"hash": "",
"key": "testKey",
"pathname": "/",
"search": "",
},
],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"index": 0,
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"key": "testKey",
"pathname": "/",
"search": "",
},
"push": [Function],
"replace": [Function],
}
}
>
<SuggestionsComponent
groupIndex={0}
itemIndex={0}
loadMore={[Function]}
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
show={true}
suggestionGroups={
Array [
Object {
"hasMore": false,
"suggestions": Array [
Object {
"description": "elastic/elasticsearch > src/foo/bar.java",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java",
"start": 1,
"text": "java.lang.String",
"tokenType": "tokenClass",
},
],
"total": 1,
"type": "symbol",
},
Object {
"hasMore": false,
"suggestions": Array [
Object {
"description": "This is a file",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java",
"start": 1,
"text": "src/foo/bar.java",
"tokenType": "",
},
],
"total": 1,
"type": "file",
},
Object {
"hasMore": true,
"suggestions": Array [
Object {
"description": "",
"end": 10,
"selectUrl": "http://github.com/elastic/kibana",
"start": 1,
"text": "elastic/kibana",
"tokenType": "",
},
Object {
"description": "",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch",
"start": 1,
"text": "elastic/elasticsearch",
"tokenType": "",
},
],
"total": 2,
"type": "repository",
},
]
}
>
<div
className="reactSuggestionTypeahead"
>
<div
className="kbnTypeahead"
>
<div
className="kbnTypeahead__popover"
>
<div
className="kbnTypeahead__items codeSearch-suggestion__group"
data-test-subj="codeTypeaheadList-symbol"
id="kbnTypeahead__items"
key="symbol-suggestions"
onScroll={[Function]}
role="listbox"
>
<EuiFlexGroup
className="codeSearch-suggestion__group-header"
justifyContent="spaceBetween"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive codeSearch-suggestion__group-header"
>
<EuiFlexGroup
alignItems="center"
direction="row"
gutterSize="none"
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiToken
displayOptions={Object {}}
iconType="tokenSymbol"
size="s"
>
<div
className="euiToken euiToken--tokenTint07 euiToken--rectangle euiToken--small euiToken--fill"
>
<EuiIcon
type="tokenSymbol"
>
<tokenSymbol
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.316 14a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-1.333a4.667 4.667 0 1 0 0-9.334 4.667 4.667 0 0 0 0 9.334zm2.19-5.72h1.143c.019 1.448-.793 2.338-1.922 2.338-.632 0-1.194-.267-1.706-.811-.36-.397-.636-.576-1-.576-.517 0-.849.355-.885 1.083H4.983c.014-1.47.858-2.314 1.95-2.314.595 0 1.125.249 1.678.802.392.382.641.595 1.038.595.484 0 .857-.323.857-1.116z"
/>
</svg>
</tokenSymbol>
</EuiIcon>
</div>
</EuiToken>
<EuiText
className="codeSearch-suggestion__group-title"
>
<div
className="euiText euiText--medium codeSearch-suggestion__group-title"
>
Symbols
</div>
</EuiText>
</div>
</EuiFlexGroup>
<div
className="codeSearch-suggestion__group-result"
>
1
Result
</div>
</div>
</EuiFlexGroup>
<SuggestionComponent
ariaId="suggestion-0-0"
innerRef={[Function]}
key="tokenClass - 0-0 - java.lang.String"
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
selected={true}
suggestion={
Object {
"description": "elastic/elasticsearch > src/foo/bar.java",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java",
"start": 1,
"text": "java.lang.String",
"tokenType": "tokenClass",
}
}
>
<div
aria-selected={true}
className="codeSearch__suggestion-item codeSearch__suggestion-item--active"
id="suggestion-0-0"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div
className="codeSearch-suggestion__token"
>
<EuiToken
displayOptions={Object {}}
iconType="tokenClass"
size="s"
>
<div
className="euiToken euiToken--tokenTint01 euiToken--circle euiToken--small"
>
<EuiIcon
type="tokenClass"
>
<tokenClass
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.333 7.027H9.375c-.056-.708-.48-1.187-1.222-1.187-.972 0-1.5.806-1.5 2.16 0 1.43.545 2.16 1.486 2.16.708 0 1.139-.415 1.236-1.08l1.958.015C11.236 10.418 10.181 12 8.097 12c-1.958 0-3.43-1.41-3.43-4 0-2.6 1.514-4 3.43-4 1.792 0 3.084 1.095 3.236 3.027z"
fillRule="evenodd"
/>
</svg>
</tokenClass>
</EuiIcon>
</div>
</EuiToken>
</div>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
<span>
java.lang.
<strong>
String
</strong>
</span>
</div>
<div
className="codeSearch-suggestion__description"
>
elastic/elasticsearch &gt; src/foo/bar.java
</div>
</div>
</div>
</div>
</SuggestionComponent>
</div>
<div
className="kbnTypeahead__items codeSearch-suggestion__group"
data-test-subj="codeTypeaheadList-file"
id="kbnTypeahead__items"
key="file-suggestions"
onScroll={[Function]}
role="listbox"
>
<EuiFlexGroup
className="codeSearch-suggestion__group-header"
justifyContent="spaceBetween"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive codeSearch-suggestion__group-header"
>
<EuiFlexGroup
alignItems="center"
direction="row"
gutterSize="none"
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiToken
displayOptions={Object {}}
iconType="tokenFile"
size="s"
>
<div
className="euiToken euiToken--tokenTint12 euiToken--rectangle euiToken--small euiToken--fill"
>
<EuiIcon
type="tokenFile"
>
<tokenFile
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.867 2.667H4a.667.667 0 0 0-.667.666v9.334c0 .368.299.666.667.666h8a.667.667 0 0 0 .667-.666V5.619a.669.669 0 0 0-.183-.459l-2.133-2.285a.668.668 0 0 0-.484-.208m1.466 4V12H4.667V4h4v2.333c0 .184.149.334.333.334h2.333z"
/>
</svg>
</tokenFile>
</EuiIcon>
</div>
</EuiToken>
<EuiText
className="codeSearch-suggestion__group-title"
>
<div
className="euiText euiText--medium codeSearch-suggestion__group-title"
>
Files
</div>
</EuiText>
</div>
</EuiFlexGroup>
<div
className="codeSearch-suggestion__group-result"
>
1
Result
</div>
</div>
</EuiFlexGroup>
<SuggestionComponent
ariaId="suggestion-1-0"
innerRef={[Function]}
key=" - 1-0 - src/foo/bar.java"
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
selected={false}
suggestion={
Object {
"description": "This is a file",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch/src/foo/bar.java",
"start": 1,
"text": "src/foo/bar.java",
"tokenType": "",
}
}
>
<div
aria-selected={false}
className="codeSearch__suggestion-item "
id="suggestion-1-0"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
src/foo/bar.java
</div>
<div
className="codeSearch-suggestion__description"
>
This is a file
</div>
</div>
</div>
</div>
</SuggestionComponent>
</div>
<div
className="kbnTypeahead__items codeSearch-suggestion__group"
data-test-subj="codeTypeaheadList-repository"
id="kbnTypeahead__items"
key="repository-suggestions"
onScroll={[Function]}
role="listbox"
>
<EuiFlexGroup
className="codeSearch-suggestion__group-header"
justifyContent="spaceBetween"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive codeSearch-suggestion__group-header"
>
<EuiFlexGroup
alignItems="center"
direction="row"
gutterSize="none"
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiToken
displayOptions={Object {}}
iconType="tokenRepo"
size="s"
>
<div
className="euiToken euiToken--tokenTint05 euiToken--rectangle euiToken--small euiToken--fill"
>
<EuiIcon
type="tokenRepo"
>
<tokenRepo
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.533 9.067c-1.792 0-2.378.72-2.57 1.194a1.601 1.601 0 1 1-1.163-.037V5.776a1.595 1.595 0 0 1-1.067-1.51c0-.885.715-1.6 1.6-1.6.886 0 1.6.715 1.6 1.6 0 .7-.442 1.291-1.066 1.51v2.821C6.336 8.251 7.019 8 8 8c1.424 0 1.899-.715 2.053-1.19a1.603 1.603 0 0 1-.986-1.477c0-.885.714-1.6 1.6-1.6.885 0 1.6.715 1.6 1.6a1.59 1.59 0 0 1-1.115 1.526c-.139.762-.656 2.208-2.619 2.208zm-3.2 2.133a.535.535 0 0 0-.533.533c0 .294.24.534.533.534a.535.535 0 0 0 0-1.067zm0-7.467a.535.535 0 0 0-.533.534c0 .293.24.533.533.533.294 0 .534-.24.534-.533a.535.535 0 0 0-.534-.534zM10.667 4.8a.535.535 0 0 0-.534.533.535.535 0 0 0 1.067 0 .535.535 0 0 0-.533-.533z"
fillRule="evenodd"
/>
</svg>
</tokenRepo>
</EuiIcon>
</div>
</EuiToken>
<EuiText
className="codeSearch-suggestion__group-title"
>
<div
className="euiText euiText--medium codeSearch-suggestion__group-title"
>
Repos
</div>
</EuiText>
</div>
</EuiFlexGroup>
<div
className="codeSearch-suggestion__group-result"
>
2
Result
s
</div>
</div>
</EuiFlexGroup>
<SuggestionComponent
ariaId="suggestion-2-0"
innerRef={[Function]}
key=" - 2-0 - elastic/kibana"
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
selected={false}
suggestion={
Object {
"description": "",
"end": 10,
"selectUrl": "http://github.com/elastic/kibana",
"start": 1,
"text": "elastic/kibana",
"tokenType": "",
}
}
>
<div
aria-selected={false}
className="codeSearch__suggestion-item "
id="suggestion-2-0"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
elastic/kibana
</div>
<div
className="codeSearch-suggestion__description"
/>
</div>
</div>
</div>
</SuggestionComponent>
<SuggestionComponent
ariaId="suggestion-2-1"
innerRef={[Function]}
key=" - 2-1 - elastic/elasticsearch"
onClick={[Function]}
onMouseEnter={[Function]}
query="string"
selected={false}
suggestion={
Object {
"description": "",
"end": 10,
"selectUrl": "http://github.com/elastic/elasticsearch",
"start": 1,
"text": "elastic/elasticsearch",
"tokenType": "",
}
}
>
<div
aria-selected={false}
className="codeSearch__suggestion-item "
id="suggestion-2-1"
onClick={[Function]}
onMouseEnter={[Function]}
role="option"
>
<div
className="codeSearch-suggestion--inner"
>
<div>
<div
className="codeSearch__suggestion-text"
data-test-subj="codeTypeaheadItem"
>
elastic/elasticsearch
</div>
<div
className="codeSearch-suggestion__description"
/>
</div>
</div>
</div>
</SuggestionComponent>
<div
className="codeSearch-suggestion__link"
>
<Link
replace={false}
to="/search?q=string"
>
<a
href="/search?q=string"
onClick={[Function]}
>
View More
</a>
</Link>
</div>
</div>
<Link
replace={false}
to="/search?q=string"
>
<a
href="/search?q=string"
onClick={[Function]}
>
<div
className="codeSearch__full-text-button"
>
Press ⮐ Return for Full Text Search
</div>
</a>
</Link>
</div>
</div>
</div>
</SuggestionsComponent>
</Router>
</MemoryRouter>
`;

Some files were not shown because too many files have changed in this diff Show more