From 7d95f17184199e32c7953ab7bb8af5f8fb16b4dc Mon Sep 17 00:00:00 2001 From: Marcel Kloubert Date: Mon, 9 Jul 2018 01:34:25 +0200 Subject: [PATCH] notifications --- CHANGELOG.md | 10 + README.md | 1 + package-lock.json | 16 +- package.json | 12 +- src/contracts.ts | 32 ++- src/devtools.ts | 351 ---------------------------- src/extension.ts | 32 ++- src/i18.ts | 10 +- src/lang/de.ts | 10 +- src/lang/en.ts | 8 + src/notifications.ts | 363 +++++++++++++++++++++++++++++ src/packages.ts | 7 +- src/targets.ts | 7 +- src/targets/operations/devtools.ts | 8 +- src/tools/quickexecution.ts | 40 +--- 15 files changed, 470 insertions(+), 437 deletions(-) delete mode 100644 src/devtools.ts create mode 100644 src/notifications.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e776573..17d9a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ [![Share via Facebook](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Facebook.png)](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded"e=vscode-deploy-reloaded) [![Share via Twitter](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Twitter.png)](https://twitter.com/intent/tweet?source=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded&text=vscode-deploy-reloaded:%20https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded&via=mjkloubert) [![Share via Google+](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Google+.png)](https://plus.google.com/share?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded) [![Share via Pinterest](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Pinterest.png)](https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded&media=https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/demo1.gif&description=Recoded%20version%20of%20Visual%20Studio%20Code%20extension%20%27vs-deploy%27%2C%20which%20provides%20commands%20to%20deploy%20files%20to%20one%20or%20more%20destinations.) [![Share via Reddit](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Reddit.png)](https://www.reddit.com/submit?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded&title=vscode-deploy-reloaded) [![Share via LinkedIn](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/LinkedIn.png)](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded&title=vscode-deploy-reloaded&summary=Recoded%20version%20of%20Visual%20Studio%20Code%20extension%20%27vs-deploy%27%2C%20which%20provides%20commands%20to%20deploy%20files%20to%20one%20or%20more%20destinations.&source=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded) [![Share via Wordpress](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Wordpress.png)](https://wordpress.com/press-this.php?u=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded"e=vscode-deploy-reloaded&s=Recoded%20version%20of%20Visual%20Studio%20Code%20extension%20%27vs-deploy%27%2C%20which%20provides%20commands%20to%20deploy%20files%20to%20one%20or%20more%20destinations.&i=https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/demo1.gif) [![Share via Email](https://raw.githubusercontent.com/mkloubert/vscode-deploy-reloaded/master/img/share/Email.png)](mailto:?subject=vscode-deploy-reloaded&body=Recoded%20version%20of%20Visual%20Studio%20Code%20extension%20'vs-deploy'%2C%20which%20provides%20commands%20to%20deploy%20files%20to%20one%20or%20more%20destinations.:%20https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dmkloubert.vscode-deploy-reloaded) +## 0.84.0 (June 9th, 2018; notifications) + +* notifications are shown at startup now +* bugfixes +* removed `preview` flag from [package.json](https://github.com/mkloubert/vscode-deploy-reloaded/blob/master/package.json) +* extension requires at least [Visual Studio Code 1.25](https://code.visualstudio.com/updates/v1_25) now +* [uuid](https://www.npmjs.com/package/uuid) module is deprecated ... [here](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/deprecated#uuid) you can learn, how to migrate +* updated the following [npm](https://www.npmjs.com/) modules: + * [vscode-helpers](https://www.npmjs.com/package/vscode-helpers) `^2.12.0` + ## 0.83.0 (July 7th, 2018; chrome debugger and sync timestamps) * added [chrome target operation](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/target_operations#chrome-), which can executes a method for a DevTools compatible browser debugger, as implemented in [Google Chrome](https://www.google.de/chrome/index.html): diff --git a/README.md b/README.md index a3b95b3..8946429 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,7 @@ Press `F1` and enter one of the following commands: | `Deploy Reloaded: Proxies ...` | Handles [TCP proxies](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/proxies). | | `Deploy Reloaded: Pull ...` | Pull or download files from remote. | | `Deploy Reloaded: Select deploy operation ...` | Lets the user select an operation for the active document. | +| `Deploy Reloaded: Show notification(s) ...` | (Re-)Shows one or more notifications from the extension's author(s). | | `Deploy Reloaded: Switches ...` | Handle [switch targets](https://github.com/mkloubert/vscode-deploy-reloaded/wiki/target_switch). | | `Deploy Reloaded: Tools ...` | A set of helpful tools. | diff --git a/package-lock.json b/package-lock.json index 11a7611..07ebcd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-deploy-reloaded", - "version": "0.83.0", + "version": "0.84.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -359,7 +359,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz", "integrity": "sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg==", - "dev": true, "requires": { "@types/events": "*", "@types/node": "*" @@ -6468,9 +6467,9 @@ } }, "vscode-helpers": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/vscode-helpers/-/vscode-helpers-2.10.3.tgz", - "integrity": "sha512-pCxry1n+tkWjKH17GXtPy62IqLr+QU1x0hCe2IP+8jGKGhe+hw92WGn8M3sNkbCCnUv/ThRYk4M8o+lY51gcnQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/vscode-helpers/-/vscode-helpers-2.12.0.tgz", + "integrity": "sha512-1MPFPo5ih+MwIEP99bwWIu/8nd6pFyI+kSex8iaGjw9f/1ouKd6SQsV68fT+1nC7HhrMoRcYETpxArAukk615w==", "requires": { "@types/fs-extra": "5.0.2", "@types/glob": "^5.0.35", @@ -6479,10 +6478,12 @@ "@types/marked": "^0.3.0", "@types/minimatch": "^3.0.3", "@types/moment-timezone": "^0.5.6", - "@types/node": "^7.0.66", + "@types/node": "^7.0.67", "@types/p-queue": "^2.3.1", "@types/tmp": "0.0.33", "@types/uuid": "^3.4.3", + "@types/ws": "^5.1.2", + "compare-versions": "^3.3.0", "fast-glob": "^2.2.2", "fs-extra": "^5.0.0", "glob": "^7.1.2", @@ -6498,7 +6499,8 @@ "node-enumerable": "^3.9.0", "p-queue": "^2.4.2", "tmp": "0.0.33", - "uuid": "^3.3.2" + "uuid": "^3.3.2", + "ws": "^5.2.1" }, "dependencies": { "@types/fs-extra": { diff --git a/package.json b/package.json index 9798b11..0320e4b 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,12 @@ "name": "vscode-deploy-reloaded", "displayName": "Deploy (Reloaded)", "description": "Deploys files of a workspace to a destination.", - "version": "0.83.0", + "version": "0.84.0", "publisher": "mkloubert", "engines": { - "vscode": "^1.24.0" + "vscode": "^1.25.0" }, "license": "LGPL-3.0", - "preview": true, "categories": [ "Azure", "Other" @@ -72,6 +71,11 @@ "title": "Pull ...", "category": "Deploy Reloaded" }, + { + "command": "extension.deploy.reloaded.showNotifications", + "title": "Show notification(s) ...", + "category": "Deploy Reloaded" + }, { "command": "extension.deploy.reloaded.switches", "title": "Switches ...", @@ -80631,7 +80635,7 @@ "tmp": "0.0.33", "uglify-js": "^3.4.2", "uuid": "^3.3.2", - "vscode-helpers": "^2.10.3", + "vscode-helpers": "^2.12.0", "ws": "^5.2.1" } } diff --git a/src/contracts.ts b/src/contracts.ts index 1e61380..48ee846 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -27,6 +27,20 @@ import * as deploy_values from './values'; import * as vscode from 'vscode'; +/** + * A message item with a value. + */ +export interface ActionMessageItem extends MessageItemWithValue { + /** + * The action. + * + * @param {ActionMessageItem} [item] The underlying item. + * + * @return {TResult} The result of the action. + */ + readonly action?: (item?: ActionMessageItem) => TResult; +} + /** * A quick pick item based on an action. */ @@ -480,24 +494,6 @@ export interface MessageItemWithValue extends vscode.MessageItem { readonly value?: TValue; } -/** - * Describes the structure of the package file of that extenstion. - */ -export interface PackageFile { - /** - * The display name. - */ - readonly displayName: string; - /** - * The (internal) name. - */ - readonly name: string; - /** - * The version string. - */ - readonly version: string; -} - /** * An object that is filtered by platform. */ diff --git a/src/devtools.ts b/src/devtools.ts deleted file mode 100644 index a650064..0000000 --- a/src/devtools.ts +++ /dev/null @@ -1,351 +0,0 @@ -/** - * This file is part of the vscode-deploy-reloaded distribution. - * Copyright (c) Marcel Joachim Kloubert. - * - * vscode-deploy-reloaded is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, version 3. - * - * vscode-deploy-reloaded is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -import * as _ from 'lodash'; -import * as deploy_contracts from './contracts'; -import * as deploy_helpers from './helpers'; -import * as vscode from 'vscode'; -import * as WebSocket from 'ws'; - -/** - * A browser item. - */ -export interface BrowserItem extends NodeJS.EventEmitter, vscode.Disposable { - /** - * Closes the connection to the item. - * - * @return {PromiseLike} The promise that indicates if operation was successful or not. - */ - readonly close: () => PromiseLike; - /** - * Options a connection to the item. - * - * @return {PromiseLike} The promise that indicates if operation was successful or not. - */ - readonly connect: () => PromiseLike; - /** - * The ID of the item. - */ - readonly id: string; - /** - * Indicates if a connection to the item has been established or not. - */ - readonly isConnected: boolean; - /** - * Invokes a method for the item. - * - * @param {string} method The method to invoke. - * @param {any} [params] Parameters for the method. - * @param {SendToBrowserItemCallback} [callback] The optional callback. - */ - readonly send: (method: string, params?: any, callback?: SendToBrowserItemCallback) => PromiseLike; - /** - * Gets the underyling (web) socket URI. - */ - readonly socketUri: string; -} - -/** - * A browser page. - */ -export interface BrowserPage extends BrowserItem { - /** - * The title of the page. - */ - readonly title: string; -} - -/** - * Options for a DevTools client. - */ -export interface DevToolsClientOptions { - /** - * The host address. - */ - readonly host?: string; - /** - * The TCP host. - */ - readonly port?: number; -} - -export type SendToBrowserItemCallback = (message: any) => any; - -/** - * A DevTools client. - */ -export class DevToolsClient extends deploy_helpers.DisposableBase { - /** - * Initializes a new instance of that class. - * - * @param {DevToolsClientOptions} [opts] Custom options. - */ - public constructor(opts?: DevToolsClientOptions) { - super(); - - this.options = opts || {}; - } - - private async getBrowserItems(): Promise { - const RESP = await deploy_helpers.GET(`http://${ this.host }:${ this.port }/json`); - - if (200 !== RESP.code) { - throw new Error(`Unexpected response ${ RESP.code }: '${ RESP.status }'`); - } - - return deploy_helpers.asArray( - JSON.parse( - (await RESP.readBody()).toString('utf8') - ) - ).filter(i => { - return !deploy_helpers.isEmptyString( i['webSocketDebuggerUrl'] ); - }); - } - - /** - * Returns a list of all pages. - * - * @return {Promise} The promise with the pages. - */ - public async getPages() { - const PAGES: BrowserPage[] = []; - - const PAGE_ITEMS = ( - await this.getBrowserItems() - ).filter(i => { - return 'page' === deploy_helpers.normalizeString(i['type']); - }); - - for (const PI of PAGE_ITEMS) { - const NEW_PAGE = new BrowserPageImpl(this); - NEW_PAGE.id = deploy_helpers.toStringSafe( PI['id'] ); - NEW_PAGE.title = deploy_helpers.toStringSafe( PI['title'] ); - NEW_PAGE.socketUri = deploy_helpers.toStringSafe( PI['webSocketDebuggerUrl'] ); - - PAGES.push( NEW_PAGE ); - } - - return PAGES; - } - - /** - * Gets the host address. - */ - public get host() { - let hostAddr = deploy_helpers.toStringSafe(this.options.host); - if ('' === hostAddr) { - hostAddr = deploy_contracts.DEFAULT_HOST; - } - - return hostAddr; - } - - /** - * Gets the options for the client. - */ - public readonly options: DevToolsClientOptions; - - /** - * Gets the TCP port. - */ - public get port() { - let tcpPort = parseInt( - deploy_helpers.toStringSafe(this.options.port).trim() - ); - if (isNaN(tcpPort)) { - tcpPort = 9222; - } - - return tcpPort; - } -} - -abstract class BrowserItemBase extends deploy_helpers.DisposableBase implements BrowserItem { - private _nextId = 0; - private _sendCallbacks: { [id: number]: SendToBrowserItemCallback }; - private _socket: WebSocket; - - public constructor(public readonly client: DevToolsClient) { - super(); - } - - public close() { - return new Promise((resolve, reject) => { - const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); - - const CUR_SOCKET = this._socket; - if (_.isNil(CUR_SOCKET)) { - COMPLETED(null, false); - return; - } - - try { - CUR_SOCKET.close(); - deploy_helpers.tryRemoveAllListeners(CUR_SOCKET); - - this._socket = null; - - COMPLETED(null); - } catch (e) { - COMPLETED(e); - } - }); - } - - public connect() { - return new Promise((resolve, reject) => { - const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); - - if (this.isInFinalizeState) { - COMPLETED( - new Error('Object is or is going to be disposed') - ); - return; - } - - if (!_.isNil(this._socket)) { - COMPLETED(null, false); - return; - } - - try { - const NEW_SOCKET = new WebSocket( this.socketUri ); - - NEW_SOCKET.once('error', (err) => { - if (err) { - COMPLETED(err); - } - }); - - NEW_SOCKET.once('close', () => { - this._socket = null; - - this.emit('close', - NEW_SOCKET); - }); - - NEW_SOCKET.once('open', () => { - this._sendCallbacks = {}; - this._socket = NEW_SOCKET; - - COMPLETED(null, true); - }); - - NEW_SOCKET.on('message', (data) => { - const ALL_CALLBACKS = this._sendCallbacks; - if (!_.isNil(ALL_CALLBACKS)) { - try { - let msg: any; - if (!_.isNil(data)) { - msg = JSON.parse( - deploy_helpers.toStringSafe(data) - ); - } - - if (_.isObject(msg)) { - const MSG_ID = parseInt( - deploy_helpers.toStringSafe(msg.id).trim() - ); - if (!isNaN(MSG_ID)) { - const DELETE_CALLBACK = (err?: any) => { - delete ALL_CALLBACKS[MSG_ID]; - }; - - try { - const CALLBACK = ALL_CALLBACKS[MSG_ID]; - if (!_.isNil(CALLBACK)) { - Promise.resolve( - CALLBACK(msg) - ).then(() => { - DELETE_CALLBACK(); - }, (err) => { - DELETE_CALLBACK(err); - }); - } - } finally { - DELETE_CALLBACK(); - } - } - } - } catch { } - } - - this.emit('message', - NEW_SOCKET, data); - }); - } catch (e) { - COMPLETED(e); - } - }); - } - - public id: string; - - public get isConnected() { - return !_.isNil(this._socket); - } - - public onDispose() { - const CUR_SOCKET = this._socket; - if (!_.isNil(CUR_SOCKET)) { - CUR_SOCKET.close(); - deploy_helpers.tryRemoveAllListeners(CUR_SOCKET); - - this._socket = null; - } - - this._sendCallbacks = null; - } - - public send(method: string, params?: any, callback?: SendToBrowserItemCallback) { - method = deploy_helpers.toStringSafe(method).trim(); - - return new Promise((resolve, reject) => { - const COMPLETED = deploy_helpers.createCompletedAction(resolve, reject); - - let id = 0; - try { - id = ++this._nextId; - - if (!_.isNil(callback)) { - this._sendCallbacks[id] = callback; - } - - this._socket.send( - JSON.stringify({ - id: id, - method: method, - params: params, - }), - (err) => { - COMPLETED(err); - } - ); - } catch (e) { - delete this._sendCallbacks[id]; - - COMPLETED(e); - } - }); - } - - public socketUri: string; -} - -class BrowserPageImpl extends BrowserItemBase implements BrowserPage { - public title: string; -} diff --git a/src/extension.ts b/src/extension.ts index 6c75392..8eb7a30 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,6 +28,7 @@ import * as deploy_gui from './gui'; import * as deploy_helpers from './helpers'; import * as deploy_html from './html'; import * as deploy_log from './log'; +import * as deploy_notifications from './notifications'; import * as deploy_packages from './packages'; import * as deploy_plugins from './plugins'; import * as deploy_proxies from './proxies'; @@ -55,7 +56,7 @@ let currentContext: vscode.ExtensionContext; let isDeactivating = false; let nextWorkspaceId = Number.MAX_SAFE_INTEGER; let outputChannel: vscode.OutputChannel; -let packageFile: deploy_contracts.PackageFile; +let packageFile: deploy_helpers.PackageFile; const PLUGINS: deploy_plugins.Plugin[] = []; let selectWorkspaceBtn: vscode.StatusBarItem; const WORKSPACE_COMMANDS: deploy_commands.WorkspaceCommandRepository = {}; @@ -528,6 +529,11 @@ export async function activate(context: vscode.ExtensionContext) { async function activateExtension(context: vscode.ExtensionContext) { const WF = deploy_helpers.buildWorkflow(); + // extension's root directory + WF.next(() => { + deploy_helpers.setExtensionRoot(__dirname); + }); + WF.next(() => { currentContext = context; }); @@ -618,12 +624,7 @@ async function activateExtension(context: vscode.ExtensionContext) { // package file WF.next(async () => { try { - const CUR_DIR = __dirname; - const FILE_PATH = Path.join(CUR_DIR, '../package.json'); - - packageFile = JSON.parse( - (await deploy_helpers.readFile(FILE_PATH)).toString('utf8') - ); + packageFile = await deploy_helpers.getPackageFile(); } catch (e) { deploy_log.CONSOLE @@ -957,6 +958,7 @@ async function activateExtension(context: vscode.ExtensionContext) { deploy_pull.registerPullCommands(context); deploy_delete.registerDeleteCommands(context); deploy_tools_send_file.registerSendFileCommands(context); + deploy_notifications.registerNotificationCommands(context, packageFile); }); // HTML document provider @@ -1201,6 +1203,22 @@ async function activateExtension(context: vscode.ExtensionContext) { outputChannel.appendLine(''); }); + // notifications + WF.next(() => { + try { + deploy_notifications.showExtensionNotifications( + context, packageFile, + ).then(() => { + }, (err) => { + deploy_log.CONSOLE + .trace(err, 'extension.notifications(2)'); + }); + } catch (e) { + deploy_log.CONSOLE + .trace(e, 'extension.notifications(1)'); + } + }); + if (!isDeactivating) { await WF.start(); } diff --git a/src/i18.ts b/src/i18.ts index 82957b6..bc31e77 100644 --- a/src/i18.ts +++ b/src/i18.ts @@ -351,6 +351,12 @@ export interface Translation { notFound?: { dir?: string; }; + notifications?: { + defaultName?: string; + loading?: string; + noneFound?: string; + selectNotifications?: string; + }; ok?: string; output?: { open?: string; @@ -635,7 +641,9 @@ export interface Translation { waitingForOther?: string; }; time?: { - dateTimeWithSeconds?: string; + date?: string; + dateTime?: string; + dateTimeWithSeconds?: string; timeWithSeconds?: string; }; tools?: { diff --git a/src/lang/de.ts b/src/lang/de.ts index ec25e1d..1a86b92 100644 --- a/src/lang/de.ts +++ b/src/lang/de.ts @@ -346,6 +346,12 @@ export const translation: Translation = { notFound: { dir: "Das Verzeichnis{0:trim,surround,leading_space} wurde nicht gefunden!", }, + notifications: { + defaultName: "Benachrichtigung #{0:trim}", + loading: "Lade Benachrichtigungen ...", + noneFound: "Keine Benachrichtigungen gefunden!", + selectNotifications: "Wählen Sie eine oder mehrere Benachrichtigungen aus ...", + }, output: { open: "Ausgabe öffnen", }, @@ -631,7 +637,9 @@ export const translation: Translation = { waitingForOther: "Warte auf{0:trim,surround,leading_space} ...", }, time: { - dateTimeWithSeconds: "DD.MM.YYYY HH:mm:ss", + date: "DD.MM.YYYY", + dateTime: "DD.MM.YYYY HH:mm", + dateTimeWithSeconds: "DD.MM.YYYY HH:mm:ss", }, tools: { bower: { diff --git a/src/lang/en.ts b/src/lang/en.ts index 4ba174b..4bb0ab3 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -346,6 +346,12 @@ export const translation: Translation = { notFound: { dir: "Directory{0:trim,surround,leading_space} not found!", }, + notifications: { + defaultName: "Notification #{0:trim}", + loading: "Loading notifications ...", + noneFound: "No notifications found!", + selectNotifications: "Select one or more notifications ...", + }, ok: 'OK', output: { open: "Open output", @@ -632,6 +638,8 @@ export const translation: Translation = { waitingForOther: "Wating for{0:trim,surround,leading_space} ...", }, time: { + date: "YYYY-MM-DD", + dateTime: "YYYY-MM-DD HH:mm", dateTimeWithSeconds: "YYYY-MM-DD HH:mm:ss", timeWithSeconds: "HH:mm:ss", }, diff --git a/src/notifications.ts b/src/notifications.ts new file mode 100644 index 0000000..58a658b --- /dev/null +++ b/src/notifications.ts @@ -0,0 +1,363 @@ +/** + * This file is part of the vscode-deploy-reloaded distribution. + * Copyright (c) Marcel Joachim Kloubert. + * + * vscode-deploy-reloaded is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, version 3. + * + * vscode-deploy-reloaded is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +import * as _ from 'lodash'; +import * as deploy_contracts from './contracts'; +import * as deploy_helpers from './helpers'; +import * as deploy_log from './log'; +import * as i18 from './i18'; +import * as Moment from 'moment'; +import * as vscode from 'vscode'; + + +interface DisplayNotificationOptions { + note: deploy_helpers.ExtensionNotification; + onOKClick?: () => any; + onLinkClick?: () => any; + onRemindMeLaterClick?: () => any; +} + +type ReadNotifications = { [id: string]: boolean }; + + +const KEY_READ_NOTIFICATIONS = 'vscdrReadNotifications'; + + +async function displayNotification(opts?: DisplayNotificationOptions) { + if (_.isNil(opts)) { + opts = {}; + } + + const NOTE = opts.note; + if (_.isNil(NOTE)) { + return; + } + + const CONTENT = deploy_helpers.toStringSafe( + NOTE.content + ).trim(); + if ('' === CONTENT) { + return; + } + + const MESSAGE_ITEMS: deploy_contracts.ActionMessageItem[] = []; + + if (!_.isNil(NOTE.link)) { + const LINK = deploy_helpers.toStringSafe(NOTE.link.href).trim(); + if ('' !== LINK) { + let linkText = deploy_helpers.toStringSafe(NOTE.link.text).trim(); + if ('' === linkText) { + linkText = 'Open link ...'; + } + + MESSAGE_ITEMS.push({ + action: async () => { + deploy_helpers.open(LINK); + + if (opts.onLinkClick) { + await Promise.resolve( + opts.onLinkClick() + ); + } + }, + title: linkText, + }); + } + } + + MESSAGE_ITEMS.push({ + action: async () => { + if (opts.onOKClick) { + await Promise.resolve( + opts.onOKClick() + ); + } + }, + isCloseAffordance: true, + title: "OK", + }); + + MESSAGE_ITEMS.push({ + action: async () => { + if (opts.onRemindMeLaterClick) { + await Promise.resolve( + opts.onRemindMeLaterClick() + ); + } + }, + title: 'Remind me later', + }); + + let popupFunc: Function; + + const TYPE = deploy_helpers.normalizeString(NOTE.type); + switch (TYPE) { + case 'e': + case 'emerg': + case 'emergency': + popupFunc = vscode.window.showErrorMessage; + break; + + case 'important': + case 'w': + case 'warn': + case 'warning': + popupFunc = vscode.window.showWarningMessage; + break; + + default: + popupFunc = vscode.window.showInformationMessage; + break; + } + + const SELECTED_ITEM: deploy_contracts.ActionMessageItem = await popupFunc.apply( + null, + [ CONTENT ].concat( MESSAGE_ITEMS ) + ); + + if (SELECTED_ITEM) { + if (SELECTED_ITEM.action) { + await Promise.resolve( + SELECTED_ITEM.action( + SELECTED_ITEM + ) + ); + } + } +} + +async function loadNotifications(packageFile?: deploy_helpers.PackageFile) { + return deploy_helpers.filterExtensionNotifications( + deploy_helpers.from( + await deploy_helpers.getExtensionNotifications('https://mkloubert.github.io/notifications/vscode-deploy-reloaded.json') + ).orderBy(x => { + try { + const NOTE_TIME = deploy_helpers.toStringSafe(x.time).trim(); + if ('' !== NOTE_TIME) { + const TIME = Moment.utc(NOTE_TIME); + if (TIME.isValid()) { + return TIME.unix(); + } + } + } catch { } + + return Number.MIN_SAFE_INTEGER; + }).toArray(), + { + version: packageFile ? packageFile.version + : undefined, + }); +} + +/** + * Shows the notifications for that extension. + * + * @param {vscode.ExtensionContext} context The extension context. + * @param {deploy_helpers.PackageFile} [packageFile] The underlying package file. + * @param {deploy_helpers.ExtensionNotification|deploy_helpers.ExtensionNotification[]} [notifications] Custom list of notifications. + */ +export async function showExtensionNotifications( + context: vscode.ExtensionContext, + packageFile?: deploy_helpers.PackageFile, + notifications?: deploy_helpers.ExtensionNotification | deploy_helpers.ExtensionNotification[], +) { + if (arguments.length < 3) { + notifications = await loadNotifications(packageFile); + } else { + notifications = deploy_helpers.asArray(notifications); + } + + await withReadNotificationsStorage(context, async (readNotifications) => { + const GET_NOTE_ID = (note: deploy_helpers.ExtensionNotification) => { + if (!_.isNil(note)) { + return deploy_helpers.normalizeString( note.id ); + } + }; + + // cleanups + _.forIn(readNotifications, (value, key) => { + const ID = deploy_helpers.normalizeString( key ); + + const EXISTS = deploy_helpers.from( + notifications + ).any(n => { + return GET_NOTE_ID(n) === ID; + }); + + if (!EXISTS) { + delete readNotifications[key]; + } + }); + + for (const NOTE of notifications) { + try { + const NOTE_ID = GET_NOTE_ID( NOTE ); + if ('' === NOTE_ID) { + continue; + } + + if (true === readNotifications[NOTE_ID]) { + continue; + } + + await displayNotification({ + note: NOTE, + onLinkClick: () => { + readNotifications[NOTE_ID] = true; + }, + onOKClick: () => { + readNotifications[NOTE_ID] = true; + }, + onRemindMeLaterClick: () => { + delete readNotifications[NOTE_ID]; + } + }); + } catch { } + } + }); + +} + +/** + * Registers notification commands. + * + * @param {vscode.ExtensionContext} context The extension's context. + * @param {deploy_helpers.PackageFile} packageFile The package file meta data. + */ +export function registerNotificationCommands( + context: vscode.ExtensionContext, + packageFile: deploy_helpers.PackageFile, +) { + context.subscriptions.push( + // show notification + vscode.commands.registerCommand('extension.deploy.reloaded.showNotifications', async () => { + try { + let hasCancelled = false; + + const QUICK_PICKS: deploy_contracts.ActionQuickPick[] = await vscode.window.withProgress({ + cancellable: true, + location: vscode.ProgressLocation.Notification, + title: i18.t('notifications.loading'), + }, async (progress, cancelToken) => { + try { + let i = 0; + + return ( + deploy_helpers.from( + await loadNotifications(packageFile) + ) + ).where(n => { + return !deploy_helpers.isEmptyString(n.content); + }).reverse().select((n) => { + let label = deploy_helpers.toStringSafe(n.title).trim(); + if ('' === label) { + label = i18.t('notifications.defaultName', + i + 1); + } + + let detail: string; + try { + const NOTE_TIME = deploy_helpers.toStringSafe(n.time); + if ('' !== NOTE_TIME) { + const TIME = deploy_helpers.asLocalTime( + Moment.utc(NOTE_TIME) + ); + + if (TIME.isValid()) { + detail = TIME.format( i18.t('time.dateTime') ); + } + } + } catch { } + + return { + action: async () => { + await displayNotification({ + note: n, + }); + }, + label: label, + detail: detail, + }; + }).toArray(); + } finally { + hasCancelled = cancelToken.isCancellationRequested; + } + }); + + if (hasCancelled) { + return; + } + + if (QUICK_PICKS.length < 1) { + vscode.window.showWarningMessage( + i18.t('notifications.noneFound') + ); + + return; + } + + const SELECTED_ITEMS = deploy_helpers.asArray( + await vscode.window.showQuickPick( + QUICK_PICKS, { + canPickMany: true, + placeHolder: i18.t('notifications.selectNotifications'), + } + ) + ); + + for (const SI of SELECTED_ITEMS) { + await SI.action(); + } + } catch (e) { + deploy_log.CONSOLE + .trace(e, 'extension.deploy.reloaded.showNotification'); + + deploy_helpers.showErrorMessage( + i18.t('tools.errors.operationFailed') + ); + } + }), + ); +} + +async function withReadNotificationsStorage( + context: vscode.ExtensionContext, + action: (readNotifications: ReadNotifications) => TResult +) { + let readNotifications: ReadNotifications | false; + try { + readNotifications = context.globalState + .get(KEY_READ_NOTIFICATIONS, null); + } catch { + readNotifications = false; + } + + if (!readNotifications) { + readNotifications = {}; + } + + try { + if (action) { + return await Promise.resolve( + action(readNotifications) + ); + } + } finally { + await context.globalState + .update(KEY_READ_NOTIFICATIONS, readNotifications); + } +} diff --git a/src/packages.ts b/src/packages.ts index 3afce69..fad6e4e 100644 --- a/src/packages.ts +++ b/src/packages.ts @@ -30,7 +30,6 @@ import * as Enumerable from 'node-enumerable'; import * as i18 from './i18'; import * as Moment from 'moment'; import * as Path from 'path'; -import * as UUID from 'uuid'; import * as vscode from 'vscode'; @@ -581,9 +580,9 @@ export function getTargetsOfPackage(pkg: Package, targetResolver: deploy_targets targets = ME.getTargets(); } else if (targets.length > 1) { - const ID = `${pkg.__id}\n` + - `${UUID.v4()}\n` + - `${Moment.utc().unix()}`; + const ID = `${ pkg.__id }\n` + + `${ deploy_helpers.uuid() }\n` + + `${ Moment.utc().unix() }`; const BATCH_TARGET = { __cache: new deploy_helpers.MemoryCache(), diff --git a/src/targets.ts b/src/targets.ts index 7388422..d4c5a37 100644 --- a/src/targets.ts +++ b/src/targets.ts @@ -41,7 +41,6 @@ import * as Minimatch from 'minimatch'; import * as Moment from 'moment'; import * as Path from 'path'; import * as SanitizeFilename from 'sanitize-filename'; -import * as UUID from 'uuid'; import * as vscode from 'vscode'; @@ -375,9 +374,9 @@ export function createTargetSessionValue(target: Target): symbol { } return Symbol( - `${Moment.utc().unix()}::` + - `${getTargetIdHash(target)}::` + - `${UUID.v4()}`, + `${ Moment.utc().unix() }::` + + `${ getTargetIdHash(target) }::` + + `${ deploy_helpers.uuid() }`, ); } diff --git a/src/targets/operations/devtools.ts b/src/targets/operations/devtools.ts index 9c676dc..c98bcb8 100644 --- a/src/targets/operations/devtools.ts +++ b/src/targets/operations/devtools.ts @@ -17,7 +17,6 @@ import * as _ from 'lodash'; import * as deploy_contracts from '../../contracts'; -import * as deploy_devtools from '../../devtools'; import * as deploy_helpers from '../../helpers'; import * as deploy_targets from '../../targets'; import * as vscode from 'vscode'; @@ -67,7 +66,7 @@ export async function execute(context: deploy_targets.TargetOperationExecutionCo const ALWAYS_ASK_FOR_PAGE = deploy_helpers.toBooleanSafe(OPERATION.alwaysAskForPage); const PAGES = deploy_helpers.toStringSafe( OPERATION.pages ); - let sendCallback: deploy_devtools.SendToBrowserItemCallback; + let sendCallback: deploy_helpers.SendToBrowserItemCallback; if (deploy_helpers.toBooleanSafe(OPERATION.debug, true)) { sendCallback = async (msg) => { if (_.isNil(msg)) { @@ -116,7 +115,7 @@ export async function execute(context: deploy_targets.TargetOperationExecutionCo pageFilter = new RegExp(PAGES, 'i'); } - const CLIENT = new deploy_devtools.DevToolsClient({ + const CLIENT = deploy_helpers.createDevToolsClient({ host: OPERATION.host, port: OPERATION.port, }); @@ -134,6 +133,8 @@ export async function execute(context: deploy_targets.TargetOperationExecutionCo i + 1); } + let description = deploy_helpers.toStringSafe(p.description).trim(); + return { action: async () => { if (!(await p.connect())) { @@ -154,6 +155,7 @@ export async function execute(context: deploy_targets.TargetOperationExecutionCo } catch { } } }, + description: description, detail: p.socketUri, label: title, state: p, diff --git a/src/tools/quickexecution.ts b/src/tools/quickexecution.ts index 50dfdfd..5ddddd6 100644 --- a/src/tools/quickexecution.ts +++ b/src/tools/quickexecution.ts @@ -392,49 +392,15 @@ export async function _1b87f2ee_b636_45b6_807c_0e2d25384b02_1409614337( // tslint:disable-next-line:no-unused-variable const $guid = async (ver?: string, ...guidArgs: any[]) => { - const UUID = require('uuid'); - ver = $h.normalizeString( await $unwrap(ver) ); - guidArgs = $h.normalizeString( + guidArgs = $h.asArray( await $unwrap(guidArgs) ); - const ARGS = []; - if (guidArgs) { - for (const A of guidArgs) { - ARGS.push( - await $unwrap(A) - ); - } - } - - let func: (...a: any[]) => string; - switch (ver) { - case '1': - case 'v1': - func = UUID.v1; - break; - - case '': - case '4': - case 'v4': - func = UUID.v4; - break; - - case '5': - case 'v5': - func = UUID.v5; - break; - } - - if (!func) { - throw new Error($i18.t('tools.quickExecution.uuid.notSupported', - ver)); - } - - return func.apply(null, ARGS); + return $h.guid + .apply(null, [ ver ].concat( guidArgs )); }; // tslint:disable-next-line:no-unused-variable