Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV2-4440: Fix inconsistencies in VSCode self hosted status bar #1399

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/enterprise/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import BINARY_STATE from "../binary/binaryStateSingleton";
import { activeTextEditorState } from "../activeTextEditorState";
import { ChatAPI } from "../tabnineChatWidget/ChatApi";
import ChatViewProvider from "../tabnineChatWidget/ChatViewProvider";
import USER_INFO_STATE from "./lifecycle/UserInfoState";

export async function activate(
context: vscode.ExtensionContext
Expand All @@ -64,6 +65,7 @@ export async function activate(
context.subscriptions.push(await setEnterpriseContext());
context.subscriptions.push(new WorkspaceUpdater());
context.subscriptions.push(BINARY_STATE);
context.subscriptions.push(USER_INFO_STATE);
context.subscriptions.push(activeTextEditorState);
context.subscriptions.push(
commands.registerCommand(CONFIG_COMMAND, () => {
Expand Down
33 changes: 33 additions & 0 deletions src/enterprise/lifecycle/UserInfoState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Disposable } from "vscode";
import EventEmitterBasedState from "../../state/EventEmitterBasedState";
import getUserInfo, { UserInfo } from "../requests/UserInfo";
import { useDerviedState } from "../../state/deriveState";
import BINARY_STATE from "../../binary/binaryStateSingleton";

class UserInfoState extends EventEmitterBasedState<UserInfo> {
toDispose: Disposable;

constructor() {
super();

this.toDispose = useDerviedState(
BINARY_STATE,
(s) => s.is_logged_in,
() => {
void this.asyncSet(fetchUserState);
}
);
}

dispose(): void {
super.dispose();
this.toDispose.dispose();
}
}

async function fetchUserState(): Promise<UserInfo | null> {
return (await getUserInfo()) ?? null;
}

const USER_INFO_STATE = new UserInfoState();
export default USER_INFO_STATE;
159 changes: 42 additions & 117 deletions src/enterprise/statusBar/StatusBar.ts
Original file line number Diff line number Diff line change
@@ -1,148 +1,73 @@
import { Disposable, ExtensionContext, authentication, window } from "vscode";
import { Disposable, ExtensionContext, window } from "vscode";
import { StatusItem } from "./StatusItem";
import { StatusState, showLoginNotification } from "./statusAction";
import { isHealthyServer } from "../update/isHealthyServer";
import { rejectOnTimeout } from "../../utils/utils";
import { getState, tabNineProcess } from "../../binary/requests/requests";
import {
BINARY_NOTIFICATION_POLLING_INTERVAL,
BRAND_NAME,
CONGRATS_MESSAGE_SHOWN_KEY,
} from "../../globals/consts";
import getUserInfo, { UserInfo } from "../requests/UserInfo";
import { Logger } from "../../utils/logger";
import { completionsState } from "../../state/completionsState";
import { showPleaseLoginNotification } from "./statusAction";
import { CONGRATS_MESSAGE_SHOWN_KEY } from "../../globals/consts";
import StatusBarState from "./StatusBarState";
import { useDerviedState } from "../../state/deriveState";
import USER_INFO_STATE from "../lifecycle/UserInfoState";
import { StatusBarStateData } from "./calculateStatusBarState";

export class StatusBar implements Disposable {
private item: StatusItem;

private statusPollingInterval: NodeJS.Timeout | undefined = undefined;

private disposables: Disposable[] = [];

private context: ExtensionContext;

private statusBarState = new StatusBarState();

constructor(context: ExtensionContext) {
context.subscriptions.push(this);
this.context = context;
this.item = new StatusItem();
void authentication.getSession(BRAND_NAME, []);

this.disposables.push(
authentication.onDidChangeSessions((e) => {
if (e.provider.id === BRAND_NAME) {
void this.enforceLogin();
this.statusBarState,
this.statusBarState.onChange((statusBarData) => {
this.updateStatusBar(statusBarData);
}),
useDerviedState(
USER_INFO_STATE,
(s) => s.isLoggedIn,
(isLoggedIn) => {
if (!isLoggedIn) {
showPleaseLoginNotification();
}
}
})
)
);
this.setDefaultStatus();

// eslint-disable-next-line @typescript-eslint/unbound-method
this.setServerRequired().catch(Logger.error);

completionsState.on("changed", () => this.setDefaultStatus());
}

private async setServerRequired() {
Logger.debug("Checking if server url is set and healthy.");
if (await isHealthyServer()) {
Logger.debug("Server is healthy");
this.setDefaultStatus();
} else {
Logger.warn("Server url isn't set or not responding to GET /health");
this.item.setWarning("Please set your Tabnine server URL");
this.item.setCommand(StatusState.SetServer);
private updateStatusBar(statusBarData: StatusBarStateData) {
switch (statusBarData.type) {
case "default":
this.item.setDefault();
void this.showFirstSuceessNotification();
break;
case "loading":
this.item.setLoading();
break;
case "error":
this.item.setError(statusBarData.message);
break;
case "warning":
this.item.setWarning(statusBarData.message);
break;
default:
}
}

public waitForProcess() {
Logger.debug("Waiting for Tabnine process to become ready.");
this.item.setLoading();
this.item.setCommand(StatusState.WaitingForProcess);

rejectOnTimeout(tabNineProcess.onReady, 10_000).then(
() => this.enforceLogin(),
() => this.setProcessTimedoutError()
);
}

private setProcessTimedoutError() {
Logger.error("Timedout waiting for Tabnine process to become ready.");
this.item.setError("Tabnine failed to start, view logs for more details");
this.item.setCommand(StatusState.ErrorWaitingForProcess);
}

private setGenericError(error: Error) {
Logger.error(error);
this.item.setError("Something went wrong");
this.item.setCommand(StatusState.OpenLogs);
}

private setDefaultStatus() {
if (!completionsState.value) {
this.item.setCompletionsDisabled();
} else {
this.item.setDefault();
}
this.item.setCommand(StatusState.Ready);
}

private async enforceLogin() {
const userInfo = await getUserInfo();
if (userInfo?.isLoggedIn) {
Logger.debug("The user is logged in.");
this.checkTeamMembership(userInfo);
} else {
Logger.info(
"The user isn't logged in, set status bar and showing notification"
);
this.item.setWarning("Please sign in to access Tabnine");
this.item.setCommand(StatusState.LogIn);
showLoginNotification();
if (statusBarData.command) {
this.item.setCommand(statusBarData.command);
}
}

private checkTeamMembership(userInfo: UserInfo | null | undefined) {
this.setDefaultStatus();
try {
if (!userInfo?.team) {
Logger.warn("User isn't part of a team");
this.item.setWarning("You are not part of a team");
this.item.setCommand(StatusState.NotPartOfTheTeam);
} else {
Logger.debug("Everything seems to be fine, we are ready!");
this.setReady();
}
} catch (error) {
this.setGenericError(error as Error);
}
}

private setReady() {
void this.showFirstSuceessNotification();
this.setDefaultStatus();
this.statusPollingInterval = setInterval(() => {
void getState().then(
(state) => {
if (state?.cloud_connection_health_status !== "Ok") {
this.item.setWarning(
"Connectivity issue - Tabnine is unable to reach the server"
);
this.item.setCommand(StatusState.ConnectivityIssue);
} else {
this.setDefaultStatus();
}
},
(error) => this.setGenericError(error as Error)
);
}, BINARY_NOTIFICATION_POLLING_INTERVAL);
waitForProcess() {
this.statusBarState.startWaitingForProcess();
}

public dispose() {
this.item.dispose();
Disposable.from(...this.disposables).dispose();
if (this.statusPollingInterval) {
clearInterval(this.statusPollingInterval);
}
}

private async showFirstSuceessNotification() {
Expand Down
135 changes: 135 additions & 0 deletions src/enterprise/statusBar/StatusBarState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Disposable } from "vscode";
import EventEmitterBasedNonNullState from "../../state/EventEmitterBasedNonNullState";
import { tabNineProcess } from "../../binary/requests/requests";
import {
PromiseStateData,
convertPromiseToState,
triggeredPromiseState,
useDerviedState,
} from "../../state/deriveState";
import BINARY_STATE from "../../binary/binaryStateSingleton";
import { rejectOnTimeout } from "../../utils/utils";
import { completionsState } from "../../state/completionsState";
import { isHealthyServer } from "../update/isHealthyServer";
import { UserInfo } from "../requests/UserInfo";
import USER_INFO_STATE from "../lifecycle/UserInfoState";
import calculateStatusBarState, {
INITIAL_STATE,
StatusBarStateData,
} from "./calculateStatusBarState";

export default class StatusBarState extends EventEmitterBasedNonNullState<StatusBarStateData> {
private toDispose: Disposable;

private processStartedState = triggeredPromiseState(() =>
rejectOnTimeout(tabNineProcess.onReady, 10_000)
);

constructor() {
super(INITIAL_STATE);

const serverHealthOnPluginStartState = convertPromiseToState(
isHealthyServer()
);

const startedProcessDisposable = this.processStartedState.onChange(
(startedState) => {
this.updateState(
BINARY_STATE.get()?.cloud_connection_health_status,
startedState,
completionsState.value,
serverHealthOnPluginStartState.get(),
USER_INFO_STATE.get()
);
}
);
const serverHealthDisposable = serverHealthOnPluginStartState.onChange(
(isHealthy) => {
this.updateState(
BINARY_STATE.get()?.cloud_connection_health_status,
this.processStartedState.get(),
completionsState.value,
isHealthy,
USER_INFO_STATE.get()
);
}
);

this.updateState(
BINARY_STATE.get()?.cloud_connection_health_status,
this.processStartedState.get(),
completionsState.value,
serverHealthOnPluginStartState.get(),
USER_INFO_STATE.get()
);

const stateDisposable = useDerviedState(
BINARY_STATE,
(s) => s.cloud_connection_health_status,
(cloudConnection) => {
this.updateState(
cloudConnection,
this.processStartedState.get(),
completionsState.value,
serverHealthOnPluginStartState.get(),
USER_INFO_STATE.get()
);
}
);

const userInfoStateDisposable = USER_INFO_STATE.onChange((userInfo) => {
this.updateState(
BINARY_STATE.get()?.cloud_connection_health_status,
this.processStartedState.get(),
completionsState.value,
serverHealthOnPluginStartState.get(),
userInfo
);
});

completionsState.on("changed", () => {
this.updateState(
BINARY_STATE.get()?.cloud_connection_health_status,
this.processStartedState.get(),
completionsState.value,
serverHealthOnPluginStartState.get(),
USER_INFO_STATE.get()
);
});

this.toDispose = Disposable.from(
userInfoStateDisposable,
stateDisposable,
startedProcessDisposable,
serverHealthDisposable,
this.processStartedState
);
}

private updateState(
cloudConnection: "Ok" | string | undefined | null,
processStartedState: PromiseStateData<unknown>,
isCompletionsEnabled: boolean,
serverHealthOnPluginStart: PromiseStateData<boolean>,
userInfo: UserInfo | null
) {
this.set(
calculateStatusBarState(
cloudConnection,
processStartedState,
isCompletionsEnabled,
serverHealthOnPluginStart,
userInfo
)
);
}

startWaitingForProcess() {
this.processStartedState.trigger();
}

dispose(): void {
super.dispose();
this.toDispose.dispose();
}
}
Loading