Skip to content

Commit

Permalink
Merge pull request #2522 from posit-dev/sagerb-support-dismissed-depl…
Browse files Browse the repository at this point in the history
…oyment

Improvements to dismissing a deployment
  • Loading branch information
sagerb authored Jan 22, 2025
2 parents 5dc3269 + bdf87bb commit 3d28b74
Show file tree
Hide file tree
Showing 36 changed files with 798 additions and 147 deletions.
18 changes: 18 additions & 0 deletions extensions/vscode/src/api/resources/ContentRecords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,22 @@ export class ContentRecords {
},
);
}

// Returns:
// 200 - success
// 404 - not found
// 500 - internal server error
cancelDeployment(deploymentName: string, dir: string, localId: string) {
const encodedName = encodeURIComponent(deploymentName);
const encodedLocalId = encodeURIComponent(localId);
return this.client.post<ContentRecord>(
`deployments/${encodedName}/cancel/${encodedLocalId}`,
{},
{
params: {
dir,
},
},
);
}
}
1 change: 1 addition & 0 deletions extensions/vscode/src/api/types/contentRecords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ContentRecordRecord = {
serverUrl: string;
saveName: string;
createdAt: string;
dismissedAt: string;
configurationName: string;
type: ContentType;
deploymentError: AgentError | null;
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/api/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,7 +1254,7 @@ export interface PublishFailure extends EventStreamMessage {
data: {
dashboardUrl: string;
url: string;
canceled?: string; // not defined if not user cancelled. Value of "true" if true.
canceled?: string; // not defined if not user canceled. Value of "true" if true.
// and other non-defined attributes
};
error: string; // translated internally
Expand Down
25 changes: 17 additions & 8 deletions extensions/vscode/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export function displayEventStreamMessage(msg: EventStreamMessage): string {
if (msg.data.dashboardUrl) {
return `Deployment failed, click to view Connect logs: ${msg.data.dashboardUrl}`;
}
if (msg.data.canceled === "true") {
return "Deployment dismissed";
}
return "Deployment failed";
}
if (msg.error !== undefined) {
Expand All @@ -95,8 +98,8 @@ export class EventStream extends Readable implements Disposable {
private messages: EventStreamMessage[] = [];
// Map to store event callbacks
private callbacks: Map<string, EventStreamRegistration[]> = new Map();
// Cancelled Event Streams - Suppressed when received
private cancelledLocalIDs: string[] = [];
// Canceled Event Streams - Suppressed when received
private canceledLocalIDs: string[] = [];

/**
* Creates a new instance of the EventStream class.
Expand Down Expand Up @@ -170,19 +173,25 @@ export class EventStream extends Readable implements Disposable {
* @returns undefined
*/
public suppressMessages(localId: string) {
this.cancelledLocalIDs.push(localId);
this.canceledLocalIDs.push(localId);
}

private processMessage(msg: EventStreamMessage) {
const localId = msg.data.localId;
if (localId && this.cancelledLocalIDs.includes(localId)) {
// Some log messages passed on from Connect include
// the localId using snake_case, rather than pascalCase.
// To filter correctly, we need to check for both.

const localId = msg.data.localId || msg.data.local_id;
if (localId && this.canceledLocalIDs.includes(localId)) {
// suppress and ignore
return;
}

// Trace message
// console.debug(
// `eventSource trace: ${event.type}: ${JSON.stringify(event)}`,
// );
// Uncomment the following code if you want to dump every message to the
// console as it is received.
// console.debug(`eventSource trace: ${msg.type}: ${JSON.stringify(msg)}`);

// Add the message to the messages array
this.messages.push(msg);
// Emit a 'message' event with the message as the payload
Expand Down
2 changes: 1 addition & 1 deletion extensions/vscode/src/multiStepInputs/newCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function newCredential(
totalSteps: -1,
data: {
// each attribute is initialized to undefined
// to be returned when it has not been cancelled
// to be returned when it has not been canceled
url: startingServerUrl, // eventual type is string
apiKey: <string | undefined>undefined, // eventual type is string
name: <string | undefined>undefined, // eventual type is string
Expand Down
6 changes: 3 additions & 3 deletions extensions/vscode/src/multiStepInputs/newDeployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export async function newDeployment(
title: "Select Entrypoint File (main file for your project)",
});
if (!fileUris || !fileUris[0]) {
// cancelled.
// canceled.
continue;
}
const fileUri = fileUris[0];
Expand Down Expand Up @@ -783,7 +783,7 @@ export async function newDeployment(
!newDeploymentData.title ||
(!newCredentialByAnyMeans() && !newDeploymentData.existingCredentialName)
) {
console.log("User has aborted flow. Exiting.");
console.log("User has dismissed flow. Exiting.");
return undefined;
}

Expand All @@ -796,7 +796,7 @@ export async function newDeployment(
!newDeploymentData.newCredentials.apiKey ||
!newDeploymentData.newCredentials.name
) {
console.log("User has aborted flow. Exiting.");
console.log("User has dismissed flow. Exiting.");
return undefined;
}
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export async function selectNewOrExistingConfig(
totalSteps: -1,
data: {
// each attribute is initialized to undefined
// to be returned when it has not been cancelled to assist type guards
// to be returned when it has not been canceled to assist type guards
// Note: We can't initialize existingConfigurationName to a specific initial
// config, as we then wouldn't be able to detect if the user hit ESC to exit
// the selection. :-(
Expand Down
20 changes: 20 additions & 0 deletions extensions/vscode/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ export class PublisherState implements Disposable {
}
}

updateContentRecord(
newValue: ContentRecord | PreContentRecord | PreContentRecordWithConfig,
) {
const existingContentRecord = this.findContentRecord(
newValue.saveName,
newValue.projectDir,
);
if (existingContentRecord) {
const crIndex = this.contentRecords.findIndex(
(contentRecord) =>
contentRecord.deploymentPath === existingContentRecord.deploymentPath,
);
if (crIndex !== -1) {
this.contentRecords[crIndex] = newValue;
} else {
this.contentRecords.push(newValue);
}
}
}

async getSelectedConfiguration() {
const contentRecord = await this.getSelectedContentRecord();
if (!contentRecord) {
Expand Down
2 changes: 2 additions & 0 deletions extensions/vscode/src/test/unit-test-utils/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const preContentRecordFactory = Factory.define<PreContentRecord>(
serverUrl: `https://connect-test-${sequence}/connect`,
saveName: `Report ${sequence}`,
createdAt: new Date().toISOString(),
dismissedAt: "",
configurationName: `report-GUD${sequence}`,
type: ContentType.RMD,
deploymentError: null,
Expand All @@ -67,6 +68,7 @@ export const contentRecordFactory = Factory.define<ContentRecord>(
serverUrl: `https://connect-test-${sequence}/connect`,
saveName: `Report ${sequence}`,
createdAt: new Date().toISOString(),
dismissedAt: "",
deployedAt: new Date().toISOString(),
configurationName: `report-GUD${sequence}`,
type: ContentType.RMD,
Expand Down
75 changes: 56 additions & 19 deletions extensions/vscode/src/views/deployProgress.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
// Copyright (C) 2024 by Posit Software, PBC.

import { ProgressLocation, Uri, env, window } from "vscode";
import { EventStreamMessage, eventMsgToString } from "src/api";
import {
EventStreamMessage,
eventMsgToString,
useApi,
ContentRecord,
PreContentRecord,
PreContentRecordWithConfig,
} from "src/api";
import { EventStream, UnregisterCallback } from "src/events";
import { getSummaryStringFromError } from "src/utils/errors";

export function deployProject(localID: string, stream: EventStream) {
type UpdateActiveContentRecordCB = (
contentRecord: ContentRecord | PreContentRecord | PreContentRecordWithConfig,
) => void;

export function deployProject(
deploymentName: string,
dir: string,
localID: string,
stream: EventStream,
updateActiveContentRecordCB: UpdateActiveContentRecordCB,
) {
window.withProgress(
{
location: ProgressLocation.Notification,
Expand All @@ -27,25 +45,44 @@ export function deployProject(localID: string, stream: EventStream) {
registrations.forEach((cb) => cb.unregister());
};

token.onCancellationRequested(() => {
token.onCancellationRequested(async () => {
const api = await useApi();
streamID = "NEVER_A_VALID_STREAM";
unregisterAll();
// inject a psuedo end of publishing event
stream.injectMessage({
type: "publish/failure",
time: Date.now().toString(),
data: {
dashboardUrl: "",
url: "",
// and other non-defined attributes
localId: localID,
cancelled: "true",
message:
"Deployment has been dismissed (but will continue to be processed on the Connect Server). Deployment status will be reset to the prior known state.",
},
error: "Deployment has been dismissed.",
});
stream.suppressMessages(localID);
try {
const response = await api.contentRecords.cancelDeployment(
deploymentName,
dir,
localID,
);

// update the UX locally
updateActiveContentRecordCB(response.data);

// we must have been successful...
// inject a psuedo end of publishing event
stream.injectMessage({
type: "publish/failure",
time: Date.now().toString(),
data: {
dashboardUrl: "",
url: "",
// and other non-defined attributes
localId: localID,
canceled: "true",
message:
"Deployment has been dismissed, but will continue to be processed on the Connect Server.",
},
error: "Deployment has been dismissed.",
});
stream.suppressMessages(localID);
} catch (error: unknown) {
const summary = getSummaryStringFromError(
"deployProject, token.onCancellationRequested",
error,
);
window.showErrorMessage(`Unable to dismiss deployment: ${summary}`);
}
resolveCB();
});

Expand Down
24 changes: 22 additions & 2 deletions extensions/vscode/src/views/homeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
useApi,
AllContentRecordTypes,
EnvironmentConfig,
PreContentRecordWithConfig,
} from "src/api";
import { EventStream } from "src/events";
import { getPythonInterpreterPath, getRInterpreterPath } from "../utils/vscode";
Expand Down Expand Up @@ -208,7 +209,13 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
secrets,
r,
);
deployProject(response.data.localId, this.stream);
deployProject(
deploymentName,
projectDir,
response.data.localId,
this.stream,
this.updateActiveContentRecordLocally.bind(this),
);
} catch (error: unknown) {
// Most failures will occur on the event stream. These are the ones which
// are immediately rejected as part of the API request to initiate deployment.
Expand Down Expand Up @@ -310,6 +317,19 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
}
}

private updateActiveContentRecordLocally(
activeContentRecord:
| ContentRecord
| PreContentRecord
| PreContentRecordWithConfig,
) {
// update our local state, so we don't wait on file refreshes
this.state.updateContentRecord(activeContentRecord);

// refresh the webview
this.updateWebViewViewContentRecords();
}

private onPublishStart() {
this.webviewConduit.sendMsg({
kind: HostToWebviewMessageType.PUBLISH_START,
Expand Down Expand Up @@ -949,7 +969,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
activeConfig.configuration.environment,
);
if (name === undefined) {
// Cancelled by the user
// Canceled by the user
return;
}

Expand Down
Loading

0 comments on commit 3d28b74

Please sign in to comment.