[Code] add an api to show status of repo file and language servers (#39566)

* [Code] add an api to show status of repo file and language servers
add a test for status api
add a field to indicate whether current file is covered by ctags or not
show status ui
This commit is contained in:
Yulong 2019-07-02 12:51:22 +08:00 committed by GitHub
parent ad77e901b2
commit 12ba1f1894
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 518 additions and 16 deletions

View file

@ -0,0 +1,67 @@
/*
* 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 RepoFileStatus {
LANG_SERVER_IS_INITIALIZING = 'Language server is initializing.',
INDEXING = 'Indexing in progress',
FILE_NOT_SUPPORTED = 'Current file is not of a supported language.',
REVISION_NOT_INDEXED = 'Current revision is not indexed.',
LANG_SERVER_NOT_INSTALLED = 'Install additional language server to support current file.',
FILE_IS_TOO_BIG = 'Current file is too big.',
}
export enum Severity {
NONE,
NOTICE,
WARNING,
ERROR,
}
export enum LangServerType {
NONE = 'Current file is not supported by any language server',
GENERIC = 'Current file is only covered by generic language server',
DEDICATED = 'Current file is covered by dedicated language server',
}
export enum CTA {
SWITCH_TO_HEAD,
GOTO_LANG_MANAGE_PAGE,
}
export const REPO_FILE_STATUS_SEVERITY = {
[RepoFileStatus.LANG_SERVER_IS_INITIALIZING]: {
severity: Severity.NOTICE,
},
[RepoFileStatus.INDEXING]: {
severity: Severity.NOTICE,
},
[RepoFileStatus.FILE_NOT_SUPPORTED]: {
severity: Severity.NOTICE,
},
[LangServerType.GENERIC]: {
severity: Severity.NOTICE,
},
[RepoFileStatus.REVISION_NOT_INDEXED]: {
severity: Severity.WARNING,
fix: CTA.SWITCH_TO_HEAD,
},
[RepoFileStatus.LANG_SERVER_NOT_INSTALLED]: {
severity: Severity.WARNING,
fix: CTA.GOTO_LANG_MANAGE_PAGE,
},
[RepoFileStatus.FILE_IS_TOO_BIG]: {
severity: Severity.NOTICE,
},
};
export interface StatusReport {
repoStatus?: RepoFileStatus.INDEXING | RepoFileStatus.REVISION_NOT_INDEXED;
fileStatus?: RepoFileStatus.FILE_NOT_SUPPORTED | RepoFileStatus.FILE_IS_TOO_BIG;
langServerType: LangServerType;
langServerStatus?:
| RepoFileStatus.LANG_SERVER_IS_INITIALIZING
| RepoFileStatus.LANG_SERVER_NOT_INSTALLED;
}

View file

@ -13,6 +13,8 @@ import {
Repository,
CloneProgress,
} from '../../model';
import { FetchFilePayload } from './file';
import { StatusReport } from '../../common/repo_file_status';
export enum RepoState {
CLONING,
@ -60,3 +62,10 @@ export const pollRepoDeleteStatusStart = createAction<string>('POLL DELETE STATU
export const pollRepoCloneStatusStop = createAction<RepositoryUri>('POLL CLONE STATUS STOP');
export const pollRepoIndexStatusStop = createAction<RepositoryUri>('POLL INDEX STATUS STOP');
export const pollRepoDeleteStatusStop = createAction<RepositoryUri>('POLL DELETE STATUS STOP');
export const FetchRepoFileStatus = createAction<FetchFilePayload>('FETCH REPO FILE STATUS');
export const FetchRepoFileStatusSuccess = createAction<{
path: FetchFilePayload;
statusReport: StatusReport;
}>('FETCH REPO FILE STATUS SUCCESS');
export const FetchRepoFileStatusFailed = createAction<any>('FETCH REPO FILE STATUS FAILED');

View file

@ -13,6 +13,7 @@ import { encodeRevisionString, decodeRevisionString } from '../../../common/uri_
import { history } from '../../utils/url';
import { Breadcrumb } from './breadcrumb';
import { SearchBar } from '../search_bar';
import { StatusIndicator } from '../status_indicator/status_indicator';
interface Props {
routeParams: MainRouteParams;
@ -83,7 +84,7 @@ export class TopBar extends React.Component<Props, { value: string }> {
className="codeTopBar__toolbar"
>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none">
<EuiFlexGroup gutterSize="l" alignItems="center">
<EuiFlexItem className="codeContainer__select" grow={false}>
<EuiSelect
options={this.branchOptions}
@ -93,6 +94,9 @@ export class TopBar extends React.Component<Props, { value: string }> {
/>
</EuiFlexItem>
<Breadcrumb routeParams={this.props.routeParams} />
<EuiFlexItem grow={false}>
<StatusIndicator />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>{this.props.buttons}</EuiFlexItem>

View file

@ -0,0 +1,3 @@
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.58956 9.05857L7.34995 4.18143H8.64995L8.40014 9.05857H7.58956ZM7.98423 10.96C7.80995 10.96 7.66066 10.8979 7.53638 10.7736C7.41209 10.6493 7.34995 10.5014 7.34995 10.33C7.34995 10.1557 7.41209 10.0071 7.53638 9.88429C7.66066 9.76143 7.80995 9.7 7.98423 9.7C8.15281 9.7 8.29923 9.76071 8.42352 9.88214C8.54781 10.0036 8.60995 10.1529 8.60995 10.33C8.60995 10.5043 8.54709 10.6529 8.42138 10.7757C8.29566 10.8986 8.14995 10.96 7.98423 10.96ZM1.99995 13C1.24089 13 0.758575 12.1875 1.12205 11.5211L7.12205 0.521148C7.50107 -0.173716 8.49883 -0.173716 8.87784 0.521148L14.8778 11.5211C15.2413 12.1875 14.759 13 13.9999 13H1.99995ZM1.99995 12H13.9999L7.99995 1L1.99995 12Z" fill="#F5A700"/>
</svg>

After

Width:  |  Height:  |  Size: 841 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" fill="#C4C4C4"/>
</svg>

After

Width:  |  Height:  |  Size: 149 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.74572 8L11.8456 4.90016C12.0515 4.69424 12.0515 4.36037 11.8456 4.15444C11.6396 3.94852 11.3058 3.94852 11.0998 4.15444L8 7.25428L4.90016 4.15444C4.69424 3.94852 4.36037 3.94852 4.15444 4.15444C3.94852 4.36037 3.94852 4.69424 4.15444 4.90016L7.25428 8L4.15444 11.0998C3.94852 11.3058 3.94852 11.6396 4.15444 11.8456C4.36037 12.0515 4.69424 12.0515 4.90016 11.8456L8 8.74572L11.0998 11.8456C11.3058 12.0515 11.6396 12.0515 11.8456 11.8456C12.0515 11.6396 12.0515 11.3058 11.8456 11.0998L8.74572 8ZM8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16Z" fill="#BD271E"/>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.31121 13.8239L8.26929 9H6.59375V7.625H9.89506L9.93699 12.4489H11.475V13.8239H8.31121ZM7.96875 5.29603C7.96875 4.97768 8.06342 4.71334 8.25275 4.503C8.44208 4.29267 8.71275 4.1875 9.06475 4.1875C9.41675 4.1875 9.68875 4.29267 9.88075 4.503C10.0728 4.71334 10.1688 4.97768 10.1688 5.29603C10.1688 5.60869 10.0728 5.86877 9.88075 6.07626C9.68875 6.28375 9.41675 6.3875 9.06475 6.3875C8.71275 6.3875 8.44208 6.28375 8.25275 6.07626C8.06342 5.86877 7.96875 5.60869 7.96875 5.29603ZM9 15.875C12.797 15.875 15.875 12.797 15.875 9C15.875 5.20304 12.797 2.125 9 2.125C5.20304 2.125 2.125 5.20304 2.125 9C2.125 12.797 5.20304 15.875 9 15.875ZM9 17.25C4.44365 17.25 0.75 13.5563 0.75 9C0.75 4.44365 4.44365 0.75 9 0.75C13.5563 0.75 17.25 4.44365 17.25 9C17.25 13.5563 13.5563 17.25 9 17.25Z" fill="#006BB4"/>
</svg>

After

Width:  |  Height:  |  Size: 953 B

View file

@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiButtonIcon, EuiPopover, EuiText } from '@elastic/eui';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import ErrorSvg from './error.svg';
import InfoSvg from './info.svg';
import AlertSvg from './alert.svg';
import BlankSvg from './blank.svg';
import {
CTA,
LangServerType,
REPO_FILE_STATUS_SEVERITY,
RepoFileStatus,
Severity,
StatusReport,
} from '../../../common/repo_file_status';
import { RootState } from '../../reducers';
import { FetchFilePayload } from '../../actions';
interface Props {
statusReport?: StatusReport;
currentStatusPath?: FetchFilePayload;
pathType: string;
}
interface State {
isPopoverOpen: boolean;
}
const svgs = {
[Severity.NOTICE]: InfoSvg,
[Severity.WARNING]: AlertSvg,
[Severity.ERROR]: ErrorSvg,
[Severity.NONE]: BlankSvg,
};
export class StatusIndicatorComponent extends React.Component<Props, State> {
constructor(props: Readonly<Props>) {
super(props);
this.state = {
isPopoverOpen: false,
};
}
closePopover() {
this.setState({
isPopoverOpen: false,
});
}
openPopover() {
this.setState({
isPopoverOpen: true,
});
}
render() {
const { statusReport } = this.props;
let severity = Severity.NONE;
const children: any[] = [];
const addError = (error: RepoFileStatus | LangServerType) => {
// @ts-ignore
const s: any = REPO_FILE_STATUS_SEVERITY[error];
if (s) {
if (s.severity > severity) {
severity = s.severity;
}
const fix = s.fix;
if (fix !== undefined) {
const fixUrl = this.fixUrl(fix);
children.push(
<p>
{error} You can {fixUrl}.
</p>
);
} else {
children.push(<p>{error}</p>);
}
}
};
if (statusReport) {
if (statusReport.repoStatus) {
addError(statusReport.repoStatus);
}
if (statusReport.fileStatus) {
addError(statusReport.fileStatus);
}
if (statusReport.langServerType === LangServerType.GENERIC) {
addError(statusReport.langServerType);
}
if (statusReport.langServerStatus) {
addError(statusReport.langServerStatus);
}
}
const svg = svgs[severity];
const icon = <EuiButtonIcon iconType={svg} onClick={this.openPopover.bind(this)} />;
if (children.length === 0) {
return <div />;
}
return (
<EuiPopover
id="trapFocus"
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover.bind(this)}
ownFocus
button={icon}
>
<EuiText size="xs">{children}</EuiText>
</EuiPopover>
);
}
private fixUrl(fix: CTA) {
switch (fix) {
case CTA.GOTO_LANG_MANAGE_PAGE:
return <Link to="/admin?tab=LanguageServers">install it here</Link>;
case CTA.SWITCH_TO_HEAD:
const { uri, path } = this.props.currentStatusPath!;
return <Link to={`/${uri}/${this.props.pathType}/HEAD/${path}`}>switch to HEAD</Link>;
}
}
}
const mapStateToProps = (state: RootState) => ({
statusReport: state.status.repoFileStatus,
currentStatusPath: state.status.currentStatusPath,
pathType: state.route.match.params.pathType,
});
export const StatusIndicator = connect(mapStateToProps)(StatusIndicatorComponent);

View file

@ -10,6 +10,8 @@ import { Action, handleActions } from 'redux-actions';
import { RepositoryUri, WorkerReservedProgress } from '../../model';
import {
deleteRepoFinished,
FetchFilePayload,
FetchRepoFileStatusSuccess,
loadStatus,
loadStatusFailed,
loadStatusSuccess,
@ -20,11 +22,14 @@ import {
RepoStatus,
RepoState,
} from '../actions';
import { StatusReport } from '../../common/repo_file_status';
export interface StatusState {
status: { [key: string]: RepoStatus };
loading: boolean;
error?: Error;
currentStatusPath?: FetchFilePayload;
repoFileStatus?: StatusReport;
}
const initialState: StatusState = {
@ -170,6 +175,12 @@ export const status = handleActions<StatusState, StatusPayload>(
produce<StatusState>(state, draft => {
delete draft.status[action.payload!];
}),
[String(FetchRepoFileStatusSuccess)]: (state: StatusState, action: Action<any>) =>
produce<StatusState>(state, (draft: StatusState) => {
const { path, statusReport } = action.payload;
draft.repoFileStatus = statusReport;
draft.currentStatusPath = path;
}),
},
initialState
);

View file

@ -47,7 +47,7 @@ import {
watchSearchRouteChange,
} from './search';
import { watchRootRoute } from './setup';
import { watchRepoCloneSuccess, watchRepoDeleteFinished } from './status';
import { watchRepoCloneSuccess, watchRepoDeleteFinished, watchStatusChange } from './status';
import { watchLoadStructure } from './structure';
export function* rootSaga() {
@ -82,6 +82,7 @@ export function* rootSaga() {
yield fork(watchInstallLanguageServer);
yield fork(watchLoadRepoListStatus);
yield fork(watchLoadRepoStatus);
yield fork(watchStatusChange);
// Repository status polling sagas begin
yield fork(watchPollingRepoStatus);

View file

@ -5,7 +5,10 @@
*/
import { Action } from 'redux-actions';
import { put, select, takeEvery } from 'redux-saga/effects';
import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { kfetch } from 'ui/kfetch';
import { isEqual } from 'lodash';
import { RepositoryUri, WorkerReservedProgress } from '../../model';
import {
deleteRepoFinished,
@ -16,9 +19,15 @@ import {
pollRepoCloneStatusStop,
pollRepoDeleteStatusStop,
pollRepoIndexStatusStop,
FetchRepoFileStatus,
FetchRepoFileStatusSuccess,
FetchRepoFileStatusFailed,
FetchFilePayload,
} from '../actions';
import * as ROUTES from '../components/routes';
import { RootState } from '../reducers';
import { mainRoutePattern } from './patterns';
import { StatusReport } from '../../common/repo_file_status';
const matchSelector = (state: RootState) => state.route.match;
@ -66,3 +75,41 @@ function* handleRepoDeleteFinished(action: any) {
export function* watchRepoDeleteFinished() {
yield takeEvery(deleteCompletedPattern, handleRepoDeleteFinished);
}
function* handleMainRouteChange(action: Action<Match>) {
// in source view page, we need repos as default repo scope options when no query input
const { resource, org, repo, path, revision } = action.payload!.params;
const uri = `${resource}/${org}/${repo}`;
const newStatusPath: FetchFilePayload = { uri, revision, path };
const currentStatusPath = yield select((state: RootState) => state.status.currentStatusPath);
if (!isEqual(newStatusPath, currentStatusPath)) {
yield call(fetchStatus, newStatusPath);
}
}
function* fetchStatus(location: FetchFilePayload) {
yield put(FetchRepoFileStatus(location));
try {
const newStatus = yield call(requestStatus, location);
yield put(
FetchRepoFileStatusSuccess({
statusReport: newStatus as StatusReport,
path: location,
})
);
} catch (e) {
yield put(FetchRepoFileStatusFailed(e));
}
}
function requestStatus(location: FetchFilePayload) {
const { uri, revision, path } = location;
return kfetch({
pathname: `/api/code/repo/${uri}/status/${revision}/${path}`,
method: 'GET',
});
}
export function* watchStatusChange() {
yield takeLatest(mainRoutePattern, handleMainRouteChange);
}

View file

@ -34,6 +34,7 @@ import { ServerOptions } from './server_options';
import { ServerLoggerFactory } from './utils/server_logger_factory';
import { EsClientWithInternalRequest } from './utils/esclient_with_internal_request';
import { checkCodeNode, checkRoute } from './routes/check';
import { statusRoute } from './routes/status';
async function retryUntilAvailable<T>(
func: () => Promise<T>,
@ -260,6 +261,7 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L
installRoute(codeServerRouter, lspService);
lspRoute(codeServerRouter, lspService, serverOptions);
setupRoute(codeServerRouter);
statusRoute(codeServerRouter, gitOps, lspService);
server.events.on('stop', () => {
gitOps.cleanAllRepo();

View file

@ -187,8 +187,12 @@ export class LanguageServerController implements ILanguageServerHandler {
return status;
}
public supportLanguage(lang: string) {
return this.languageServerMap[lang] !== undefined;
public getLanguageServerDef(lang: string): LanguageServerDefinition | null {
const data = this.languageServerMap[lang];
if (data) {
return data.definition;
}
return null;
}
private async findOrCreateHandler(

View file

@ -79,7 +79,11 @@ export class LspService {
}
public supportLanguage(lang: string) {
return this.controller.supportLanguage(lang);
return this.controller.getLanguageServerDef(lang) !== null;
}
public getLanguageSeverDef(lang: string) {
return this.controller.getLanguageServerDef(lang);
}
public languageServerStatus(lang: string): LanguageServerStatus {
@ -88,6 +92,6 @@ export class LspService {
public async initializeState(repoUri: string, revision: string) {
const workspacePath = await this.workspaceHandler.revisionDir(repoUri, revision);
return this.controller.initializeState(workspacePath);
return await this.controller.initializeState(workspacePath);
}
}

View file

@ -41,15 +41,6 @@ export function lspRoute(
) {
const log = new Logger(server.server);
server.route({
path: '/api/code/repo/{uri*3}/{ref}/lspstate',
method: 'GET',
async handler(req) {
const { uri, ref } = req.params;
return lspService.initializeState(uri, ref);
},
});
server.route({
path: '/api/code/lsp/textDocument/{method}',
async handler(req, h: hapi.ResponseToolkit) {

View file

@ -0,0 +1,119 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import hapi from 'hapi';
import Boom from 'boom';
import { LspService } from '../lsp/lsp_service';
import { GitOperations } from '../git_operations';
import { CodeServerRouter } from '../security';
import { LangServerType, RepoFileStatus, StatusReport } from '../../common/repo_file_status';
import { CTAGS, LanguageServerDefinition } from '../lsp/language_servers';
import { LanguageServerStatus } from '../../common/language_server';
import { WorkspaceStatus } from '../lsp/request_expander';
import { RepositoryObjectClient } from '../search';
import { EsClientWithRequest } from '../utils/esclient_with_request';
import { TEXT_FILE_LIMIT } from '../../common/file';
import { detectLanguage } from '../utils/detect_language';
export function statusRoute(
server: CodeServerRouter,
gitOps: GitOperations,
lspService: LspService
) {
async function handleRepoStatus(
report: StatusReport,
repoUri: string,
revision: string,
repoObjectClient: RepositoryObjectClient
) {
const commit = await gitOps.getCommit(repoUri, decodeURIComponent(revision));
const head = await gitOps.getHeadRevision(repoUri);
if (head === commit.sha()) {
try {
const indexStatus = await repoObjectClient.getRepositoryLspIndexStatus(repoUri);
if (indexStatus.progress < 100) {
report.repoStatus = RepoFileStatus.INDEXING;
}
} catch (e) {
// index may not stated yet
report.repoStatus = RepoFileStatus.INDEXING;
}
} else {
report.repoStatus = RepoFileStatus.REVISION_NOT_INDEXED;
}
}
async function handleFileStatus(report: StatusReport, content: Buffer, path: string) {
if (content.length <= TEXT_FILE_LIMIT) {
const lang: string = await detectLanguage(path, content);
const def = lspService.getLanguageSeverDef(lang);
if (def === null) {
report.fileStatus = RepoFileStatus.FILE_NOT_SUPPORTED;
} else {
return def;
}
} else {
report.fileStatus = RepoFileStatus.FILE_IS_TOO_BIG;
}
}
async function handleLspStatus(
report: StatusReport,
def: LanguageServerDefinition,
repoUri: string,
revision: string
) {
report.langServerType = def === CTAGS ? LangServerType.GENERIC : LangServerType.DEDICATED;
if (lspService.languageServerStatus(def.languages[0]) === LanguageServerStatus.NOT_INSTALLED) {
report.langServerStatus = RepoFileStatus.LANG_SERVER_NOT_INSTALLED;
} else {
const state = await lspService.initializeState(repoUri, revision);
const initState = state[def.name];
if (initState !== WorkspaceStatus.Initialized) {
report.langServerStatus = RepoFileStatus.LANG_SERVER_IS_INITIALIZING;
}
}
}
server.route({
path: '/api/code/repo/{uri*3}/status/{ref}/{path*}',
method: 'GET',
async handler(req: hapi.Request) {
const { uri, path, ref } = req.params;
const report: StatusReport = {
langServerType: LangServerType.NONE,
};
const repoObjectClient = new RepositoryObjectClient(new EsClientWithRequest(req));
try {
// Check if the repository already exists
await repoObjectClient.getRepository(uri);
} catch (e) {
return Boom.notFound(`repo ${uri} not found`);
}
await handleRepoStatus(report, uri, ref, repoObjectClient);
if (path) {
try {
try {
const blob = await gitOps.fileContent(uri, path, decodeURIComponent(ref));
// text file
if (!blob.isBinary()) {
const def = await handleFileStatus(report, blob.content(), path);
if (def) {
await handleLspStatus(report, def, uri, ref);
}
}
} catch (e) {
// not a file? The path may be a dir.
}
} catch (e) {
return Boom.internal(e.message || e.name);
}
}
return report;
},
});
}

View file

@ -12,5 +12,6 @@ export default function apmApiIntegrationTests({
}: KibanaFunctionalTestDefaultProviders) {
describe('Code', () => {
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./repo_status'));
});
}

View file

@ -0,0 +1,93 @@
/*
* 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 expect from '@kbn/expect';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import {
RepoFileStatus,
StatusReport,
} from '../../../../legacy/plugins/code/common/repo_file_status';
// eslint-disable-next-line import/no-default-export
export default function repoStatusTests({ getService }: KibanaFunctionalTestDefaultProviders) {
const supertest = getService('supertest');
const retry = getService('retry');
const log = getService('log');
const CLONE_API = '/api/code/repo';
const DELETE_API = '/api/code/repo';
const REPO_STATUS_API = '/api/code/repo/status';
const TEST_REPO = 'github.com/elastic/TypeScript-Node-Starter';
const TEST_REPO_URL = `https://${TEST_REPO}.git`;
describe('repo status', () => {
after(async () => {
await supertest
.delete(`${DELETE_API}/${TEST_REPO}`)
.set('kbn-xsrf', 'xxx')
.expect(200);
});
async function getStatus(revision: string, path: string): Promise<StatusReport> {
const api = `/api/code/repo/${TEST_REPO}/status/${revision}/${path}`;
const { body } = await supertest.get(api).expect(200);
return body as StatusReport;
}
it('', async () => {
{
// send a clone request
const response = await supertest
.post(CLONE_API)
.set('kbn-xsrf', 'xxx')
.send({ url: TEST_REPO_URL })
.expect(200);
expect(response.body).to.eql({
uri: TEST_REPO,
url: TEST_REPO_URL,
name: 'TypeScript-Node-Starter',
org: 'elastic',
protocol: 'https',
});
}
log.info('cloning');
await retry.tryForTime(60000, async () => {
const { body } = await supertest.get(`${REPO_STATUS_API}/${TEST_REPO}`).expect(200);
log.info('clone progress:' + body.gitStatus.progress);
expect(body.gitStatus).to.have.property('progress', 100);
});
log.info('indexing');
let sawInitializing = false;
await retry.tryForTime(60000, async () => {
const { body } = await supertest.get(`${REPO_STATUS_API}/${TEST_REPO}`).expect(200);
expect(body.indexStatus).to.have.property('progress');
if (body.indexStatus.progress === 0 && !sawInitializing) {
const status = await getStatus('master', 'src/app.ts');
expect(status.langServerStatus).to.be(RepoFileStatus.LANG_SERVER_IS_INITIALIZING);
sawInitializing = true;
}
expect(sawInitializing).to.be(true);
// indexing
if (body.progress < 100) {
const status = getStatus('master', '');
expect(status).to.have.property('repoStatus', RepoFileStatus.INDEXING);
}
expect(body.indexStatus.progress).to.be(100);
log.info('index done');
const status = await getStatus('master', '');
expect(status.repoStatus).not.to.be(RepoFileStatus.INDEXING);
});
{
const status = await getStatus('46971a8454761f1a11d8fde4d96ff8d29bc4e754', '');
expect(status.repoStatus).to.be(RepoFileStatus.REVISION_NOT_INDEXED);
}
{
const status = await getStatus('master', 'README.md');
expect(status.fileStatus).to.be(RepoFileStatus.FILE_NOT_SUPPORTED);
}
});
});
}