Skip to content

Commit

Permalink
plugin-2802: Introduce window.withProgress API endpoint
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
  • Loading branch information
vinokurig committed Nov 28, 2018
1 parent 1ecc481 commit e220094
Show file tree
Hide file tree
Showing 14 changed files with 695 additions and 40 deletions.
77 changes: 71 additions & 6 deletions packages/core/src/common/message-service-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,59 @@

import { injectable, inject } from 'inversify';
import { ILogger } from './logger';
import { CancellationToken } from './cancellation';

export const messageServicePath = '/services/messageService';

export enum MessageType {
Error = 1,
Warning = 2,
Info = 3,
Log = 4
Log = 4,
Progress = 5
}

export interface Message {
type: MessageType;
text: string;
actions?: string[];
options?: MessageOptions;
readonly type?: MessageType;
readonly text: string;
readonly actions?: string[];
readonly options?: MessageOptions;
}

export interface ProgressMessage extends Message {
readonly type?: MessageType.Progress;
readonly options?: ProgressMessageOptions;
}
export namespace ProgressMessage {
export const Cancel = 'Cancel';
export function isCancelable(message: ProgressMessage): boolean {
return !message.options
|| message.options.cancelable === undefined
|| message.options.cancelable === true;
}
}

export interface MessageOptions {
timeout?: number;
readonly timeout?: number;
}

export interface ProgressMessageOptions extends MessageOptions {
/**
* Default: `true`
*/
readonly cancelable?: boolean;
}

export interface Progress {
readonly id: string;
readonly report: (update: ProgressUpdate) => void;
readonly cancel: () => void;
readonly result: Promise<string | undefined>;
}

export interface ProgressUpdate {
readonly message?: string;
readonly work?: { done: number, total: number };
}

@injectable()
Expand All @@ -53,6 +87,25 @@ export class MessageClient {
this.logger.info(message.text);
return Promise.resolve(undefined);
}

/**
* Show progress message with possible actions to user.
*
* To be implemented by an extension, e.g. by the messages extension.
*/
showProgress(progressId: string, message: ProgressMessage, cancellationToken: CancellationToken): Promise<string | undefined> {
this.logger.info(message.text);
return Promise.resolve(undefined);
}

/**
* Update started progress message.
*
* To be implemented by an extension, e.g. by the messages extension.
*/
reportProgress(progressId: string, update: ProgressUpdate, message: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
return Promise.resolve(undefined);
}
}

@injectable()
Expand All @@ -66,4 +119,16 @@ export class DispatchingMessageClient extends MessageClient {
));
}

showProgress(progressId: string, message: ProgressMessage, cancellationToken: CancellationToken): Promise<string | undefined> {
return Promise.race([...this.clients].map(client =>
client.showProgress(progressId, message, cancellationToken)
));
}

reportProgress(progressId: string, update: ProgressUpdate, message: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
return Promise.race([...this.clients].map(client =>
client.reportProgress(progressId, update, message, cancellationToken)
));
}

}
43 changes: 42 additions & 1 deletion packages/core/src/common/message-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
********************************************************************************/

import { injectable, inject } from 'inversify';
import { MessageClient, MessageType, MessageOptions } from './message-service-protocol';
import {
MessageClient,
MessageType,
MessageOptions,
Progress,
ProgressUpdate,
ProgressMessage
} from './message-service-protocol';
import { CancellationTokenSource } from './cancellation';

