From f3a3e9f75ab6c09065ef7d6e3f97bbcf7c2a81cb Mon Sep 17 00:00:00 2001
From: "Brandon Waterloo [MSFT]"
<36966225+bwateratmsft@users.noreply.github.com>
Date: Thu, 24 Oct 2019 14:20:36 -0400
Subject: [PATCH] Cherry pick several things for 0.8.2 release (#1367)
* Update package
* Cherry pick clearer error message for duplicate registry
* Cherry pick improved Dockerfile pattern matches, save event telemetry
* Cherry pick only refresh if VSCode is in focus
* Cherry pick add a command to create the simplest network
* Cherry pick ask some active users to take NPS survey
---
package-lock.json | 19 +++--
package.json | 27 +++++--
resources/dark/add.svg | 3 +
resources/light/add.svg | 3 +
src/commands/networks/createNetwork.ts | 33 +++++++++
src/commands/registerCommands.ts | 2 +
src/extension.ts | 8 +++
src/registerListeners.ts | 24 +++++++
src/tree/LocalRootTreeItemBase.ts | 6 +-
src/tree/registries/RegistriesTreeItem.ts | 6 +-
src/utils/nps.ts | 85 +++++++++++++++++++++++
11 files changed, 204 insertions(+), 12 deletions(-)
create mode 100644 resources/dark/add.svg
create mode 100644 resources/light/add.svg
create mode 100644 src/commands/networks/createNetwork.ts
create mode 100644 src/registerListeners.ts
create mode 100644 src/utils/nps.ts
diff --git a/package-lock.json b/package-lock.json
index ee4f096834..a8825f91e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4503,13 +4503,24 @@
"dev": true
},
"https-proxy-agent": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
- "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.3.tgz",
+ "integrity": "sha512-Ytgnz23gm2DVftnzqRRz2dOXZbGd2uiajSw/95bPp6v53zPRspQjLm/AfBgqbJ2qfeRXWIOMVLpp86+/5yX39Q==",
"dev": true,
"requires": {
- "agent-base": "^4.1.0",
+ "agent-base": "^4.3.0",
"debug": "^3.1.0"
+ },
+ "dependencies": {
+ "agent-base": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+ "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+ "dev": true,
+ "requires": {
+ "es6-promisify": "^5.0.0"
+ }
+ }
}
},
"ieee754": {
diff --git a/package.json b/package.json
index 2a5aba88c6..7d58c0b122 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"onCommand:vscode-docker.images.runInteractive",
"onCommand:vscode-docker.images.tag",
"onCommand:vscode-docker.networks.configureExplorer",
+ "onCommand:vscode-docker.networks.create",
"onCommand:vscode-docker.networks.inspect",
"onCommand:vscode-docker.networks.prune",
"onCommand:vscode-docker.networks.refresh",
@@ -213,10 +214,15 @@
"group": "navigation@9"
},
{
- "command": "vscode-docker.networks.prune",
+ "command": "vscode-docker.networks.create",
"when": "view == dockerNetworks",
"group": "navigation@1"
},
+ {
+ "command": "vscode-docker.networks.prune",
+ "when": "view == dockerNetworks",
+ "group": "navigation@2"
+ },
{
"command": "vscode-docker.networks.refresh",
"when": "view == dockerNetworks",
@@ -684,7 +690,11 @@
],
"filenamePatterns": [
"*.dockerfile",
- "Dockerfile"
+ "Dockerfile",
+ "Dockerfile.debug",
+ "Dockerfile.dev",
+ "Dockerfile.develop",
+ "Dockerfile.prod"
]
},
{
@@ -704,8 +714,8 @@
},
"docker.explorerRefreshInterval": {
"type": "number",
- "default": 1000,
- "description": "Explorer refresh interval, default is 1000ms"
+ "default": 2000,
+ "description": "Explorer refresh interval, default is 2000ms"
},
"docker.containers.groupBy": {
"type": "string",
@@ -1291,6 +1301,15 @@
"dark": "resources/dark/settings.svg"
}
},
+ {
+ "command": "vscode-docker.networks.create",
+ "title": "Create...",
+ "category": "Docker Networks",
+ "icon": {
+ "light": "resources/light/add.svg",
+ "dark": "resources/dark/add.svg"
+ }
+ },
{
"command": "vscode-docker.networks.inspect",
"title": "Inspect",
diff --git a/resources/dark/add.svg b/resources/dark/add.svg
new file mode 100644
index 0000000000..4d9389336b
--- /dev/null
+++ b/resources/dark/add.svg
@@ -0,0 +1,3 @@
+
diff --git a/resources/light/add.svg b/resources/light/add.svg
new file mode 100644
index 0000000000..01a9de7d5a
--- /dev/null
+++ b/resources/light/add.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/commands/networks/createNetwork.ts b/src/commands/networks/createNetwork.ts
new file mode 100644
index 0000000000..5deeb8bc7c
--- /dev/null
+++ b/src/commands/networks/createNetwork.ts
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See LICENSE.md in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { window } from 'vscode';
+import { IActionContext } from 'vscode-azureextensionui';
+import { ext } from '../../extensionVariables';
+
+export async function createNetwork(_context: IActionContext): Promise {
+
+ const name = await ext.ui.showInputBox({
+ value: '',
+ prompt: 'Name of the network'
+ });
+
+ const driverSelection = await ext.ui.showQuickPick(
+ [
+ { label: 'bridge' },
+ { label: 'host' },
+ { label: 'overlay' },
+ { label: 'macvlan' }
+ ],
+ {
+ canPickMany: false,
+ placeHolder: 'Select the network driver to use (default is "bridge").'
+ }
+ );
+
+ const result = <{ id: string }>await ext.dockerode.createNetwork({ Name: name, Driver: driverSelection.label });
+
+ window.showInformationMessage(`Network Created with ID ${result.id.substr(0, 12)}`);
+}
diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts
index cc40d55b90..a60a5eb110 100644
--- a/src/commands/registerCommands.ts
+++ b/src/commands/registerCommands.ts
@@ -26,6 +26,7 @@ import { runAzureCliImage } from "./images/runAzureCliImage";
import { runImage, runImageInteractive } from "./images/runImage";
import { tagImage } from "./images/tagImage";
import { configureNetworksExplorer } from "./networks/configureNetworksExplorer";
+import { createNetwork } from "./networks/createNetwork";
import { inspectNetwork } from "./networks/inspectNetwork";
import { pruneNetworks } from "./networks/pruneNetworks";
import { removeNetwork } from "./networks/removeNetwork";
@@ -86,6 +87,7 @@ export function registerCommands(): void {
registerCommand('vscode-docker.images.tag', tagImage);
registerCommand('vscode-docker.networks.configureExplorer', configureNetworksExplorer);
+ registerCommand('vscode-docker.networks.create', createNetwork);
registerCommand('vscode-docker.networks.inspect', inspectNetwork);
registerCommand('vscode-docker.networks.remove', removeNetwork);
registerCommand('vscode-docker.networks.prune', pruneNetworks);
diff --git a/src/extension.ts b/src/extension.ts
index ab100df324..8935b48f46 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -24,11 +24,13 @@ import composeVersionKeys from './dockerCompose/dockerComposeKeyInfo';
import { DockerComposeParser } from './dockerCompose/dockerComposeParser';
import { DockerfileCompletionItemProvider } from './dockerfileCompletionItemProvider';
import { ext } from './extensionVariables';
+import { registerListeners } from './registerListeners';
import { registerTrees } from './tree/registerTrees';
import { addDockerSettingsToEnv } from './utils/addDockerSettingsToEnv';
import { addUserAgent } from './utils/addUserAgent';
import { getTrustedCertificates } from './utils/getTrustedCertificates';
import { Keytar } from './utils/keytar';
+import { nps } from './utils/nps';
import { DefaultTerminalProvider } from './utils/TerminalProvider';
import { wrapError } from './utils/wrapError';
@@ -122,6 +124,12 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats:
await consolidateDefaultRegistrySettings();
activateLanguageClient();
+
+ registerListeners(ctx);
+
+ // Don't wait
+ // tslint:disable-next-line: no-floating-promises
+ nps(ctx.globalState);
});
}
diff --git a/src/registerListeners.ts b/src/registerListeners.ts
new file mode 100644
index 0000000000..005cf265fc
--- /dev/null
+++ b/src/registerListeners.ts
@@ -0,0 +1,24 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See LICENSE.md in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { ExtensionContext, TextDocument, workspace } from 'vscode';
+import { ext } from './extensionVariables';
+
+let lastUploadTime: number = 0;
+const hourInMilliseconds = 1000 * 60 * 60;
+
+export function registerListeners(ctx: ExtensionContext): void {
+ ctx.subscriptions.push(workspace.onDidSaveTextDocument(onDidSaveTextDocument));
+}
+
+function onDidSaveTextDocument(doc: TextDocument): void {
+ // If it's not a Dockerfile, or last upload time is within an hour, skip
+ if (doc.languageId !== 'dockerfile' || lastUploadTime + hourInMilliseconds > Date.now()) {
+ return;
+ }
+
+ lastUploadTime = Date.now();
+ ext.reporter.sendTelemetryEvent('dockerfilesave', { "lineCount": doc.lineCount.toString() }, {});
+}
diff --git a/src/tree/LocalRootTreeItemBase.ts b/src/tree/LocalRootTreeItemBase.ts
index bb44a6de1a..c87a843fcf 100644
--- a/src/tree/LocalRootTreeItemBase.ts
+++ b/src/tree/LocalRootTreeItemBase.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { ConfigurationChangeEvent, ConfigurationTarget, TreeView, TreeViewVisibilityChangeEvent, workspace, WorkspaceConfiguration } from "vscode";
+import { ConfigurationChangeEvent, ConfigurationTarget, TreeView, TreeViewVisibilityChangeEvent, window, workspace, WorkspaceConfiguration } from "vscode";
import { AzExtParentTreeItem, AzExtTreeItem, AzureWizard, GenericTreeItem, IActionContext, InvalidTreeItem, registerEvent } from "vscode-azureextensionui";
import { configPrefix } from "../constants";
import { ext } from "../extensionVariables";
@@ -74,10 +74,10 @@ export abstract class LocalRootTreeItemBase('explorerRefreshInterval', 1000);
+ const refreshInterval: number = configOptions.get('explorerRefreshInterval', 2000);
intervalId = setInterval(
async () => {
- if (await this.hasChanged()) {
+ if (window.state.focused && await this.hasChanged()) {
await this.refresh();
}
},
diff --git a/src/tree/registries/RegistriesTreeItem.ts b/src/tree/registries/RegistriesTreeItem.ts
index c6a9defd02..cd1cc5fecc 100644
--- a/src/tree/registries/RegistriesTreeItem.ts
+++ b/src/tree/registries/RegistriesTreeItem.ts
@@ -98,7 +98,11 @@ export class RegistriesTreeItem extends AzExtParentTreeItem {
context.telemetry.properties.cancelStep = 'learnHowToContribute';
throw new UserCancelledError();
} else if (provider.onlyOneAllowed && this._cachedProviders.find(c => c.id === provider.id)) {
- throw new Error(`Only one provider with id "${provider.id}" is allowed at a time.`);
+ // Don't wait, no input to wait for anyway
+ // tslint:disable-next-line: no-floating-promises
+ ext.ui.showWarningMessage(`The "${provider.label}" registry provider is already connected.`);
+ context.telemetry.properties.cancelStep = 'registryProviderAlreadyAdded';
+ throw new UserCancelledError();
}
context.telemetry.properties.providerId = provider.id;
diff --git a/src/utils/nps.ts b/src/utils/nps.ts
new file mode 100644
index 0000000000..b793080891
--- /dev/null
+++ b/src/utils/nps.ts
@@ -0,0 +1,85 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.md in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// Loosely adapted from https://github.com/microsoft/vscode-azure-account/blob/2f497562cab5f3db09f983ab5101040f27dceb70/src/nps.ts
+
+import { env, Memento, Uri, window } from "vscode";
+import { ext } from "vscode-azureappservice/out/src/extensionVariables";
+
+const PROBABILITY = 0.15;
+const MIN_SESSION_COUNT = 10;
+
+const SURVEY_NAME = 'nps1';
+const SURVEY_URL = 'https://aka.ms/vscodedockernpsinproduct';
+
+const SESSION_COUNT_KEY = `${SURVEY_NAME}/sessioncount`;
+const LAST_SESSION_DATE_KEY = `${SURVEY_NAME}/lastsessiondate`;
+const IS_CANDIDATE_KEY = `${SURVEY_NAME}/iscandidate`;
+
+export async function nps(globalState: Memento): Promise {
+ try {
+ // If not English-language, don't ask
+ if (env.language !== 'en' && !env.language.startsWith('en-')) {
+ return;
+ }
+
+ let isCandidate: boolean | undefined = globalState.get(IS_CANDIDATE_KEY);
+
+ // If not a candidate, don't ask
+ if (isCandidate === false) {
+ return;
+ }
+
+ const date = new Date().toDateString();
+ const lastSessionDate = globalState.get(LAST_SESSION_DATE_KEY, new Date(0).toDateString());
+
+ // If this session is on same date as last session, don't count it
+ if (date === lastSessionDate) {
+ return;
+ }
+
+ // Count this session
+ const sessionCount = globalState.get(SESSION_COUNT_KEY, 0) + 1;
+ await globalState.update(LAST_SESSION_DATE_KEY, date);
+ await globalState.update(SESSION_COUNT_KEY, sessionCount);
+
+ // If under the MIN_SESSION_COUNT, don't ask
+ if (sessionCount < MIN_SESSION_COUNT) {
+ return;
+ }
+
+ // Decide if they are a candidate (if we previously decided they are and they did Remind Me Later, we will not do probability again)
+ // i.e. Probability only comes into play if isCandidate is undefined
+ // tslint:disable-next-line: insecure-random
+ isCandidate = isCandidate || Math.random() < PROBABILITY;
+ await globalState.update(IS_CANDIDATE_KEY, isCandidate);
+
+ // If not a candidate, don't ask
+ if (!isCandidate) {
+ return;
+ }
+
+ const take = { title: 'Take Survey', telName: 'take' };
+ const remind = { title: 'Remind Me Later', telName: 'remind' };
+ const never = { title: 'Don\'t Show Again', telName: 'never' };
+
+ // Prompt, treating hitting X as Remind Me Later
+ const result = (await window.showInformationMessage('Do you mind taking a quick feedback survey about the Docker Extension for VS Code?', take, remind, never)) || remind;
+
+ ext.reporter.sendTelemetryEvent('nps', { survey: SURVEY_NAME, response: result.telName });
+
+ if (result === take) {
+ // If they hit Take, don't ask again (for this survey name), and open the survey
+ await globalState.update(IS_CANDIDATE_KEY, false);
+ await env.openExternal(Uri.parse(`${SURVEY_URL}?o=${encodeURIComponent(process.platform)}&m=${encodeURIComponent(env.machineId)}`));
+ } else if (result === remind) {
+ // If they hit the X or Remind Me Later, ask again in 3 sessions
+ await globalState.update(SESSION_COUNT_KEY, MIN_SESSION_COUNT - 3);
+ } else if (result === never) {
+ // If they hit Never, don't ask again (for this survey name)
+ await globalState.update(IS_CANDIDATE_KEY, false);
+ }
+ } catch { } // Best effort
+}