Skip to content

Commit

Permalink
Unify input capture with back buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne committed Aug 18, 2023
1 parent 9559f65 commit 3c095b1
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 240 deletions.
59 changes: 59 additions & 0 deletions src/platform/common/utils/inputValueProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { CancellationError, QuickInputButtons, window } from 'vscode';
import { CancellationTokenSource, Disposable } from 'vscode-jsonrpc';
import { raceCancellationError } from '../cancellation';
import { disposeAllDisposables } from '../helpers';
import { Disposables } from '../utils';
import { createDeferred } from './async';

class BackError extends Error {}
export class WorkflowInputValueProvider extends Disposables {
private readonly token = this._register(new CancellationTokenSource());
public async getValue(options: {
title: string;
value?: string;
ignoreFocusOut?: boolean;
prompt?: string;
validationMessage?: string;
password?: boolean;
}): Promise<{ value: string; navigation?: undefined } | { value?: undefined; navigation: 'cancel' | 'back' }> {
console.error('Why was this called');
const disposables: Disposable[] = [];
try {
const input = window.createInputBox();
disposables.push(input);
input.ignoreFocusOut = true;
input.title = options.title;
input.ignoreFocusOut = options.ignoreFocusOut === true;
input.prompt = options.prompt || '';
input.value = options.value || '';
input.password = options.password === true;
input.validationMessage = options.validationMessage || '';
input.buttons = [QuickInputButtons.Back];
input.show();
const deferred = createDeferred<string>();
disposables.push(input.onDidHide(() => deferred.reject(new CancellationError())));
input.onDidTriggerButton(
(e) => {
if (e === QuickInputButtons.Back) {
deferred.reject(new BackError());
}
},
this,
disposables
);
input.onDidAccept(() => deferred.resolve(input.value.trim() || options.value), this, disposables);
const value = await raceCancellationError(this.token.token, deferred.promise);
return { value };
} catch (ex) {
if (ex instanceof BackError) {
return { navigation: 'back' };
}
return { navigation: 'cancel' };
} finally {
disposeAllDisposables(disposables);
}
}
}
75 changes: 33 additions & 42 deletions src/standalone/userJupyterHubServer/jupyterHubPasswordConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from '../../platform/common/types';
import { DataScience } from '../../platform/common/utils/localize';
import { noop } from '../../platform/common/utils/misc';
import { IMultiStepInputFactory, IMultiStepInput } from '../../platform/common/utils/multiStepInput';
import { traceWarning } from '../../platform/logging';
import { sendTelemetryEvent, Telemetry } from '../../telemetry';
import {
Expand All @@ -21,6 +20,7 @@ import {
IJupyterServerUriStorage
} from '../../kernels/jupyter/types';
import { disposeAllDisposables } from '../../platform/common/helpers';
import { WorkflowInputValueProvider } from '../../platform/common/utils/inputValueProvider';

export interface IJupyterPasswordConnectInfo {
requiresPassword: boolean;
Expand All @@ -37,7 +37,6 @@ export class JupyterHubPasswordConnect {
constructor(
private appShell: IApplicationShell,

private readonly multiStepFactory: IMultiStepInputFactory,
private readonly asyncDisposableRegistry: IAsyncDisposableRegistry,
private readonly configService: IConfigurationService,
private readonly agentCreator: IJupyterRequestAgentCreator | undefined,
Expand Down Expand Up @@ -305,47 +304,39 @@ export class JupyterHubPasswordConnect {
}

private async getUserNameAndPassword(validationMessage?: string): Promise<{ username: string; password: string }> {
const multistep = this.multiStepFactory.create<{
username: string;
password: string;
validationMessage?: string;
}>();
const state = { username: '', password: '', validationMessage };
await multistep.run(this.getUserNameMultiStep.bind(this), state);
return state;
}

private async getUserNameMultiStep(
input: IMultiStepInput<{ username: string; password: string; validationErrorMessage?: string }>,
state: { username: string; password: string; validationMessage?: string }
) {
state.username = await input.showInputBox({
title: DataScience.jupyterSelectUserAndPasswordTitle,
prompt: DataScience.jupyterSelectUserPrompt,
validate: this.validateUserNameOrPassword,
validationMessage: state.validationMessage,
value: ''
});
if (state.username) {
return this.getPasswordMultiStep.bind(this);
let username = '';
let password = '';
const inputValueCapture = new WorkflowInputValueProvider();
this.disposables.push(inputValueCapture);
type Step = 'UserName' | 'Password';
let nextStep: Step = 'UserName';
while (true) {
if (nextStep === 'UserName') {
const errorMessage = validationMessage;
validationMessage = '';
const result = await inputValueCapture.getValue({
title: DataScience.jupyterSelectUserAndPasswordTitle,
prompt: DataScience.jupyterSelectUserPrompt,
validationMessage: errorMessage
});
if (result.value) {
username = result.value;
nextStep = 'Password';
continue;
}
break;
} else {
const result = await inputValueCapture.getValue({
title: DataScience.jupyterSelectUserAndPasswordTitle,
prompt: DataScience.jupyterSelectPasswordPrompt
});
if (result.value) {
password = result.value;
}
break;
}
}
}

private async validateUserNameOrPassword(_value: string): Promise<string | undefined> {
return undefined;
}

private async getPasswordMultiStep(
input: IMultiStepInput<{ username: string; password: string }>,
state: { username: string; password: string }
) {
state.password = await input.showInputBox({
title: DataScience.jupyterSelectUserAndPasswordTitle,
prompt: DataScience.jupyterSelectPasswordPrompt,
validate: this.validateUserNameOrPassword,
value: '',
password: true
});
return { username, password };
}

private async makeRequest(url: string, options: RequestInit): Promise<Response> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,82 +1,48 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import * as sinon from 'sinon';
import { assert } from 'chai';
import * as sinon from 'sinon';
import * as nodeFetch from 'node-fetch';
import { anything, instance, mock, when } from 'ts-mockito';
import { JupyterRequestCreator } from '../../kernels/jupyter/session/jupyterRequestCreator.web';
import { IJupyterRequestCreator, IJupyterServerUriStorage } from '../../kernels/jupyter/types';
import { ApplicationShell } from '../../platform/common/application/applicationShell';
import { AsyncDisposableRegistry } from '../../platform/common/asyncDisposableRegistry';
import { ConfigurationService } from '../../platform/common/configuration/service.node';
import { IDisposableRegistry } from '../../platform/common/types';
import { MultiStepInputFactory } from '../../platform/common/utils/multiStepInput';
import { MockInputBox } from '../../test/datascience/mockInputBox';
import { MockQuickPick } from '../../test/datascience/mockQuickPick';
import { IDisposable } from '../../platform/common/types';
import { JupyterHubPasswordConnect } from './jupyterHubPasswordConnect';
import { Disposable, InputBox } from 'vscode';
import { noop } from '../../test/core';
import { disposeAllDisposables } from '../../platform/common/helpers';
import { WorkflowInputValueProvider } from '../../platform/common/utils/inputValueProvider';

/* eslint-disable @typescript-eslint/no-explicit-any, , */
suite('Jupyter Hub Password Connect', () => {
let jupyterPasswordConnect: JupyterHubPasswordConnect;
let appShell: ApplicationShell;
let configService: ConfigurationService;
let requestCreator: IJupyterRequestCreator;

let inputBox: InputBox;
const disposables: IDisposable[] = [];
setup(() => {
inputBox = {
show: noop,
onDidAccept: noop as any,
onDidHide: noop as any,
hide: noop,
dispose: noop as any,
onDidChangeValue: noop as any,
onDidTriggerButton: noop as any,
valueSelection: undefined,
totalSteps: undefined,
validationMessage: '',
busy: false,
buttons: [],
enabled: true,
ignoreFocusOut: false,
password: false,
step: undefined,
title: '',
value: '',
prompt: '',
placeholder: ''
};
sinon.stub(inputBox, 'show').callsFake(noop);
sinon.stub(inputBox, 'onDidHide').callsFake(() => new Disposable(noop));
sinon.stub(inputBox, 'onDidAccept').callsFake((cb) => {
(cb as Function)();
return new Disposable(noop);
});

appShell = mock(ApplicationShell);
when(appShell.showInputBox(anything())).thenReturn(Promise.resolve('Python'));
when(appShell.createInputBox()).thenReturn(inputBox);
const multiStepFactory = new MultiStepInputFactory(instance(appShell));
const mockDisposableRegistry = mock(AsyncDisposableRegistry);
configService = mock(ConfigurationService);
requestCreator = mock(JupyterRequestCreator);
const serverUriStorage = mock<IJupyterServerUriStorage>();
const disposables = mock<IDisposableRegistry>();

jupyterPasswordConnect = new JupyterHubPasswordConnect(
instance(appShell),
multiStepFactory,
instance(mockDisposableRegistry),
instance(configService),
undefined,
instance(requestCreator),
instance(serverUriStorage),
instance(disposables)
disposables
);
});
teardown(() => {
sinon.restore();
disposeAllDisposables(disposables);
});

function createJupyterHubSetup() {
const dsSettings = {
Expand All @@ -85,11 +51,6 @@ suite('Jupyter Hub Password Connect', () => {
} as any;
when(configService.getSettings(anything())).thenReturn(dsSettings as any);

const quickPick = new MockQuickPick('');
const input = new MockInputBox('test', 2); // We want the input box to enter twice for this scenario
when(appShell.createQuickPick()).thenReturn(quickPick!);
when(appShell.createInputBox()).thenReturn(input);

const hubActiveResponse = mock(nodeFetch.Response);
when(hubActiveResponse.ok).thenReturn(true);
when(hubActiveResponse.status).thenReturn(200);
Expand Down Expand Up @@ -134,6 +95,7 @@ suite('Jupyter Hub Password Connect', () => {
};
}
test('Jupyter hub', async () => {
sinon.stub(WorkflowInputValueProvider.prototype, 'getValue').resolves({ value: 'test' });
const fetch = createJupyterHubSetup();
when(requestCreator.getFetchMethod()).thenReturn(fetch as any);

Expand Down
41 changes: 18 additions & 23 deletions src/standalone/userJupyterServer/jupyterPasswordConnect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { CancellationError, ConfigurationTarget, QuickInputButtons } from 'vscode';
import { CancellationError, ConfigurationTarget } from 'vscode';
import { IApplicationShell } from '../../platform/common/application/types';
import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../platform/common/types';
import { DataScience } from '../../platform/common/utils/localize';
Expand All @@ -16,6 +16,7 @@ import {
IJupyterServerUriStorage
} from '../../kernels/jupyter/types';
import { disposeAllDisposables } from '../../platform/common/helpers';
import { WorkflowInputValueProvider } from '../../platform/common/utils/inputValueProvider';

export interface IJupyterPasswordConnectInfo {
requiresPassword: boolean;
Expand Down Expand Up @@ -123,29 +124,23 @@ export class JupyterPasswordConnect {
friendlyUrl = `${uri.protocol}//${uri.hostname}`;
friendlyUrl = options.displayName ? `${options.displayName} (${friendlyUrl})` : friendlyUrl;
if (requiresPassword && options.isTokenEmpty) {
const input = this.appShell.createInputBox();
options.disposables.push(input);
input.title = DataScience.jupyterSelectPasswordTitle(friendlyUrl);
input.prompt = DataScience.jupyterSelectPasswordPrompt;
input.ignoreFocusOut = true;
input.password = true;
input.validationMessage = options.validationErrorMessage || '';
input.show();
input.buttons = [QuickInputButtons.Back];
userPassword = await new Promise<string>((resolve, reject) => {
input.onDidTriggerButton(
(e) => {
if (e === QuickInputButtons.Back) {
reject(InputFlowAction.back);
}
},
this,
options.disposables
);
input.onDidChangeValue(() => (input.validationMessage = ''), this, options.disposables);
input.onDidAccept(() => resolve(input.value), this, options.disposables);
input.onDidHide(() => reject(InputFlowAction.cancel), this, options.disposables);
const inputProvider = new WorkflowInputValueProvider();
options.disposables.push(inputProvider);
const result = await inputProvider.getValue({
title: DataScience.jupyterSelectPasswordTitle(friendlyUrl),
prompt: DataScience.jupyterSelectPasswordPrompt,
ignoreFocusOut: true,
password: true,
validationMessage: options.validationErrorMessage || ''
});
if (result.navigation) {
if (result.navigation === 'back') {
throw InputFlowAction.back;
}
throw InputFlowAction.cancel;
} else {
userPassword = result.value;
}
}

if (typeof userPassword === undefined && !userPassword && options.isTokenEmpty) {
Expand Down
Loading

0 comments on commit 3c095b1

Please sign in to comment.