@injectable()
export class MessageService {
Expand Down Expand Up @@ -65,4 +73,37 @@ export class MessageService {
return this.client.showMessage({ type, text });
}

async showProgress(message: ProgressMessage, onDidCancel?: () => void): Promise<Progress> {
const id = this.newProgressId();
const cancellationSource = new CancellationTokenSource();
const report = (update: ProgressUpdate) => {
this.client.reportProgress(id, update, message, cancellationSource.token);
};
let clientMessage = message;
if (ProgressMessage.isCancelable(message)) {
const actions = new Set<string>(message.actions);
actions.add(ProgressMessage.Cancel);
clientMessage = { ...message, actions: Array.from(actions) };
}
const result = this.client.showProgress(id, clientMessage, cancellationSource.token);
if (ProgressMessage.isCancelable(message) && typeof onDidCancel === 'function') {
result.then(value => {
if (value === ProgressMessage.Cancel) {
onDidCancel();
}
});
}
return {
id,
cancel: () => cancellationSource.cancel(),
result,
report
};
}

private progressIdPrefix = Math.random().toString(36).substring(5);
private counter = 0;
protected newProgressId(): string {
return `${this.progressIdPrefix}-${++this.counter}`;
}
}
72 changes: 57 additions & 15 deletions packages/messages/src/browser/notifications-message-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import { injectable, inject } from 'inversify';
import {
MessageClient,
MessageType,
Message
Message,
ProgressMessage,
ProgressUpdate,
CancellationToken
} from '@theia/core/lib/common';
import { Notifications, NotificationAction } from './notifications';
import { Notifications, NotificationAction, NotificationProperties, ProgressNotification} from './notifications';
import { NotificationPreferences } from './notification-preferences';

@injectable()
Expand All @@ -33,26 +36,65 @@ export class NotificationsMessageClient extends MessageClient {
return this.show(message);
}

showProgress(progressId: string, message: ProgressMessage, cancellationToken: CancellationToken, update?: ProgressUpdate): Promise<string | undefined> {
const messageArguments = { ...message, type: MessageType.Progress, options: { ...(message.options || {}), timeout: 0 } };
if (this.visibleProgressNotifications.has(progressId)) {
throw new Error('Cannot show new progress with already existing id.');
}
return new Promise(resolve => {
const progressNotification = this.notifications.create(this.getNotificationProperties(progressId, messageArguments, action => {
this.visibleProgressNotifications.delete(progressId);
resolve(action);
}));
this.visibleProgressNotifications.set(progressId, progressNotification);
progressNotification.show();
if (update) {
progressNotification.update(update);
}
const cancel = () => {
if (message.options && message.options.cancelable) {
resolve(ProgressMessage.Cancel);
}
progressNotification.close();
};
if (cancellationToken.isCancellationRequested) {
cancel();
} else {
cancellationToken.onCancellationRequested(cancel);
}
});
}

async reportProgress(progressId: string, update: ProgressUpdate, message: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
const notification = this.visibleProgressNotifications.get(progressId);
if (notification) {
notification.update(update);
} else {
this.showProgress(progressId, message, cancellationToken, update);
}
}

protected visibleMessages = new Set<string>();
protected visibleProgressNotifications = new Map<string, ProgressNotification>();
protected show(message: Message): Promise<string | undefined> {
const key = this.getKey(message);
if (this.visibleMessages.has(key)) {
return Promise.resolve(undefined);
}
this.visibleMessages.add(key);
return new Promise(resolve => {
this.showToast(message, a => {
this.notifications.show(this.getNotificationProperties(key, message, action => {
this.visibleMessages.delete(key);
resolve(a);
});
resolve(action);
}));
});
}

protected getKey(m: Message): string {
return `${m.type}-${m.text}-${m.actions ? m.actions.join('|') : '|'}`;
}

protected showToast(message: Message, onCloseFn: (action: string | undefined) => void): void {
protected getNotificationProperties(id: string, message: Message, onCloseFn: (action: string | undefined) => void): NotificationProperties {
const icon = this.iconFor(message.type);
const text = message.text;
const actions = (message.actions || []).map(action => <NotificationAction>{
Expand All @@ -69,22 +111,22 @@ export class NotificationsMessageClient extends MessageClient {
label: 'Close',
fn: element => onCloseFn(undefined)
});
this.notifications.show({
return {
id,
icon,
text,
actions,
timeout,
onTimeout: () => onCloseFn(undefined)
});
};
}

protected iconFor(type: MessageType): string {
if (type === MessageType.Error) {
return 'error';
}
if (type === MessageType.Warning) {
return 'warning';
protected iconFor(type: MessageType | undefined): string {
switch (type) {
case MessageType.Error: return 'error';
case MessageType.Warning: return 'warning';
case MessageType.Progress: return 'progress';
default: return 'info';
}
return 'info';
}
}
Loading

0 comments on commit e220094

Please sign in to comment.