From 8e2b5d8a1dc3fd8bc81aed62259739caa3218133 Mon Sep 17 00:00:00 2001 From: Ofek Ben-Yaish Date: Tue, 5 Dec 2023 20:38:32 +0200 Subject: [PATCH] TEMP COMMIT UNCHECKED --- src/enterprise/extension.ts | 2 + src/enterprise/lifecycle/UserInfoState.ts | 28 +++ src/enterprise/statusBar/StatusBar.ts | 159 +++++------------- src/enterprise/statusBar/StatusBarState.ts | 135 +++++++++++++++ src/enterprise/statusBar/StatusItem.ts | 20 +-- .../statusBar/calculateStatusBarState.ts | 93 ++++++++++ .../SelfHostedChatEnabledState.ts | 45 ++--- src/state/deriveState.ts | 63 ++++++- 8 files changed, 381 insertions(+), 164 deletions(-) create mode 100644 src/enterprise/lifecycle/UserInfoState.ts create mode 100644 src/enterprise/statusBar/StatusBarState.ts create mode 100644 src/enterprise/statusBar/calculateStatusBarState.ts diff --git a/src/enterprise/extension.ts b/src/enterprise/extension.ts index 68cd9820f5..e2b78238af 100644 --- a/src/enterprise/extension.ts +++ b/src/enterprise/extension.ts @@ -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 @@ -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, () => { diff --git a/src/enterprise/lifecycle/UserInfoState.ts b/src/enterprise/lifecycle/UserInfoState.ts new file mode 100644 index 0000000000..017caf840a --- /dev/null +++ b/src/enterprise/lifecycle/UserInfoState.ts @@ -0,0 +1,28 @@ +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 { + toDispose: Disposable; + + constructor() { + super(); + + this.toDispose = useDerviedState( + BINARY_STATE, + (s) => s.is_logged_in, + () => { + void this.asyncSet(fetchUserState); + } + ); + } +} + +async function fetchUserState(): Promise { + return (await getUserInfo()) ?? null; +} + +const USER_INFO_STATE = new UserInfoState(); +export default USER_INFO_STATE; diff --git a/src/enterprise/statusBar/StatusBar.ts b/src/enterprise/statusBar/StatusBar.ts index ff3b68714f..373bf6c45e 100644 --- a/src/enterprise/statusBar/StatusBar.ts +++ b/src/enterprise/statusBar/StatusBar.ts @@ -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 { showLoginNotification } 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) { + showLoginNotification(); + } } - }) + ) ); - 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() { diff --git a/src/enterprise/statusBar/StatusBarState.ts b/src/enterprise/statusBar/StatusBarState.ts new file mode 100644 index 0000000000..d748dcb33f --- /dev/null +++ b/src/enterprise/statusBar/StatusBarState.ts @@ -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 { + 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, + isCompletionsEnabled: boolean, + serverHealthOnPluginStart: PromiseStateData, + userInfo: UserInfo | null + ) { + this.set( + calculateStatusBarState( + cloudConnection, + processStartedState, + isCompletionsEnabled, + serverHealthOnPluginStart, + userInfo + ) + ); + } + + startWaitingForProcess() { + this.processStartedState.trigger(); + } + + dispose(): void { + super.dispose(); + this.toDispose.dispose(); + } +} diff --git a/src/enterprise/statusBar/StatusItem.ts b/src/enterprise/statusBar/StatusItem.ts index e70b0d6a6f..93eb0304b2 100644 --- a/src/enterprise/statusBar/StatusItem.ts +++ b/src/enterprise/statusBar/StatusItem.ts @@ -40,24 +40,24 @@ export class StatusItem implements Disposable { this.item.tooltip = "Starting tabnine process, please wait..."; } - public setError(message: string) { + public setError(message: string | undefined) { this.item.text = `$(warning) ${STATUS_NAME}`; this.item.backgroundColor = new ThemeColor("statusBarItem.errorBackground"); - this.item.tooltip = message; - } - public setWarning(message: string) { - this.item.backgroundColor = new ThemeColor( - "statusBarItem.warningBackground" - ); - this.item.tooltip = message; - this.item.text = STATUS_NAME; + if (message) { + this.item.tooltip = message; + } } - public setCompletionsDisabled() { + public setWarning(message: string | undefined) { this.item.backgroundColor = new ThemeColor( "statusBarItem.warningBackground" ); + + if (message) { + this.item.tooltip = message; + } + this.item.text = STATUS_NAME; } diff --git a/src/enterprise/statusBar/calculateStatusBarState.ts b/src/enterprise/statusBar/calculateStatusBarState.ts new file mode 100644 index 0000000000..53280a5fdf --- /dev/null +++ b/src/enterprise/statusBar/calculateStatusBarState.ts @@ -0,0 +1,93 @@ +import { PromiseStateData } from "../../state/deriveState"; +import { Logger } from "../../utils/logger"; +import { UserInfo } from "../requests/UserInfo"; +import { StatusState } from "./statusAction"; + +export type StatusBarStateData = { + type: "loading" | "error" | "warning" | "default"; + command?: StatusState; + message?: string; +}; + +export const INITIAL_STATE: StatusBarStateData = { + type: "loading", + command: StatusState.WaitingForProcess, +}; + +export default function calculateStatusBarState( + cloudConnection: "Ok" | string | undefined | null, + processStartedState: PromiseStateData, + isCompletionsEnabled: boolean, + serverHealthOnPluginStart: PromiseStateData, + userInfo: UserInfo | null +): StatusBarStateData { + if (!processStartedState.resolved) { + return INITIAL_STATE; + } + + if (processStartedState.isError) { + Logger.error("Timedout waiting for Tabnine process to become ready."); + + return { + type: "error", + message: "Tabnine failed to start, view logs for more details", + command: StatusState.ErrorWaitingForProcess, + }; + } + + if (!serverHealthOnPluginStart.resolved) { + return { + type: "loading", + }; + } + + if (serverHealthOnPluginStart.isError || !serverHealthOnPluginStart.data) { + return { + type: "error", + message: "Please set your Tabnine server URL", + command: StatusState.SetServer, + }; + } + + if (cloudConnection !== "Ok") { + return { + type: "warning", + message: "Connectivity issue - Tabnine is unable to reach the server", + command: StatusState.ConnectivityIssue, + }; + } + + if (!isCompletionsEnabled) { + return { + type: "warning", + command: StatusState.Ready, + }; + } + + if (!userInfo) { + return { + type: "loading", + }; + } + + if (!userInfo.isLoggedIn) { + return { + type: "warning", + message: "Please sign in to access Tabnine", + command: StatusState.LogIn, + }; + } + + if (!userInfo.team) { + return { + type: "warning", + message: "You are not part of a team", + command: StatusState.NotPartOfTheTeam, + }; + } + + return { + type: "default", + command: StatusState.Ready, + }; +} diff --git a/src/enterprise/tabnineChatWidget/SelfHostedChatEnabledState.ts b/src/enterprise/tabnineChatWidget/SelfHostedChatEnabledState.ts index 7784191a05..b8bec03814 100644 --- a/src/enterprise/tabnineChatWidget/SelfHostedChatEnabledState.ts +++ b/src/enterprise/tabnineChatWidget/SelfHostedChatEnabledState.ts @@ -4,9 +4,8 @@ import ChatEnabledState, { ChatStates, } from "../../tabnineChatWidget/ChatEnabledState"; import EventEmitterBasedNonNullState from "../../state/EventEmitterBasedNonNullState"; -import getUserInfo from "../requests/UserInfo"; -import { useDerviedState } from "../../state/deriveState"; -import BINARY_STATE from "../../binary/binaryStateSingleton"; +import USER_INFO_STATE from "../lifecycle/UserInfoState"; +import { UserInfo } from "../requests/UserInfo"; export default class SelfHostedChatEnabledState extends EventEmitterBasedNonNullState @@ -15,37 +14,21 @@ export default class SelfHostedChatEnabledState super(ChatStates.loading); context.subscriptions.push( - useDerviedState( - BINARY_STATE, - (state) => state.is_logged_in, - () => { - void this.updateState(); - } - ) + USER_INFO_STATE.onChange((userInfo) => { + this.updateState(userInfo); + }) ); } - async updateState() { - await this.asyncSet(fetchChatState); - } -} - -async function fetchChatState(): Promise { - const userInfo = await getUserInfo(); + updateState(userInfo: UserInfo) { + const isEnabled = userInfo.team !== null; - if (!userInfo) { - return null; + if (isEnabled) { + this.set(ChatStates.enabled); + } else if (userInfo.isLoggedIn) { + this.set(ChatStates.disabled("part_of_a_team_required")); + } else { + this.set(ChatStates.disabled("authnetication_required")); + } } - - const isEnabled = userInfo.team !== null; - - if (isEnabled) { - return ChatStates.enabled; - } - - if (userInfo.isLoggedIn) { - return ChatStates.disabled("part_of_a_team_required"); - } - - return ChatStates.disabled("authnetication_required"); } diff --git a/src/state/deriveState.ts b/src/state/deriveState.ts index f3231f3822..7f4a78c54d 100644 --- a/src/state/deriveState.ts +++ b/src/state/deriveState.ts @@ -3,14 +3,10 @@ import { Disposable } from "vscode"; import EventEmitterBasedState from "./EventEmitterBasedState"; import EventEmitterBasedNonNullState from "./EventEmitterBasedNonNullState"; -export type DerivedState = Disposable & EventEmitterBasedState; -export type DerivedNonNullState = Disposable & - EventEmitterBasedNonNullState; - function deriveState( state: EventEmitterBasedState, mapping: (value: I) => O -): DerivedState { +): EventEmitterBasedState { return new (class extends EventEmitterBasedState implements Disposable { useStateDisposabled!: Disposable; @@ -33,7 +29,7 @@ export function deriveNonNullState( state: EventEmitterBasedState, mapping: (value: I, self: O) => O, initailValue: O -): DerivedNonNullState { +): EventEmitterBasedNonNullState { return new (class extends EventEmitterBasedNonNullState implements Disposable { @@ -69,3 +65,58 @@ export function useDerviedState( }, }; } + +export type PromiseStateData = + | { resolved: false; isError: false } + | { resolved: true; isError: false; data: T } + | { resolved: true; isError: true; error: unknown }; + +export function convertPromiseToState( + promise: Promise +): EventEmitterBasedNonNullState> { + return new (class extends EventEmitterBasedNonNullState> { + constructor() { + super({ resolved: false, isError: false }); + + promise + .then((data) => { + this.set({ resolved: true, isError: false, data }); + }) + .catch((error) => { + this.set({ resolved: true, isError: true, error: error as unknown }); + }); + } + })(); +} + +export function triggeredPromiseState( + toTrigger: () => Promise +): EventEmitterBasedNonNullState> & { + trigger: () => void; +} { + return new (class extends EventEmitterBasedNonNullState> { + toTrigger = toTrigger; + + toDispose: Disposable | null = null; + + constructor() { + super({ resolved: false, isError: false }); + } + + trigger() { + const state = convertPromiseToState(this.toTrigger()); + + this.toDispose = Disposable.from( + state, + state.onChange((s) => { + this.set(s); + }) + ); + } + + dispose(): void { + super.dispose(); + this.toDispose?.dispose(); + } + })(); +}