Skip to content

Commit

Permalink
Simplify state management for new webviews (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterbom authored Oct 26, 2023
1 parent 405cd2d commit 9c5b05d
Show file tree
Hide file tree
Showing 46 changed files with 945 additions and 1,011 deletions.
164 changes: 66 additions & 98 deletions src/panels/AzureServiceOperatorPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import { createTempFile } from "../commands/utils/tempfile";

export class AzureServiceOperatorPanel extends BasePanel<"aso"> {
constructor(extensionUri: vscode.Uri) {
super(extensionUri, "aso");
super(extensionUri, "aso", {
checkSPResponse: null,
installCertManagerResponse: null,
installOperatorResponse: null,
installOperatorSettingsResponse: null,
waitForCertManagerResponse: null,
waitForControllerManagerResponse: null
});
}
}

Expand Down Expand Up @@ -49,30 +56,24 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
private async _handleCheckSPRequest(appId: string, appSecret: string, webview: MessageSink<ToWebViewMsgDef>): Promise<void> {
const servicePrincipalAccess = await getServicePrincipalAccess(this.azAccount, appId, appSecret);
if (failed(servicePrincipalAccess)) {
webview.postMessage({
command: "checkSPResponse",
parameters: {
succeeded: false,
errorMessage: servicePrincipalAccess.error,
commandResults: [],
cloudName: null,
subscriptions: [],
tenantId: null
}
webview.postCheckSPResponse({
succeeded: false,
errorMessage: servicePrincipalAccess.error,
commandResults: [],
cloudName: null,
subscriptions: [],
tenantId: null
});
return;
}

webview.postMessage({
command: "checkSPResponse",
parameters: {
succeeded: true,
errorMessage: null,
commandResults: [],
cloudName: servicePrincipalAccess.result.cloudName as AzureCloudName,
subscriptions: servicePrincipalAccess.result.subscriptions,
tenantId: servicePrincipalAccess.result.tenantId
}
webview.postCheckSPResponse({
succeeded: true,
errorMessage: null,
commandResults: [],
cloudName: servicePrincipalAccess.result.cloudName as AzureCloudName,
subscriptions: servicePrincipalAccess.result.subscriptions,
tenantId: servicePrincipalAccess.result.tenantId
});
}

Expand All @@ -83,13 +84,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const kubectlArgs = `create -f ${asoCrdYamlFile}`;
const shellOutput = await invokeKubectlCommand(this.kubectl, this.kubeConfigFilePath, kubectlArgs, NonZeroExitCodeBehaviour.Succeed);
if (failed(shellOutput)) {
webview.postMessage({
command: "installCertManagerResponse",
parameters: {
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
}
webview.postInstallCertManagerResponse({
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
});
return;
}
Expand All @@ -98,13 +96,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const errorMessage = succeeded ? null : "Installing cert-manager failed, see error output.";
const { stdout, stderr } = shellOutput.result;
const command = `kubectl ${kubectlArgs}`;
webview.postMessage({
command: "installCertManagerResponse",
parameters: {
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
}
webview.postInstallCertManagerResponse({
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
});
}

Expand All @@ -117,27 +112,21 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
}));
const shellResults = combine(promiseResults);
if (failed(shellResults)) {
webview.postMessage({
command: "waitForCertManagerResponse",
parameters: {
succeeded: false,
errorMessage: shellResults.error,
commandResults: []
}
webview.postWaitForCertManagerResponse({
succeeded: false,
errorMessage: shellResults.error,
commandResults: []
});
return;
}

// There was no error running the commands, but there may have been a non-zero exit code.
const succeeded = !shellResults.result.some(r => r.code !== 0);
const errorMessage = succeeded ? null : "Waiting for cert-manager failed, see error output.";
webview.postMessage({
command: "waitForCertManagerResponse",
parameters: {
succeeded,
errorMessage,
commandResults: shellResults.result.map(r => ({command: r.command, stdout: r.stdout, stderr: r.stderr}))
}
webview.postWaitForCertManagerResponse({
succeeded,
errorMessage,
commandResults: shellResults.result.map(r => ({command: r.command, stdout: r.stdout, stderr: r.stderr}))
});
}

Expand All @@ -153,13 +142,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const kubectlArgs = `create -f ${asoYamlFile} --request-timeout 120s`;
const shellOutput = await invokeKubectlCommand(this.kubectl, this.kubeConfigFilePath, kubectlArgs, NonZeroExitCodeBehaviour.Succeed);
if (failed(shellOutput)) {
webview.postMessage({
command: "installOperatorResponse",
parameters: {
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
}
webview.postInstallOperatorResponse({
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
});
return;
}
Expand All @@ -168,13 +154,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const errorMessage = succeeded ? null : "Installing operator resource failed, see error output.";
const { stdout, stderr } = shellOutput.result;
const command = `kubectl ${kubectlArgs}`;
webview.postMessage({
command: "installOperatorResponse",
parameters: {
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
}
webview.postInstallOperatorResponse({
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
});
}

Expand All @@ -186,13 +169,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
try {
settingsTemplate = await fs.readFile(yamlPathOnDisk.fsPath, 'utf8');
} catch (e) {
webview.postMessage({
command: "installOperatorSettingsResponse",
parameters: {
succeeded: false,
errorMessage: `Failed to read settings template from ${yamlPathOnDisk.fsPath}: ${getErrorMessage(e)}`,
commandResults: []
}
webview.postInstallOperatorSettingsResponse({
succeeded: false,
errorMessage: `Failed to read settings template from ${yamlPathOnDisk.fsPath}: ${getErrorMessage(e)}`,
commandResults: []
});
return;
}
Expand All @@ -210,13 +190,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const kubectlArgs = `apply -f ${templateYamlFile.filePath} --request-timeout 120s`;
const shellOutput = await invokeKubectlCommand(this.kubectl, this.kubeConfigFilePath, kubectlArgs, NonZeroExitCodeBehaviour.Succeed);
if (failed(shellOutput)) {
webview.postMessage({
command: "installOperatorSettingsResponse",
parameters: {
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
}
webview.postInstallOperatorSettingsResponse({
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
});
return;
}
Expand All @@ -225,27 +202,21 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const errorMessage = succeeded ? null : "Installing operator settings failed, see error output.";
const { stdout, stderr } = shellOutput.result;
const command = `kubectl ${kubectlArgs}`;
webview.postMessage({
command: "installOperatorSettingsResponse",
parameters: {
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
}
webview.postInstallOperatorSettingsResponse({
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
});
}

private async _handleWaitForControllerManagerRequest(webview: MessageSink<ToWebViewMsgDef>): Promise<void> {
const kubectlArgs = "rollout status -n azureserviceoperator-system deploy/azureserviceoperator-controller-manager --timeout=240s";
const shellOutput = await invokeKubectlCommand(this.kubectl, this.kubeConfigFilePath, kubectlArgs, NonZeroExitCodeBehaviour.Succeed);
if (failed(shellOutput)) {
webview.postMessage({
command: "waitForControllerManagerResponse",
parameters: {
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
}
webview.postWaitForControllerManagerResponse({
succeeded: false,
errorMessage: shellOutput.error,
commandResults: []
});
return;
}
Expand All @@ -254,13 +225,10 @@ export class AzureServiceOperatorDataProvider implements PanelDataProvider<"aso"
const errorMessage = succeeded ? null : "Waiting for ASO Controller Manager failed, see error output.";
const { stdout, stderr } = shellOutput.result;
const command = `kubectl ${kubectlArgs}`;
webview.postMessage({
command: "waitForControllerManagerResponse",
parameters: {
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
}
webview.postWaitForControllerManagerResponse({
succeeded,
errorMessage,
commandResults: [{ command, stdout, stderr }]
});
}
}
68 changes: 31 additions & 37 deletions src/panels/BasePanel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Disposable, Webview, window, Uri, ViewColumn } from "vscode";
import { MessageDefinition, MessageHandler, isValidMessage } from "../webview-contract/messaging";
import { CommandKeys, MessageDefinition, MessageHandler, MessageSource, PostMessageImpl, asMessageSink, isValidMessage } from "../webview-contract/messaging";
import { getNonce, getUri } from "./utilities/webview";
import { encodeState } from "../webview-contract/initialState";
import { ContentId, InitialState, ToVsCodeMessage, ToVsCodeMessageHandler, ToWebviewMessage, ToWebviewMessageSink, VsCodeMessageContext } from "../webview-contract/webviewTypes";
import { ContentId, InitialState, ToVsCodeMessage, ToVsCodeMessageHandler, ToVsCodeMsgDef, ToWebviewMessageSink, ToWebviewMsgDef, VsCodeMessageContext } from "../webview-contract/webviewTypes";

const viewType = "aksVsCodeTools";

Expand All @@ -23,7 +23,8 @@ export interface PanelDataProvider<TContent extends ContentId> {
export abstract class BasePanel<TContent extends ContentId> {
protected constructor(
readonly extensionUri: Uri,
readonly contentId: TContent
readonly contentId: TContent,
readonly webviewCommandKeys: CommandKeys<ToWebviewMsgDef<TContent>>
) { }

show(dataProvider: PanelDataProvider<TContent>, ...disposables: Disposable[]) {
Expand All @@ -40,7 +41,7 @@ export abstract class BasePanel<TContent extends ContentId> {
const panel = window.createWebviewPanel(viewType, title, ViewColumn.One, panelOptions);

// Set up messaging between VSCode and the webview.
const messageContext = new MessageContext<TContent>(panel.webview, disposables);
const messageContext = getMessageContext(panel.webview, this.webviewCommandKeys, disposables);
const messageHandler = dataProvider.getMessageHandler(messageContext);
messageContext.subscribeToMessages(messageHandler);

Expand Down Expand Up @@ -86,36 +87,29 @@ export abstract class BasePanel<TContent extends ContentId> {
}
}

/**
* A `MessageContext` that represents the VSCode end of the communication,
* i.e. it posts messages to the webview, and listens to messages to VS Code.
*/
class MessageContext<TContent extends ContentId> implements VsCodeMessageContext<TContent> {
constructor(
private readonly _webview: Webview,
private readonly _disposables: Disposable[]
) { }

postMessage(message: ToWebviewMessage<TContent>) {
this._webview.postMessage(message);
}

subscribeToMessages(handler: ToVsCodeMessageHandler<TContent>) {
this._webview.onDidReceiveMessage(
(message: any) => {
if (!isValidMessage<ToVsCodeMessage<TContent>>(message)) {
throw new Error(`Invalid message to VsCode: ${JSON.stringify(message)}`);
}

const action = (handler as MessageHandler<MessageDefinition>)[message.command];
if (action) {
action(message.parameters, message.command);
} else {
window.showErrorMessage(`No handler found for command ${message.command}`);
}
},
undefined,
this._disposables
);
}
}
function getMessageContext<TContent extends ContentId>(webview: Webview, webviewCommandKeys: CommandKeys<ToWebviewMsgDef<TContent>>, disposables: Disposable[]): VsCodeMessageContext<TContent> {
const postMessageImpl: PostMessageImpl<ToWebviewMsgDef<TContent>> = message => webview.postMessage(message);
const sink = asMessageSink(postMessageImpl, webviewCommandKeys);
const source: MessageSource<ToVsCodeMsgDef<TContent>> = {
subscribeToMessages(handler) {
webview.onDidReceiveMessage(
(message: any) => {
if (!isValidMessage<ToVsCodeMessage<TContent>>(message)) {
throw new Error(`Invalid message to VsCode: ${JSON.stringify(message)}`);
}

const action = (handler as MessageHandler<MessageDefinition>)[message.command];
if (action) {
action(message.parameters, message.command);
} else {
window.showErrorMessage(`No handler found for command ${message.command}`);
}
},
undefined,
disposables
);
},
};

return {...sink, ...source};
}
Loading

0 comments on commit 9c5b05d

Please sign in to comment.