[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:
parent
ad77e901b2
commit
12ba1f1894
67
x-pack/legacy/plugins/code/common/repo_file_status.ts
Normal file
67
x-pack/legacy/plugins/code/common/repo_file_status.ts
Normal 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;
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ import {
|
||||||
Repository,
|
Repository,
|
||||||
CloneProgress,
|
CloneProgress,
|
||||||
} from '../../model';
|
} from '../../model';
|
||||||
|
import { FetchFilePayload } from './file';
|
||||||
|
import { StatusReport } from '../../common/repo_file_status';
|
||||||
|
|
||||||
export enum RepoState {
|
export enum RepoState {
|
||||||
CLONING,
|
CLONING,
|
||||||
|
@ -60,3 +62,10 @@ export const pollRepoDeleteStatusStart = createAction<string>('POLL DELETE STATU
|
||||||
export const pollRepoCloneStatusStop = createAction<RepositoryUri>('POLL CLONE STATUS STOP');
|
export const pollRepoCloneStatusStop = createAction<RepositoryUri>('POLL CLONE STATUS STOP');
|
||||||
export const pollRepoIndexStatusStop = createAction<RepositoryUri>('POLL INDEX STATUS STOP');
|
export const pollRepoIndexStatusStop = createAction<RepositoryUri>('POLL INDEX STATUS STOP');
|
||||||
export const pollRepoDeleteStatusStop = createAction<RepositoryUri>('POLL DELETE 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');
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { encodeRevisionString, decodeRevisionString } from '../../../common/uri_
|
||||||
import { history } from '../../utils/url';
|
import { history } from '../../utils/url';
|
||||||
import { Breadcrumb } from './breadcrumb';
|
import { Breadcrumb } from './breadcrumb';
|
||||||
import { SearchBar } from '../search_bar';
|
import { SearchBar } from '../search_bar';
|
||||||
|
import { StatusIndicator } from '../status_indicator/status_indicator';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
routeParams: MainRouteParams;
|
routeParams: MainRouteParams;
|
||||||
|
@ -83,7 +84,7 @@ export class TopBar extends React.Component<Props, { value: string }> {
|
||||||
className="codeTopBar__toolbar"
|
className="codeTopBar__toolbar"
|
||||||
>
|
>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiFlexGroup gutterSize="none">
|
<EuiFlexGroup gutterSize="l" alignItems="center">
|
||||||
<EuiFlexItem className="codeContainer__select" grow={false}>
|
<EuiFlexItem className="codeContainer__select" grow={false}>
|
||||||
<EuiSelect
|
<EuiSelect
|
||||||
options={this.branchOptions}
|
options={this.branchOptions}
|
||||||
|
@ -93,6 +94,9 @@ export class TopBar extends React.Component<Props, { value: string }> {
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<Breadcrumb routeParams={this.props.routeParams} />
|
<Breadcrumb routeParams={this.props.routeParams} />
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<StatusIndicator />
|
||||||
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>{this.props.buttons}</EuiFlexItem>
|
<EuiFlexItem grow={false}>{this.props.buttons}</EuiFlexItem>
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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);
|
|
@ -10,6 +10,8 @@ import { Action, handleActions } from 'redux-actions';
|
||||||
import { RepositoryUri, WorkerReservedProgress } from '../../model';
|
import { RepositoryUri, WorkerReservedProgress } from '../../model';
|
||||||
import {
|
import {
|
||||||
deleteRepoFinished,
|
deleteRepoFinished,
|
||||||
|
FetchFilePayload,
|
||||||
|
FetchRepoFileStatusSuccess,
|
||||||
loadStatus,
|
loadStatus,
|
||||||
loadStatusFailed,
|
loadStatusFailed,
|
||||||
loadStatusSuccess,
|
loadStatusSuccess,
|
||||||
|
@ -20,11 +22,14 @@ import {
|
||||||
RepoStatus,
|
RepoStatus,
|
||||||
RepoState,
|
RepoState,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
|
import { StatusReport } from '../../common/repo_file_status';
|
||||||
|
|
||||||
export interface StatusState {
|
export interface StatusState {
|
||||||
status: { [key: string]: RepoStatus };
|
status: { [key: string]: RepoStatus };
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
|
currentStatusPath?: FetchFilePayload;
|
||||||
|
repoFileStatus?: StatusReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: StatusState = {
|
const initialState: StatusState = {
|
||||||
|
@ -170,6 +175,12 @@ export const status = handleActions<StatusState, StatusPayload>(
|
||||||
produce<StatusState>(state, draft => {
|
produce<StatusState>(state, draft => {
|
||||||
delete draft.status[action.payload!];
|
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
|
initialState
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,7 +47,7 @@ import {
|
||||||
watchSearchRouteChange,
|
watchSearchRouteChange,
|
||||||
} from './search';
|
} from './search';
|
||||||
import { watchRootRoute } from './setup';
|
import { watchRootRoute } from './setup';
|
||||||
import { watchRepoCloneSuccess, watchRepoDeleteFinished } from './status';
|
import { watchRepoCloneSuccess, watchRepoDeleteFinished, watchStatusChange } from './status';
|
||||||
import { watchLoadStructure } from './structure';
|
import { watchLoadStructure } from './structure';
|
||||||
|
|
||||||
export function* rootSaga() {
|
export function* rootSaga() {
|
||||||
|
@ -82,6 +82,7 @@ export function* rootSaga() {
|
||||||
yield fork(watchInstallLanguageServer);
|
yield fork(watchInstallLanguageServer);
|
||||||
yield fork(watchLoadRepoListStatus);
|
yield fork(watchLoadRepoListStatus);
|
||||||
yield fork(watchLoadRepoStatus);
|
yield fork(watchLoadRepoStatus);
|
||||||
|
yield fork(watchStatusChange);
|
||||||
|
|
||||||
// Repository status polling sagas begin
|
// Repository status polling sagas begin
|
||||||
yield fork(watchPollingRepoStatus);
|
yield fork(watchPollingRepoStatus);
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Action } from 'redux-actions';
|
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 { RepositoryUri, WorkerReservedProgress } from '../../model';
|
||||||
import {
|
import {
|
||||||
deleteRepoFinished,
|
deleteRepoFinished,
|
||||||
|
@ -16,9 +19,15 @@ import {
|
||||||
pollRepoCloneStatusStop,
|
pollRepoCloneStatusStop,
|
||||||
pollRepoDeleteStatusStop,
|
pollRepoDeleteStatusStop,
|
||||||
pollRepoIndexStatusStop,
|
pollRepoIndexStatusStop,
|
||||||
|
FetchRepoFileStatus,
|
||||||
|
FetchRepoFileStatusSuccess,
|
||||||
|
FetchRepoFileStatusFailed,
|
||||||
|
FetchFilePayload,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
import * as ROUTES from '../components/routes';
|
import * as ROUTES from '../components/routes';
|
||||||
import { RootState } from '../reducers';
|
import { RootState } from '../reducers';
|
||||||
|
import { mainRoutePattern } from './patterns';
|
||||||
|
import { StatusReport } from '../../common/repo_file_status';
|
||||||
|
|
||||||
const matchSelector = (state: RootState) => state.route.match;
|
const matchSelector = (state: RootState) => state.route.match;
|
||||||
|
|
||||||
|
@ -66,3 +75,41 @@ function* handleRepoDeleteFinished(action: any) {
|
||||||
export function* watchRepoDeleteFinished() {
|
export function* watchRepoDeleteFinished() {
|
||||||
yield takeEvery(deleteCompletedPattern, handleRepoDeleteFinished);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { ServerOptions } from './server_options';
|
||||||
import { ServerLoggerFactory } from './utils/server_logger_factory';
|
import { ServerLoggerFactory } from './utils/server_logger_factory';
|
||||||
import { EsClientWithInternalRequest } from './utils/esclient_with_internal_request';
|
import { EsClientWithInternalRequest } from './utils/esclient_with_internal_request';
|
||||||
import { checkCodeNode, checkRoute } from './routes/check';
|
import { checkCodeNode, checkRoute } from './routes/check';
|
||||||
|
import { statusRoute } from './routes/status';
|
||||||
|
|
||||||
async function retryUntilAvailable<T>(
|
async function retryUntilAvailable<T>(
|
||||||
func: () => Promise<T>,
|
func: () => Promise<T>,
|
||||||
|
@ -260,6 +261,7 @@ async function initCodeNode(server: Server, serverOptions: ServerOptions, log: L
|
||||||
installRoute(codeServerRouter, lspService);
|
installRoute(codeServerRouter, lspService);
|
||||||
lspRoute(codeServerRouter, lspService, serverOptions);
|
lspRoute(codeServerRouter, lspService, serverOptions);
|
||||||
setupRoute(codeServerRouter);
|
setupRoute(codeServerRouter);
|
||||||
|
statusRoute(codeServerRouter, gitOps, lspService);
|
||||||
|
|
||||||
server.events.on('stop', () => {
|
server.events.on('stop', () => {
|
||||||
gitOps.cleanAllRepo();
|
gitOps.cleanAllRepo();
|
||||||
|
|
|
@ -187,8 +187,12 @@ export class LanguageServerController implements ILanguageServerHandler {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public supportLanguage(lang: string) {
|
public getLanguageServerDef(lang: string): LanguageServerDefinition | null {
|
||||||
return this.languageServerMap[lang] !== undefined;
|
const data = this.languageServerMap[lang];
|
||||||
|
if (data) {
|
||||||
|
return data.definition;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findOrCreateHandler(
|
private async findOrCreateHandler(
|
||||||
|
|
|
@ -79,7 +79,11 @@ export class LspService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public supportLanguage(lang: string) {
|
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 {
|
public languageServerStatus(lang: string): LanguageServerStatus {
|
||||||
|
@ -88,6 +92,6 @@ export class LspService {
|
||||||
|
|
||||||
public async initializeState(repoUri: string, revision: string) {
|
public async initializeState(repoUri: string, revision: string) {
|
||||||
const workspacePath = await this.workspaceHandler.revisionDir(repoUri, revision);
|
const workspacePath = await this.workspaceHandler.revisionDir(repoUri, revision);
|
||||||
return this.controller.initializeState(workspacePath);
|
return await this.controller.initializeState(workspacePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,15 +41,6 @@ export function lspRoute(
|
||||||
) {
|
) {
|
||||||
const log = new Logger(server.server);
|
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({
|
server.route({
|
||||||
path: '/api/code/lsp/textDocument/{method}',
|
path: '/api/code/lsp/textDocument/{method}',
|
||||||
async handler(req, h: hapi.ResponseToolkit) {
|
async handler(req, h: hapi.ResponseToolkit) {
|
||||||
|
|
119
x-pack/legacy/plugins/code/server/routes/status.ts
Normal file
119
x-pack/legacy/plugins/code/server/routes/status.ts
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 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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -12,5 +12,6 @@ export default function apmApiIntegrationTests({
|
||||||
}: KibanaFunctionalTestDefaultProviders) {
|
}: KibanaFunctionalTestDefaultProviders) {
|
||||||
describe('Code', () => {
|
describe('Code', () => {
|
||||||
loadTestFile(require.resolve('./feature_controls'));
|
loadTestFile(require.resolve('./feature_controls'));
|
||||||
|
loadTestFile(require.resolve('./repo_status'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
93
x-pack/test/api_integration/apis/code/repo_status.ts
Normal file
93
x-pack/test/api_integration/apis/code/repo_status.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue