mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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 commite206b71171
. * [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 commit866db39ec3
. * [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 from604e4d1173
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:
parent
d2edef1965
commit
d6a11e717a
357 changed files with 38591 additions and 104 deletions
12
package.json
12
package.json
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -181,6 +181,7 @@ export default class ClusterManager {
|
|||
/[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/,
|
||||
/\.test\.js$/,
|
||||
...extraIgnores,
|
||||
'plugins/java_languageserver'
|
||||
],
|
||||
});
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
91
src/dev/build/tasks/patch_native_modules_task.js
Normal file
91
src/dev/build/tasks/patch_native_modules_task.js
Normal 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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
|
@ -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));
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
19
x-pack/plugins/code/common/git_blame.ts
Normal file
19
x-pack/plugins/code/common/git_blame.ts
Normal 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;
|
||||
};
|
||||
}
|
35
x-pack/plugins/code/common/git_diff.ts
Normal file
35
x-pack/plugins/code/common/git_diff.ts
Normal 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',
|
||||
}
|
55
x-pack/plugins/code/common/git_url_utils.test.ts
Normal file
55
x-pack/plugins/code/common/git_url_utils.test.ts
Normal 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();
|
||||
});
|
32
x-pack/plugins/code/common/git_url_utils.ts
Normal file
32
x-pack/plugins/code/common/git_url_utils.ts
Normal 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;
|
||||
}
|
26
x-pack/plugins/code/common/installation.ts
Normal file
26
x-pack/plugins/code/common/installation.ts
Normal 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;
|
||||
}
|
25
x-pack/plugins/code/common/language_server.ts
Normal file
25
x-pack/plugins/code/common/language_server.ts
Normal 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;
|
||||
}
|
37
x-pack/plugins/code/common/line_mapper.ts
Normal file
37
x-pack/plugins/code/common/line_mapper.ts
Normal 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;
|
||||
}
|
||||
}
|
44
x-pack/plugins/code/common/lsp_client.ts
Normal file
44
x-pack/plugins/code/common/lsp_client.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
13
x-pack/plugins/code/common/lsp_error_codes.ts
Normal file
13
x-pack/plugins/code/common/lsp_error_codes.ts
Normal 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;
|
39
x-pack/plugins/code/common/lsp_method.ts
Normal file
39
x-pack/plugins/code/common/lsp_method.ts
Normal 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);
|
||||
}
|
||||
}
|
206
x-pack/plugins/code/common/repository_utils.test.ts
Normal file
206
x-pack/plugins/code/common/repository_utils.test.ts
Normal 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']);
|
||||
});
|
114
x-pack/plugins/code/common/repository_utils.ts
Normal file
114
x-pack/plugins/code/common/repository_utils.ts
Normal 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;
|
||||
}
|
||||
}
|
36
x-pack/plugins/code/common/text_document_methods.ts
Normal file
36
x-pack/plugins/code/common/text_document_methods.ts
Normal 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);
|
||||
}
|
||||
}
|
86
x-pack/plugins/code/common/uri_util.test.ts
Normal file
86
x-pack/plugins/code/common/uri_util.test.ts
Normal 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();
|
||||
});
|
131
x-pack/plugins/code/common/uri_util.ts
Normal file
131
x-pack/plugins/code/common/uri_util.ts
Normal 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;
|
||||
}
|
76
x-pack/plugins/code/index.ts
Normal file
76
x-pack/plugins/code/index.ts
Normal 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,
|
||||
});
|
27
x-pack/plugins/code/model/commit.ts
Normal file
27
x-pack/plugins/code/model/commit.ts
Normal 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,
|
||||
}
|
19
x-pack/plugins/code/model/highlight.ts
Normal file
19
x-pack/plugins/code/model/highlight.ts
Normal 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
|
||||
}
|
13
x-pack/plugins/code/model/index.ts
Normal file
13
x-pack/plugins/code/model/index.ts
Normal 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';
|
16
x-pack/plugins/code/model/lsp.ts
Normal file
16
x-pack/plugins/code/model/lsp.ts
Normal 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.
|
||||
}
|
149
x-pack/plugins/code/model/repository.ts
Normal file
149
x-pack/plugins/code/model/repository.ts
Normal 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;
|
||||
}
|
155
x-pack/plugins/code/model/search.ts
Normal file
155
x-pack/plugins/code/model/search.ts
Normal 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
|
||||
}
|
12
x-pack/plugins/code/model/socket.ts
Normal file
12
x-pack/plugins/code/model/socket.ts
Normal 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',
|
||||
}
|
25
x-pack/plugins/code/model/task.ts
Normal file
25
x-pack/plugins/code/model/task.ts
Normal 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,
|
||||
}
|
21
x-pack/plugins/code/model/test_config.ts
Normal file
21
x-pack/plugins/code/model/test_config.ts
Normal 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,
|
||||
}
|
15
x-pack/plugins/code/model/workspace.ts
Normal file
15
x-pack/plugins/code/model/workspace.ts
Normal 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;
|
||||
}
|
18
x-pack/plugins/code/public/actions/blame.ts
Normal file
18
x-pack/plugins/code/public/actions/blame.ts
Normal 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');
|
12
x-pack/plugins/code/public/actions/commit.ts
Normal file
12
x-pack/plugins/code/public/actions/commit.ts
Normal 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');
|
37
x-pack/plugins/code/public/actions/editor.ts
Normal file
37
x-pack/plugins/code/public/actions/editor.ts
Normal 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');
|
77
x-pack/plugins/code/public/actions/file.ts
Normal file
77
x-pack/plugins/code/public/actions/file.ts
Normal 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');
|
30
x-pack/plugins/code/public/actions/index.ts
Normal file
30
x-pack/plugins/code/public/actions/index.ts
Normal 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');
|
21
x-pack/plugins/code/public/actions/language_server.ts
Normal file
21
x-pack/plugins/code/public/actions/language_server.ts
Normal 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');
|
14
x-pack/plugins/code/public/actions/project_config.ts
Normal file
14
x-pack/plugins/code/public/actions/project_config.ts
Normal 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');
|
11
x-pack/plugins/code/public/actions/recent_projects.ts
Normal file
11
x-pack/plugins/code/public/actions/recent_projects.ts
Normal 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');
|
46
x-pack/plugins/code/public/actions/repository.ts
Normal file
46
x-pack/plugins/code/public/actions/repository.ts
Normal 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');
|
52
x-pack/plugins/code/public/actions/search.ts
Normal file
52
x-pack/plugins/code/public/actions/search.ts
Normal 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');
|
13
x-pack/plugins/code/public/actions/shortcuts.ts
Normal file
13
x-pack/plugins/code/public/actions/shortcuts.ts
Normal 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');
|
24
x-pack/plugins/code/public/actions/status.ts
Normal file
24
x-pack/plugins/code/public/actions/status.ts
Normal 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');
|
20
x-pack/plugins/code/public/actions/structure.ts
Normal file
20
x-pack/plugins/code/public/actions/structure.ts
Normal 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');
|
60
x-pack/plugins/code/public/app.tsx
Normal file
60
x-pack/plugins/code/public/app.tsx
Normal 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);
|
||||
};
|
||||
});
|
49
x-pack/plugins/code/public/common/types.ts
Normal file
49
x-pack/plugins/code/public/common/types.ts
Normal 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;
|
||||
}
|
132
x-pack/plugins/code/public/components/admin_page/admin.tsx
Normal file
132
x-pack/plugins/code/public/components/admin_page/admin.tsx
Normal 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));
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
|
||||
<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);
|
|
@ -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);
|
286
x-pack/plugins/code/public/components/admin_page/project_tab.tsx
Normal file
286
x-pack/plugins/code/public/components/admin_page/project_tab.tsx
Normal 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);
|
169
x-pack/plugins/code/public/components/admin_page/setup_guide.tsx
Normal file
169
x-pack/plugins/code/public/components/admin_page/setup_guide.tsx
Normal 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
|
||||
<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>
|
||||
We’ve 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);
|
52
x-pack/plugins/code/public/components/app.tsx
Normal file
52
x-pack/plugins/code/public/components/app.tsx
Normal 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>
|
||||
);
|
||||
};
|
139
x-pack/plugins/code/public/components/codeblock/codeblock.tsx
Normal file
139
x-pack/plugins/code/public/components/codeblock/codeblock.tsx
Normal 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}`;
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
16
x-pack/plugins/code/public/components/diff_page/diff.scss
Normal file
16
x-pack/plugins/code/public/components/diff_page/diff.scss
Normal 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;
|
||||
}
|
246
x-pack/plugins/code/public/components/diff_page/diff.tsx
Normal file
246
x-pack/plugins/code/public/components/diff_page/diff.tsx
Normal 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)
|
||||
);
|
|
@ -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 }} />;
|
||||
}
|
||||
}
|
236
x-pack/plugins/code/public/components/editor/editor.tsx
Normal file
236
x-pack/plugins/code/public/components/editor/editor.tsx
Normal 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)
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}`;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
1443
x-pack/plugins/code/public/components/file_tree/__snapshots__/file_tree.test.tsx.snap
generated
Normal file
1443
x-pack/plugins/code/public/components/file_tree/__snapshots__/file_tree.test.tsx.snap
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
});
|
270
x-pack/plugins/code/public/components/file_tree/file_tree.tsx
Normal file
270
x-pack/plugins/code/public/components/file_tree/file_tree.tsx
Normal 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)
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
7
x-pack/plugins/code/public/components/help_menu/index.ts
Normal file
7
x-pack/plugins/code/public/components/help_menu/index.ts
Normal 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';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
101
x-pack/plugins/code/public/components/hover/hover_widget.tsx
Normal file
101
x-pack/plugins/code/public/components/hover/hover_widget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
48
x-pack/plugins/code/public/components/main/blame.tsx
Normal file
48
x-pack/plugins/code/public/components/main/blame.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
35
x-pack/plugins/code/public/components/main/breadcrumb.tsx
Normal file
35
x-pack/plugins/code/public/components/main/breadcrumb.tsx
Normal 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} />;
|
||||
}
|
||||
}
|
64
x-pack/plugins/code/public/components/main/clone_status.tsx
Normal file
64
x-pack/plugins/code/public/components/main/clone_status.tsx
Normal 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>
|
||||
);
|
||||
};
|
152
x-pack/plugins/code/public/components/main/commit_history.tsx
Normal file
152
x-pack/plugins/code/public/components/main/commit_history.tsx
Normal 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);
|
395
x-pack/plugins/code/public/components/main/content.tsx
Normal file
395
x-pack/plugins/code/public/components/main/content.tsx
Normal 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 that’s an unsupported file type and we’re unable to render it here."
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isOversize) {
|
||||
return (
|
||||
<ErrorPanel
|
||||
title={<h2>File is too big</h2>}
|
||||
content="Sorry about that, but we can’t 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)
|
||||
);
|
87
x-pack/plugins/code/public/components/main/directory.tsx
Normal file
87
x-pack/plugins/code/public/components/main/directory.tsx
Normal 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>
|
||||
);
|
||||
});
|
38
x-pack/plugins/code/public/components/main/error_panel.tsx
Normal file
38
x-pack/plugins/code/public/components/main/error_panel.tsx
Normal 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>
|
||||
);
|
||||
};
|
281
x-pack/plugins/code/public/components/main/main.scss
Normal file
281
x-pack/plugins/code/public/components/main/main.scss
Normal 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;
|
||||
}
|
70
x-pack/plugins/code/public/components/main/main.tsx
Normal file
70
x-pack/plugins/code/public/components/main/main.tsx
Normal 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);
|
18
x-pack/plugins/code/public/components/main/not_found.tsx
Normal file
18
x-pack/plugins/code/public/components/main/not_found.tsx
Normal 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 doesn’t exist. You can try searching to find what you’re looking for."
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
127
x-pack/plugins/code/public/components/main/search_bar.tsx
Normal file
127
x-pack/plugins/code/public/components/main/search_bar.tsx
Normal 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);
|
119
x-pack/plugins/code/public/components/main/side_tabs.tsx
Normal file
119
x-pack/plugins/code/public/components/main/side_tabs.tsx
Normal 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);
|
72
x-pack/plugins/code/public/components/main/top_bar.tsx
Normal file
72
x-pack/plugins/code/public/components/main/top_bar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
1437
x-pack/plugins/code/public/components/query_bar/components/__snapshots__/query_bar.test.tsx.snap
generated
Normal file
1437
x-pack/plugins/code/public/components/query_bar/components/__snapshots__/query_bar.test.tsx.snap
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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';
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 > src/foo/bar.java
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionComponent>
|
||||
`;
|
|
@ -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 > 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
Loading…
Add table
Add a link
Reference in a new issue