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 +}