diff --git a/CHANGELOG.md b/CHANGELOG.md index 1784b803ac..7729fb125e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. +## [0.8.3] - 2023-05-01 + +### 🚀 New Features and Enhancements + +- Use dvc config to store and access studio.token [#3768](https://github.com/iterative/vscode-dvc/pull/3768) by [@mattseddon](https://github.com/mattseddon) +- Integrate remaining share experiment command with the extension (exp push) [#3781](https://github.com/iterative/vscode-dvc/pull/3781) by [@mattseddon](https://github.com/mattseddon) +- Add multi-select versions of exp push [#3792](https://github.com/iterative/vscode-dvc/pull/3792) by [@mattseddon](https://github.com/mattseddon) +- Add push experiment(s) to the command palette [#3793](https://github.com/iterative/vscode-dvc/pull/3793) by [@mattseddon](https://github.com/mattseddon) + +### 🐛 Bug Fixes + +- Ensure duplicate colors are not made available during collection [#3780](https://github.com/iterative/vscode-dvc/pull/3780) by [@mattseddon](https://github.com/mattseddon) +- Add GIT_TERMINAL_PROMPT=0 to DVC environment variables [#3779](https://github.com/iterative/vscode-dvc/pull/3779) by [@mattseddon](https://github.com/mattseddon) +- Fix available revisions for multi view plots [#3774](https://github.com/iterative/vscode-dvc/pull/3774) by [@sroy3](https://github.com/sroy3) +- Display progress errors to users [#3791](https://github.com/iterative/vscode-dvc/pull/3791) by [@mattseddon](https://github.com/mattseddon) +- Fix dvc details version incorrectly showing "Not Found" [#3787](https://github.com/iterative/vscode-dvc/pull/3787) by [@julieg18](https://github.com/julieg18) + +### 🔨 Maintenance + +- Use exp push to share experiments to Studio [#3701](https://github.com/iterative/vscode-dvc/pull/3701) by [@mattseddon](https://github.com/mattseddon) +- Remove commit and share and share as branch options [#3771](https://github.com/iterative/vscode-dvc/pull/3771) by [@mattseddon](https://github.com/mattseddon) +- Stub dvc config calls in integration tests [#3777](https://github.com/iterative/vscode-dvc/pull/3777) by [@mattseddon](https://github.com/mattseddon) +- Rename exp commands to closer match CLI [#3778](https://github.com/iterative/vscode-dvc/pull/3778) by [@mattseddon](https://github.com/mattseddon) + ## [0.8.2] - 2023-04-27 ### 🚀 New Features and Enhancements diff --git a/README.md b/README.md index d96d24922a..4bfb082df2 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ These are the VS Code [settings] available for the Extension: | `dvc.studio.shareExperimentsLive` | Automatically share all new experiment metrics and plots logged with DVCLive to Studio. This option will only take effect once Studio is connected. | | `dvc.focusedProjects` | A subset of paths to the workspace's available DVC projects. Using this option will override project auto-discovery. | | `dvc.doNotShowWalkthroughAfterInstall` | Do not prompt to show the Get Started page after installing. Useful for pre-configured development environments | +| `dvc.doNotRecommendAddStudioToken` | Do not prompt to add a [studio.token] to the global DVC config, which enables automatic sharing of experiments to [Studio]. | | `dvc.doNotRecommendRedHatExtension` | Do not prompt to install the Red Hat YAML extension, which helps with DVC YAML schema validation (`dvc.yaml` and `.dvc` files). | | `dvc.doNotShowCliUnavailable` | Do not warn when the workspace contains a DVC project but the DVC binary is unavailable. | @@ -162,6 +163,9 @@ These are the VS Code [settings] available for the Extension: [python extension]: https://marketplace.visualstudio.com/items?itemName=ms-python.python +[studio.token]: + https://dvc.org/doc/user-guide/project-structure/configuration#studio +[Studio]: https://studio.iterative.ai [workspace level]: https://code.visualstudio.com/docs/getstarted/settings#_workspace-settings diff --git a/extension/package.json b/extension/package.json index fbaa91e797..3996da856a 100644 --- a/extension/package.json +++ b/extension/package.json @@ -9,7 +9,7 @@ "extensionDependencies": [ "vscode.git" ], - "version": "0.8.2", + "version": "0.8.3", "license": "Apache-2.0", "readme": "./README.md", "repository": { @@ -296,6 +296,11 @@ "light": "resources/light/queue-experiment.svg" } }, + { + "title": "Push Experiment(s)", + "command": "dvc.pushExperiments", + "category": "DVC" + }, { "title": "Remove Experiment(s)", "command": "dvc.removeExperiments", @@ -506,8 +511,14 @@ "icon": "$(play)" }, { - "title": "Share to Studio", - "command": "dvc.views.experiments.shareExperimentToStudio", + "title": "Push", + "command": "dvc.views.experiments.pushExperiment", + "category": "DVC", + "icon": "$(repo-push)" + }, + { + "title": "Push", + "command": "dvc.views.experimentsTree.pushExperiment", "category": "DVC", "icon": "$(repo-push)" }, @@ -564,6 +575,11 @@ "type": "boolean", "default": null }, + "dvc.doNotRecommendAddStudioToken": { + "description": "Do not prompt to add a studio.token to the global DVC config, which enables automatic sharing of experiments to Studio.", + "type": "boolean", + "default": null + }, "dvc.doNotShowCliUnavailable": { "description": "Do not warn when the workspace contains a DVC project but the DVC binary is unavailable.", "type": "boolean", @@ -599,7 +615,7 @@ "default": null }, "dvc.studio.shareExperimentsLive": { - "description": "Automatically share all new experiment metrics and plots logged with DVCLive to Studio. This option will only take effect once Studio is connected.", + "description": "Automatically share all new experiment metrics and plots logged with DVCLive to Studio. This option will only take effect once Studio is connected (studio.token is set).", "type": "boolean", "default": false } @@ -750,6 +766,10 @@ "command": "dvc.modifyExperimentParamsResetAndRun", "when": "dvc.commands.available && dvc.project.available && !dvc.experiment.running && dvc.experiment.checkpoints" }, + { + "command": "dvc.pushExperiments", + "when": "dvc.commands.available && dvc.project.available && !dvc.experiment.running" + }, { "command": "dvc.queueExperiment", "when": "dvc.commands.available && dvc.project.available && !dvc.experiment.running" @@ -858,6 +878,14 @@ "command": "dvc.views.experiments.branchExperiment", "when": "false" }, + { + "command": "dvc.views.experiments.pushExperiment", + "when": "false" + }, + { + "command": "dvc.views.experimentsTree.pushExperiment", + "when": "false" + }, { "command": "dvc.views.experiments.queueExperiment", "when": "false" @@ -882,10 +910,6 @@ "command": "dvc.views.experiments.resetAndRunCheckpointExperiment", "when": "false" }, - { - "command": "dvc.views.experiments.shareExperimentToStudio", - "when": "false" - }, { "command": "dvc.views.experiments.showLogs", "when": "false" @@ -1147,7 +1171,7 @@ "when": "view == dvc.views.experimentsTree && dvc.commands.available && viewItem =~ /^(experiment|queued)$/ && !dvc.experiment.running" }, { - "command": "dvc.views.experiments.shareExperimentToStudio", + "command": "dvc.views.experimentsTree.pushExperiment", "group": "1_share@0", "when": "view == dvc.views.experimentsTree && dvc.commands.available && viewItem == experiment && !dvc.experiment.running" }, @@ -1603,7 +1627,7 @@ "vscode-languageclient": "8.1.0" }, "devDependencies": { - "@swc/core": "1.3.53", + "@swc/core": "1.3.54", "@swc/jest": "0.2.26", "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", diff --git a/extension/src/cli/dvc/discovery.ts b/extension/src/cli/dvc/discovery.ts index e7ad9aa01a..e396e925b9 100644 --- a/extension/src/cli/dvc/discovery.ts +++ b/extension/src/cli/dvc/discovery.ts @@ -105,6 +105,7 @@ const warnUser = ( type CanRunCli = { isAvailable: boolean isCompatible: boolean | undefined + version: string | undefined } export const isCliCompatible = ( @@ -146,7 +147,8 @@ const processVersionDetails = ( warnUser(setup, cliCompatible, version) return { isAvailable, - isCompatible + isCompatible, + version } } @@ -171,7 +173,7 @@ const tryGlobalFallbackVersion = async ( setup.unsetPythonBinPath() } - return { isAvailable, isCompatible } + return { isAvailable, isCompatible, version } } const extensionCanAutoRunCli = async ( diff --git a/extension/src/commands/external.ts b/extension/src/commands/external.ts index 78ed077eec..d3de70e56b 100644 --- a/extension/src/commands/external.ts +++ b/extension/src/commands/external.ts @@ -2,6 +2,7 @@ export enum RegisteredCliCommands { EXPERIMENT_APPLY = 'dvc.applyExperiment', EXPERIMENT_BRANCH = 'dvc.branchExperiment', EXPERIMENT_GARBAGE_COLLECT = 'dvc.experimentGarbageCollect', + EXPERIMENT_PUSH = 'dvc.pushExperiments', EXPERIMENT_REMOVE = 'dvc.removeExperiments', EXPERIMENT_REMOVE_QUEUE = 'dvc.removeExperimentQueue', EXPERIMENT_RESET_AND_RUN = 'dvc.resetAndRunCheckpointExperiment', @@ -100,7 +101,6 @@ export enum RegisteredCommands { ADD_STUDIO_ACCESS_TOKEN = 'dvc.addStudioAccessToken', UPDATE_STUDIO_ACCESS_TOKEN = 'dvc.updateStudioAccessToken', REMOVE_STUDIO_ACCESS_TOKEN = 'dvc.removeStudioAccessToken', - EXPERIMENT_VIEW_SHARE_TO_STUDIO = 'dvc.views.experiments.shareExperimentToStudio', SETUP_SHOW_STUDIO_CONNECT = 'dvc.showStudioConnect', SETUP_SHOW_STUDIO_SETTINGS = 'dvc.showStudioSettings', diff --git a/extension/src/experiments/commands/index.ts b/extension/src/experiments/commands/index.ts index 1ccf85db11..2d43ae759a 100644 --- a/extension/src/experiments/commands/index.ts +++ b/extension/src/experiments/commands/index.ts @@ -3,6 +3,13 @@ import { AvailableCommands, InternalCommands } from '../../commands/internal' import { Toast } from '../../vscode/toast' import { WorkspaceExperiments } from '../workspace' import { Setup } from '../../setup' +import { Response } from '../../vscode/response' +import { + ConfigKey, + getConfigValue, + setUserConfigValue +} from '../../vscode/config' +import { STUDIO_URL } from '../../setup/webview/contract' import { RegisteredCommands } from '../../commands/external' export const getBranchExperimentCommand = @@ -10,30 +17,62 @@ export const getBranchExperimentCommand = (cwd: string, name: string, input: string) => experiments.runCommand(AvailableCommands.EXP_BRANCH, cwd, name, input) -export const getShareExperimentToStudioCommand = +const promptToAddStudioToken = async () => { + const response = await Toast.askShowOrCloseOrNever( + `Experiments can be automatically shared to [Studio](${STUDIO_URL}) by setting the studio.token in your config.` + ) + + if (!response || response === Response.CLOSE) { + return + } + if (response === Response.SHOW) { + return commands.executeCommand(RegisteredCommands.SETUP_SHOW_STUDIO_CONNECT) + } + if (response === Response.NEVER) { + return setUserConfigValue(ConfigKey.DO_NOT_RECOMMEND_ADD_STUDIO_TOKEN, true) + } +} + +const convertUrlTextToLink = (stdout: string) => { + const experimentAtRegex = /\sat\s+(https:\/\/studio\.iterative\.ai\/.*$)/ + const match = stdout.match(experimentAtRegex) + if (!(match?.[0] && match?.[1])) { + return stdout + } + return stdout.replace(match[0], ` in [Studio](${match[1]})`) +} + +export const getPushExperimentCommand = (internalCommands: InternalCommands, setup: Setup) => - ({ dvcRoot, id }: { dvcRoot: string; id: string }) => { + ({ dvcRoot, ids }: { dvcRoot: string; ids: string[] }) => { const studioAccessToken = setup.getStudioAccessToken() - if (!studioAccessToken) { - return commands.executeCommand(RegisteredCommands.SETUP_SHOW) + if ( + !( + getConfigValue(ConfigKey.DO_NOT_RECOMMEND_ADD_STUDIO_TOKEN) || + studioAccessToken + ) + ) { + void promptToAddStudioToken() } - return Toast.showProgress('Sharing', async progress => { + return Toast.showProgress('exp push', async progress => { progress.report({ increment: 0 }) - progress.report({ increment: 25, message: 'Running exp push...' }) - - await Toast.runCommandAndIncrementProgress( - () => - internalCommands.executeCommand( - AvailableCommands.EXP_PUSH, - dvcRoot, - id - ), - progress, - 75 + progress.report({ increment: 25, message: `Pushing ${ids.join(' ')}...` }) + + const remainingProgress = 75 + + const stdout = await internalCommands.executeCommand( + AvailableCommands.EXP_PUSH, + dvcRoot, + ...ids ) + progress.report({ + increment: remainingProgress, + message: convertUrlTextToLink(stdout) + }) + return Toast.delayProgressClosing(15000) }) } diff --git a/extension/src/experiments/commands/register.ts b/extension/src/experiments/commands/register.ts index f7764b3f27..5625d73ca5 100644 --- a/extension/src/experiments/commands/register.ts +++ b/extension/src/experiments/commands/register.ts @@ -1,7 +1,4 @@ -import { - getBranchExperimentCommand, - getShareExperimentToStudioCommand -} from '.' +import { getBranchExperimentCommand, getPushExperimentCommand } from '.' import { pickGarbageCollectionFlags } from '../quickPick' import { WorkspaceExperiments } from '../workspace' import { AvailableCommands, InternalCommands } from '../../commands/internal' @@ -163,7 +160,8 @@ const registerExperimentInputCommands = ( const registerExperimentQuickPickCommands = ( experiments: WorkspaceExperiments, - internalCommands: InternalCommands + internalCommands: InternalCommands, + setup: Setup ): void => { internalCommands.registerExternalCliCommand( RegisteredCliCommands.EXPERIMENT_GARBAGE_COLLECT, @@ -223,6 +221,11 @@ const registerExperimentQuickPickCommands = ( () => experiments.selectQueueTasksToKill() ) + internalCommands.registerExternalCliCommand( + RegisteredCliCommands.EXPERIMENT_PUSH, + () => experiments.selectExperimentsToPush(setup) + ) + internalCommands.registerExternalCliCommand( RegisteredCliCommands.EXPERIMENT_REMOVE, () => experiments.selectExperimentsToRemove() @@ -283,7 +286,7 @@ export const registerExperimentCommands = ( registerExperimentCwdCommands(experiments, internalCommands) registerExperimentNameCommands(experiments, internalCommands) registerExperimentInputCommands(experiments, internalCommands) - registerExperimentQuickPickCommands(experiments, internalCommands) + registerExperimentQuickPickCommands(experiments, internalCommands, setup) registerExperimentRunCommands(experiments, internalCommands, setup) internalCommands.registerExternalCommand( @@ -292,9 +295,9 @@ export const registerExperimentCommands = ( experiments.getRepository(dvcRoot).toggleExperimentStatus(id) ) - internalCommands.registerExternalCommand( - RegisteredCommands.EXPERIMENT_VIEW_SHARE_TO_STUDIO, - getShareExperimentToStudioCommand(internalCommands, setup) + internalCommands.registerExternalCliCommand( + RegisteredCliCommands.EXPERIMENT_VIEW_PUSH, + getPushExperimentCommand(internalCommands, setup) ) internalCommands.registerExternalCliCommand( diff --git a/extension/src/experiments/index.ts b/extension/src/experiments/index.ts index c48904b564..def9421933 100644 --- a/extension/src/experiments/index.ts +++ b/extension/src/experiments/index.ts @@ -401,6 +401,14 @@ export class Experiments extends BaseRepository { ) } + public pickExperimentsToPush() { + return pickExperiments( + this.experiments.getExperiments(), + this.getFirstThreeColumnOrder(), + Title.SELECT_EXPERIMENTS_PUSH + ) + } + public async pickAndModifyParams(overrideId?: string) { const id = await this.getExperimentId(overrideId) if (!id) { diff --git a/extension/src/experiments/model/collect.ts b/extension/src/experiments/model/collect.ts index 35101876e8..94985d1186 100644 --- a/extension/src/experiments/model/collect.ts +++ b/extension/src/experiments/model/collect.ts @@ -328,10 +328,10 @@ export const collectExperiments = ( return acc } -type DeletableExperimentAccumulator = { [dvcRoot: string]: Set } +type ExperimentTypesAccumulator = { [dvcRoot: string]: Set } const initializeAccumulatorRoot = ( - acc: DeletableExperimentAccumulator, + acc: ExperimentTypesAccumulator, dvcRoot: string ) => { if (!acc[dvcRoot]) { @@ -340,12 +340,12 @@ const initializeAccumulatorRoot = ( } const collectExperimentItem = ( - acc: DeletableExperimentAccumulator, - deletable: Set, + acc: ExperimentTypesAccumulator, + types: Set, experimentItem: ExperimentItem ) => { const { dvcRoot, type, id, label } = experimentItem - if (!deletable.has(type)) { + if (!types.has(type)) { return } initializeAccumulatorRoot(acc, dvcRoot) @@ -357,18 +357,17 @@ const collectExperimentItem = ( acc[dvcRoot].add(id) } -export const collectDeletable = ( - experimentItems: (string | ExperimentItem)[] -): DeletableExperimentAccumulator => { - const deletable = new Set([ExperimentType.EXPERIMENT, ExperimentType.QUEUED]) - - const acc: DeletableExperimentAccumulator = {} +export const collectExperimentType = ( + experimentItems: (string | ExperimentItem)[], + types: Set +): ExperimentTypesAccumulator => { + const acc: ExperimentTypesAccumulator = {} for (const experimentItem of experimentItems) { if (typeof experimentItem === 'string') { continue } - collectExperimentItem(acc, deletable, experimentItem) + collectExperimentItem(acc, types, experimentItem) } return acc diff --git a/extension/src/experiments/model/tree.ts b/extension/src/experiments/model/tree.ts index 9f63199817..0b6693468f 100644 --- a/extension/src/experiments/model/tree.ts +++ b/extension/src/experiments/model/tree.ts @@ -9,7 +9,7 @@ import { Uri } from 'vscode' import { ExperimentType } from '.' -import { collectDeletable, ExperimentItem } from './collect' +import { collectExperimentType, ExperimentItem } from './collect' import { MAX_SELECTED_EXPERIMENTS } from './status' import { getDataFromColumnPaths } from './util' import { WorkspaceExperiments } from '../workspace' @@ -69,7 +69,7 @@ export class ExperimentsTree this.experiments = experiments this.resourceLocator = resourceLocator - this.registerWorkaroundCommand() + this.registerWorkaroundCommands() this.updateDescriptionOnChange() } @@ -123,24 +123,44 @@ export class ExperimentsTree ) } - private registerWorkaroundCommand() { + private registerWorkaroundCommands() { + const callCommandWithSelected = async ( + command: + | RegisteredCliCommands.EXPERIMENT_VIEW_REMOVE + | RegisteredCliCommands.EXPERIMENT_VIEW_PUSH, + experimentItem: ExperimentItem | string, + types: ExperimentType[] + ) => { + const selected = [ + ...this.getSelectedExperimentItems(), + experimentItem + ] as (string | ExperimentItem)[] + + const acc = collectExperimentType(selected, new Set(types)) + + for (const [dvcRoot, ids] of Object.entries(acc)) { + await commands.executeCommand(command, { dvcRoot, ids: [...ids] }) + } + } + commands.registerCommand( 'dvc.views.experimentsTree.removeExperiment', - async experimentItem => { - const selected = [ - ...this.getSelectedExperimentItems(), - experimentItem - ] as (string | ExperimentItem)[] - - const deletable = collectDeletable(selected) - - for (const [dvcRoot, ids] of Object.entries(deletable)) { - await commands.executeCommand( - RegisteredCliCommands.EXPERIMENT_VIEW_REMOVE, - { dvcRoot, ids } - ) - } - } + (experimentItem: ExperimentItem) => + callCommandWithSelected( + RegisteredCliCommands.EXPERIMENT_VIEW_REMOVE, + experimentItem, + [ExperimentType.EXPERIMENT, ExperimentType.QUEUED] + ) + ) + + commands.registerCommand( + 'dvc.views.experimentsTree.pushExperiment', + (experimentItem: ExperimentItem) => + callCommandWithSelected( + RegisteredCliCommands.EXPERIMENT_VIEW_PUSH, + experimentItem, + [ExperimentType.EXPERIMENT] + ) ) } diff --git a/extension/src/experiments/webview/messages.ts b/extension/src/experiments/webview/messages.ts index 7ed3447e97..202e8ce289 100644 --- a/extension/src/experiments/webview/messages.ts +++ b/extension/src/experiments/webview/messages.ts @@ -194,10 +194,10 @@ export class WebviewMessages { case MessageFromWebviewType.ADD_CONFIGURATION: { return this.addConfiguration() } - case MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO: + case MessageFromWebviewType.PUSH_EXPERIMENT: return commands.executeCommand( - RegisteredCommands.EXPERIMENT_VIEW_SHARE_TO_STUDIO, - { dvcRoot: this.dvcRoot, id: message.payload } + RegisteredCliCommands.EXPERIMENT_VIEW_PUSH, + { dvcRoot: this.dvcRoot, ids: message.payload } ) case MessageFromWebviewType.SHOW_EXPERIMENT_LOGS: diff --git a/extension/src/experiments/workspace.ts b/extension/src/experiments/workspace.ts index 1bc63ce95e..945fce327b 100644 --- a/extension/src/experiments/workspace.ts +++ b/extension/src/experiments/workspace.ts @@ -1,6 +1,7 @@ import { EventEmitter, Memento } from 'vscode' import isEmpty from 'lodash.isempty' import { Experiments, ModifiedExperimentAndRunCommandId } from '.' +import { getPushExperimentCommand } from './commands' import { TableData } from './webview/contract' import { Args } from '../cli/dvc/constants' import { @@ -9,6 +10,7 @@ import { InternalCommands } from '../commands/internal' import { ResourceLocator } from '../resourceLocator' +import { Setup } from '../setup' import { Toast } from '../vscode/toast' import { getInput, @@ -172,6 +174,22 @@ export class WorkspaceExperiments extends BaseWorkspaceWebviews< return this.runCommand(AvailableCommands.QUEUE_KILL, cwd, ...taskIds) } + public async selectExperimentsToPush(setup: Setup) { + const dvcRoot = await this.getFocusedOrOnlyOrPickProject() + if (!dvcRoot) { + return + } + + const ids = await this.getRepository(dvcRoot).pickExperimentsToPush() + if (!ids || isEmpty(ids)) { + return + } + + const pushCommand = getPushExperimentCommand(this.internalCommands, setup) + + return pushCommand({ dvcRoot, ids }) + } + public async selectExperimentsToRemove() { const cwd = await this.getFocusedOrOnlyOrPickProject() if (!cwd) { diff --git a/extension/src/interfaces.ts b/extension/src/interfaces.ts index 98c5cebe6b..2afdcceccb 100644 --- a/extension/src/interfaces.ts +++ b/extension/src/interfaces.ts @@ -17,7 +17,10 @@ export interface IExtensionSetup { setAvailable: (available: boolean) => void getAvailable: () => boolean - setCliCompatible: (compatible: boolean | undefined) => void + setCliCompatibleAndVersion: ( + compatible: boolean | undefined, + version: string | undefined + ) => void setRoots: () => Promise unsetPythonBinPath: () => void } diff --git a/extension/src/setup/autoInstall.ts b/extension/src/setup/autoInstall.ts index 49b4b43421..b38480e43a 100644 --- a/extension/src/setup/autoInstall.ts +++ b/extension/src/setup/autoInstall.ts @@ -23,25 +23,33 @@ const showInstallProgress = ( Toast.showProgress('Installing packages', async progress => { progress.report({ increment: 0 }) - await Toast.runCommandAndIncrementProgress( - async () => { - await installPackages(root, pythonBinPath, 'dvclive') - return 'DVCLive Installed' - }, - progress, - 25 - ) + try { + await Toast.runCommandAndIncrementProgress( + async () => { + await installPackages(root, pythonBinPath, 'dvclive') + return 'DVCLive Installed' + }, + progress, + 25 + ) + } catch (error: unknown) { + return Toast.reportProgressError(error, progress) + } - await Toast.runCommandAndIncrementProgress( - async () => { - await installPackages(root, pythonBinPath, 'dvc') - return 'DVC Installed' - }, - progress, - 75 - ) + try { + await Toast.runCommandAndIncrementProgress( + async () => { + await installPackages(root, pythonBinPath, 'dvc') + return 'DVC Installed' + }, + progress, + 75 + ) - return Toast.delayProgressClosing() + return Toast.delayProgressClosing() + } catch (error: unknown) { + return Toast.reportProgressError(error, progress) + } }) export const autoInstallDvc = async (): Promise => { diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 7bb0e5b0fd..db2f622502 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -86,6 +86,7 @@ export class Setup private cliAccessible = false private cliCompatible: boolean | undefined + private cliVersion: string | undefined private dotFolderWatcher?: Disposer @@ -225,8 +226,12 @@ export class Setup return available } - public setCliCompatible(compatible: boolean | undefined) { + public setCliCompatibleAndVersion( + compatible: boolean | undefined, + version: string | undefined + ) { this.cliCompatible = compatible + this.cliVersion = version void this.updateIsStudioConnected() const incompatible = compatible === undefined ? undefined : !compatible void setContextValue(ContextKey.CLI_INCOMPATIBLE, incompatible) @@ -369,7 +374,7 @@ export class Setup return this.sendDataToWebview() } - public async getDvcCliDetails(): Promise { + public getDvcCliDetails(): DvcCliDetails { const dvcPath = this.config.getCliPath() const pythonBinPath = this.config.getPythonBinPath() const cwd = getFirstWorkspaceFolder() @@ -380,7 +385,7 @@ export class Setup return { command, - version: cwd ? await this.getCliVersion(cwd) : undefined + version: this.cliVersion } } @@ -407,12 +412,10 @@ export class Setup const pythonBinPath = await findPythonBinForInstall() - const dvcCliDetails = await this.getDvcCliDetails() - this.webviewMessages.sendWebviewMessage({ canGitInitialize, cliCompatible: this.getCliCompatible(), - dvcCliDetails, + dvcCliDetails: this.getDvcCliDetails(), hasData, isPythonExtensionUsed: !this.isDVCBeingUsedGlobally() && isPythonExtensionUsed, diff --git a/extension/src/setup/runner.test.ts b/extension/src/setup/runner.test.ts index fe190e1f55..7fd20124ba 100644 --- a/extension/src/setup/runner.test.ts +++ b/extension/src/setup/runner.test.ts @@ -74,7 +74,7 @@ const mockedInitialize = jest.fn() const mockedIsPythonExtensionUsed = jest.fn() const mockedResetMembers = jest.fn() const mockedSetAvailable = jest.fn() -const mockedSetCliCompatible = jest.fn() +const mockedSetCliCompatibleAndVersion = jest.fn() const mockedSetRoots = jest.fn() const mockedShowSetup = jest.fn() const mockedShouldWarnUserIfCLIUnavailable = jest.fn() @@ -280,7 +280,7 @@ describe('run', () => { isPythonExtensionUsed: mockedIsPythonExtensionUsed, resetMembers: mockedResetMembers, setAvailable: mockedSetAvailable, - setCliCompatible: mockedSetCliCompatible, + setCliCompatibleAndVersion: mockedSetCliCompatibleAndVersion, setRoots: mockedSetRoots, shouldWarnUserIfCLIUnavailable: mockedShouldWarnUserIfCLIUnavailable, showSetup: mockedShowSetup, @@ -617,7 +617,7 @@ describe('runWithRecheck', () => { isPythonExtensionUsed: mockedIsPythonExtensionUsed, resetMembers: mockedResetMembers, setAvailable: mockedSetAvailable, - setCliCompatible: mockedSetCliCompatible, + setCliCompatibleAndVersion: mockedSetCliCompatibleAndVersion, setRoots: mockedSetRoots, shouldWarnUserIfCLIUnavailable: mockedShouldWarnUserIfCLIUnavailable, showSetup: mockedShowSetup, diff --git a/extension/src/setup/runner.ts b/extension/src/setup/runner.ts index 51b7c6f16b..8eff4afc5b 100644 --- a/extension/src/setup/runner.ts +++ b/extension/src/setup/runner.ts @@ -179,12 +179,12 @@ export const checkAvailable = async ( setup: IExtensionSetup, dvcRootOrFirstFolder: string ) => { - const { isAvailable, isCompatible } = await extensionCanRunCli( + const { isAvailable, version, isCompatible } = await extensionCanRunCli( setup, dvcRootOrFirstFolder ) - setup.setCliCompatible(isCompatible) + setup.setCliCompatibleAndVersion(isCompatible, version) setup.setAvailable(isAvailable) if (setup.hasRoots() && isAvailable) { diff --git a/extension/src/telemetry/constants.ts b/extension/src/telemetry/constants.ts index f55c7f1a2c..a22a2302ac 100644 --- a/extension/src/telemetry/constants.ts +++ b/extension/src/telemetry/constants.ts @@ -127,6 +127,7 @@ export interface IEventNamePropertyMapping { [EventName.EXPERIMENT_FILTERS_REMOVE_ALL]: undefined [EventName.EXPERIMENT_GARBAGE_COLLECT]: undefined [EventName.EXPERIMENT_METRICS_AND_PARAMS_TOGGLE]: undefined + [EventName.EXPERIMENT_PUSH]: undefined [EventName.EXPERIMENT_REMOVE]: undefined [EventName.EXPERIMENT_REMOVE_QUEUE]: undefined [EventName.EXPERIMENT_RESUME]: undefined @@ -144,7 +145,6 @@ export interface IEventNamePropertyMapping { [EventName.EXPERIMENT_VIEW_BRANCH]: undefined [EventName.EXPERIMENT_VIEW_PUSH]: undefined [EventName.EXPERIMENT_VIEW_REMOVE]: undefined - [EventName.EXPERIMENT_VIEW_SHARE_TO_STUDIO]: undefined [EventName.EXPERIMENT_VIEW_SHOW_LOGS]: undefined [EventName.EXPERIMENT_VIEW_STOP]: undefined [EventName.QUEUE_EXPERIMENT]: undefined diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index 87ea8c710c..daff6d1ac2 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -10,7 +10,8 @@ import { workspace, Uri, QuickPickItem, - ViewColumn + ViewColumn, + CancellationToken } from 'vscode' import { buildExperiments, stubWorkspaceExperimentsGetters } from './util' import { Disposable } from '../../../extension' @@ -34,6 +35,7 @@ import { import { buildInternalCommands, buildMockExperimentsData, + bypassProgressCloseDelay, closeAllEditors, configurationChangeEvent, experimentsUpdatedEvent, @@ -84,6 +86,8 @@ import { DvcReader } from '../../../cli/dvc/reader' import { DvcViewer } from '../../../cli/dvc/viewer' import { DEFAULT_NB_ITEMS_PER_ROW } from '../../../plots/webview/contract' import { GitReader } from '../../../cli/git/reader' +import { Toast } from '../../../vscode/toast' +import { Response } from '../../../vscode/response' const { openFileInEditor } = FileSystem @@ -633,7 +637,7 @@ suite('Experiments Test Suite', () => { ) }).timeout(WEBVIEW_TEST_TIMEOUT) - it('should handle a message to share an experiment to Studio', async () => { + it('should handle a message to push an experiment', async () => { const { experiments } = buildExperiments(disposable) await experiments.isReady() @@ -643,6 +647,7 @@ suite('Experiments Test Suite', () => { const mockMessageReceived = getMessageReceivedEmitter(webview) const executeCommandSpy = spy(commands, 'executeCommand') + const mockExpPush = stub(DvcExecutor.prototype, 'expPush') const mockGetStudioAccessToken = stub( Setup.prototype, @@ -656,15 +661,24 @@ suite('Experiments Test Suite', () => { }) ) + const mockAskShowOrCloseOrNever = stub(Toast, 'askShowOrCloseOrNever') + + const userPrompted = new Promise(resolve => + mockAskShowOrCloseOrNever.callsFake(() => { + resolve(undefined) + return Promise.resolve(Response.SHOW) + }) + ) + mockMessageReceived.fire({ - payload: mockExpId, - type: MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO + payload: [mockExpId], + type: MessageFromWebviewType.PUSH_EXPERIMENT }) - await tokenNotFound + await Promise.all([tokenNotFound, userPrompted]) expect(executeCommandSpy).to.be.calledWithExactly( - RegisteredCommands.SETUP_SHOW + RegisteredCommands.SETUP_SHOW_STUDIO_CONNECT ) mockGetStudioAccessToken.resetBehavior() @@ -675,24 +689,41 @@ suite('Experiments Test Suite', () => { return 'isat_token' }) ) - const mockexpPush = stub(DvcExecutor.prototype, 'expPush') + + const mockShowProgress = stub(Toast, 'showProgress') + bypassProgressCloseDelay() + + const mockReport = stub() + + mockShowProgress.callsFake((title, callback) => { + expect(title).to.equal('exp push') + + const progress = { report: mockReport } + return callback(progress, {} as CancellationToken) + }) + const commandExecuted = new Promise(resolve => - mockexpPush.callsFake(() => { + mockExpPush.callsFake(() => { resolve(undefined) return Promise.resolve( - `Pushed experiment ${mockExpId} to Git remote 'origin'` + "Experiment major-lamb is up to date on Git remote 'origin'.\nView your experiments at \nhttps://studio.iterative.ai/user/mattseddon/projects/vscode-dvc-demo-ynm6t3jxdx" ) }) ) mockMessageReceived.fire({ - payload: mockExpId, - type: MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO + payload: [mockExpId], + type: MessageFromWebviewType.PUSH_EXPERIMENT }) await Promise.all([tokenFound, commandExecuted]) - expect(mockexpPush).to.be.calledWithExactly(dvcDemoPath, mockExpId) + expect(mockExpPush).to.be.calledWithExactly(dvcDemoPath, mockExpId) + expect(mockReport).to.be.calledWithExactly({ + increment: 75, + message: + "Experiment major-lamb is up to date on Git remote 'origin'.\nView your experiments in [Studio](https://studio.iterative.ai/user/mattseddon/projects/vscode-dvc-demo-ynm6t3jxdx)" + }) }).timeout(WEBVIEW_TEST_TIMEOUT) it("should be able to handle a message to modify an experiment's params and queue an experiment", async () => { diff --git a/extension/src/test/suite/experiments/model/tree.test.ts b/extension/src/test/suite/experiments/model/tree.test.ts index c30ae742aa..9b3ad3764b 100644 --- a/extension/src/test/suite/experiments/model/tree.test.ts +++ b/extension/src/test/suite/experiments/model/tree.test.ts @@ -12,6 +12,7 @@ import { ExperimentType } from '../../../../experiments/model' import { UNSELECTED } from '../../../../experiments/model/status' import { bypassProcessManagerDebounce, + bypassProgressCloseDelay, getFirstArgOfLastCall, getMockNow, getTimeSafeDisposer, @@ -345,6 +346,99 @@ suite('Experiments Tree Test Suite', () => { ) }) + it('should be able to push an experiment with dvc.views.experimentsTree.pushExperiment', async () => { + bypassProgressCloseDelay() + const mockExperimentId = 'exp-to-push' + const mockExperiment = { + dvcRoot: dvcDemoPath, + id: mockExperimentId, + type: ExperimentType.EXPERIMENT + } + + const mockExpPush = stub(DvcExecutor.prototype, 'expPush').resolves('') + + stubPrivatePrototypeMethod( + ExperimentsTree, + 'getSelectedExperimentItems' + ).returns([mockExperiment]) + + await commands.executeCommand( + 'dvc.views.experimentsTree.pushExperiment', + mockExperiment + ) + + expect(mockExpPush).to.be.calledWithExactly(dvcDemoPath, mockExperimentId) + }) + + it('should be able to push the provided experiment with dvc.views.experimentsTree.pushExperiment (if no experiments are selected)', async () => { + bypassProgressCloseDelay() + const mockExperiment = 'exp-to-push' + + const mockExpPush = stub(DvcExecutor.prototype, 'expPush').resolves('') + + stubPrivatePrototypeMethod( + ExperimentsTree, + 'getSelectedExperimentItems' + ).returns([]) + + await commands.executeCommand( + 'dvc.views.experimentsTree.pushExperiment', + { + dvcRoot: dvcDemoPath, + id: mockExperiment, + type: ExperimentType.EXPERIMENT + } + ) + + expect(mockExpPush).to.be.calledWithExactly(dvcDemoPath, mockExperiment) + }) + + it('should be able to push multiple experiments with dvc.views.experimentsTree.pushExperiment', async () => { + bypassProgressCloseDelay() + const mockFirstExperimentId = 'first-exp-pushed' + const mockSecondExperimentId = 'second-exp-pushed' + const mockQueuedExperimentLabel = 'queued-excluded' + + const mockExpPush = stub(DvcExecutor.prototype, 'expPush').resolves('') + + stubPrivatePrototypeMethod( + ExperimentsTree, + 'getSelectedExperimentItems' + ).returns([ + dvcDemoPath, + { + dvcRoot: dvcDemoPath, + label: mockQueuedExperimentLabel, + type: ExperimentType.QUEUED + }, + { + dvcRoot: dvcDemoPath, + id: mockFirstExperimentId, + type: ExperimentType.EXPERIMENT + }, + { + dvcRoot: dvcDemoPath, + id: 'workspace-excluded', + type: ExperimentType.WORKSPACE + } + ]) + + await commands.executeCommand( + 'dvc.views.experimentsTree.pushExperiment', + { + dvcRoot: dvcDemoPath, + id: mockSecondExperimentId, + type: ExperimentType.EXPERIMENT + } + ) + + expect(mockExpPush).to.be.calledWithExactly( + dvcDemoPath, + mockFirstExperimentId, + mockSecondExperimentId + ) + }) + it('should be able to apply an experiment to the workspace with dvc.views.experiments.applyExperiment', async () => { const { experiments } = buildExperiments(disposable) diff --git a/extension/src/test/suite/experiments/workspace.test.ts b/extension/src/test/suite/experiments/workspace.test.ts index 38ef50b874..47cb36997f 100644 --- a/extension/src/test/suite/experiments/workspace.test.ts +++ b/extension/src/test/suite/experiments/workspace.test.ts @@ -13,6 +13,7 @@ import { Experiments } from '../../../experiments' import * as QuickPick from '../../../vscode/quickPick' import { DvcExecutor } from '../../../cli/dvc/executor' import { + bypassProgressCloseDelay, closeAllEditors, getInputBoxEvent, getTimeSafeDisposer, @@ -657,6 +658,112 @@ suite('Workspace Experiments Test Suite', () => { }) }) + describe('dvc.pushExperiments', () => { + it('should ask the user to pick experiment(s) and then push selected ones to the remote', async () => { + bypassProgressCloseDelay() + const mockExperimentId = 'exp-e7a67' + const secondMockExperimentId = 'exp-83425' + type QuickPickReturnValue = QuickPickItemWithValue[] + stub(Setup.prototype, 'getStudioAccessToken').returns('isat_token') + + const { experiments } = buildExperiments(disposable) + + await experiments.isReady() + + stubWorkspaceExperimentsGetters(dvcDemoPath, experiments) + + const mockShowQuickPick = stub(window, 'showQuickPick') as SinonStub< + [items: readonly QuickPickItem[], options: QuickPickOptionsWithTitle], + Thenable + > + + mockShowQuickPick + .onFirstCall() + .resolves([ + { + value: mockExperimentId + } + ] as QuickPickReturnValue) + .onSecondCall() + .resolves([ + { + value: mockExperimentId + }, + { + value: secondMockExperimentId + } + ] as QuickPickReturnValue) + const mockExpPush = stub(DvcExecutor.prototype, 'expPush') + + await commands.executeCommand(RegisteredCliCommands.EXPERIMENT_PUSH) + expect(mockShowQuickPick).calledWithExactly( + [ + { + description: '[exp-e7a67]', + detail: `Created:${formatDate( + '2020-12-29T15:31:52' + )}, loss:2.0205045, accuracy:0.37241668`, + label: '4fb124a', + value: 'exp-e7a67' + }, + { + description: '[test-branch]', + detail: `Created:${formatDate( + '2020-12-29T15:28:59' + )}, loss:1.9293040, accuracy:0.46680000`, + label: '42b8736', + value: 'test-branch' + }, + { + description: '[exp-83425]', + detail: `Created:${formatDate( + '2020-12-29T15:27:02' + )}, loss:1.7750162, accuracy:0.59265000`, + label: EXPERIMENT_WORKSPACE_ID, + value: 'exp-83425' + }, + { + description: undefined, + detail: 'Created:-, loss:-, accuracy:-', + label: '489fd8b', + value: '489fd8b' + }, + { + description: '[exp-f13bca]', + detail: `Created:${formatDate( + '2020-12-29T15:26:36' + )}, loss:-, accuracy:-`, + label: 'f0f9186', + value: 'exp-f13bca' + }, + { + description: undefined, + detail: `Created:${formatDate( + '2020-12-29T15:25:27' + )}, loss:-, accuracy:-`, + label: '55d492c', + value: '55d492c' + } + ], + { + canPickMany: true, + matchOnDescription: true, + matchOnDetail: true, + title: 'Select Experiment(s) to Push' + } + ) + expect(mockExpPush).to.be.calledWith(dvcDemoPath, mockExperimentId) + + await commands.executeCommand(RegisteredCliCommands.EXPERIMENT_PUSH) + + expect(mockExpPush).to.be.calledWith( + dvcDemoPath, + mockExperimentId, + secondMockExperimentId + ) + }) + }) + describe('dvc.removeExperiments', () => { it('should ask the user to pick experiment(s) and then remove selected ones from the workspace', async () => { const mockExperimentId = 'exp-e7a67' @@ -754,7 +861,7 @@ suite('Workspace Experiments Test Suite', () => { canPickMany: true, matchOnDescription: true, matchOnDetail: true, - title: 'Select Experiments to Remove' + title: 'Select Experiment(s) to Remove' } ) expect(mockExperimentRemove).to.be.calledWith( diff --git a/extension/src/test/suite/setup/autoInstall.test.ts b/extension/src/test/suite/setup/autoInstall.test.ts index 44276f5dfd..775a75fb32 100644 --- a/extension/src/test/suite/setup/autoInstall.test.ts +++ b/extension/src/test/suite/setup/autoInstall.test.ts @@ -8,6 +8,7 @@ import * as Python from '../../../python' import { autoInstallDvc } from '../../../setup/autoInstall' import * as WorkspaceFolders from '../../../vscode/workspaceFolders' import { bypassProgressCloseDelay } from '../util' +import { Toast } from '../../../vscode/toast' const { getDefaultPython } = Python @@ -89,5 +90,64 @@ suite('Auto Install Test Suite', () => { 'dvclive' ) }) + + it('should show an error message and exit early if DVCLive fails to install', async () => { + bypassProgressCloseDelay() + const cwd = __dirname + stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined) + stub(Python, 'findPythonBin').resolves(defaultPython) + const mockInstallPackages = stub(Python, 'installPackages').rejects( + new Error('Failed to install DVCLive') + ) + stub(WorkspaceFolders, 'getFirstWorkspaceFolder').returns(cwd) + + const showProgressSpy = spy(window, 'withProgress') + const showErrorSpy = spy(window, 'showErrorMessage') + + await autoInstallDvc() + + expect(showProgressSpy).to.be.called + expect(showErrorSpy).not.to.be.called + expect(mockInstallPackages).to.be.calledOnce + expect(mockInstallPackages).to.be.calledWithExactly( + cwd, + defaultPython, + 'dvclive' + ) + }) + + it('should show an error message if DVC fails to install', async () => { + bypassProgressCloseDelay() + const cwd = __dirname + stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined) + stub(Python, 'findPythonBin').resolves(defaultPython) + const mockInstallPackages = stub(Python, 'installPackages') + .onFirstCall() + .resolves(undefined) + .onSecondCall() + .rejects(new Error('Network error')) + stub(WorkspaceFolders, 'getFirstWorkspaceFolder').returns(cwd) + + const showProgressSpy = spy(window, 'withProgress') + const showErrorSpy = spy(window, 'showErrorMessage') + const reportProgressErrorSpy = spy(Toast, 'reportProgressError') + + await autoInstallDvc() + + expect(showProgressSpy).to.be.called + expect(showErrorSpy).not.to.be.called + expect(reportProgressErrorSpy).to.be.calledOnce + expect(mockInstallPackages).to.be.calledTwice + expect(mockInstallPackages).to.be.calledWithExactly( + cwd, + defaultPython, + 'dvclive' + ) + expect(mockInstallPackages).to.be.calledWithExactly( + cwd, + defaultPython, + 'dvc' + ) + }) }) }) diff --git a/extension/src/test/suite/setup/index.test.ts b/extension/src/test/suite/setup/index.test.ts index 2163dfe62f..fd213975bc 100644 --- a/extension/src/test/suite/setup/index.test.ts +++ b/extension/src/test/suite/setup/index.test.ts @@ -215,10 +215,9 @@ suite('Setup Test Suite', () => { await config.isReady() - setup.setCliCompatible(undefined) + setup.setCliCompatibleAndVersion(undefined, undefined) setup.setAvailable(false) await setup.setRoots() - stub(setup, 'getCliVersion').resolves(undefined) messageSpy.restore() const mockSendMessage = stub(BaseWebview.prototype, 'show') @@ -257,7 +256,7 @@ suite('Setup Test Suite', () => { await config.isReady() - setup.setCliCompatible(true) + setup.setCliCompatibleAndVersion(true, MIN_CLI_VERSION) setup.setAvailable(true) await setup.setRoots() @@ -304,7 +303,7 @@ suite('Setup Test Suite', () => { await config.isReady() - setup.setCliCompatible(true) + setup.setCliCompatibleAndVersion(true, MIN_CLI_VERSION) setup.setAvailable(true) await setup.setRoots() @@ -354,7 +353,7 @@ suite('Setup Test Suite', () => { await config.isReady() - setup.setCliCompatible(true) + setup.setCliCompatibleAndVersion(true, MIN_CLI_VERSION) setup.setAvailable(true) await setup.setRoots() @@ -579,7 +578,6 @@ suite('Setup Test Suite', () => { mockRunSetup.restore() stub(config, 'isPythonExtensionUsed').returns(false) stub(config, 'getPythonBinPath').resolves(join('python')) - stub(setup, 'getDvcCliDetails').resolves(undefined) mockVersion.resetBehavior() mockVersion @@ -639,7 +637,6 @@ suite('Setup Test Suite', () => { mockExecuteCommand.restore() mockRunSetup.restore() stub(config, 'isPythonExtensionUsed').returns(true) - stub(setup, 'getDvcCliDetails').resolves(undefined) mockVersion.resetBehavior() mockVersion.rejects(new Error('no CLI here')) @@ -759,7 +756,6 @@ suite('Setup Test Suite', () => { const mockUpdate = stub() stub(workspace, 'getConfiguration').returns({ - get: stub(), update: mockUpdate } as unknown as WorkspaceConfiguration) diff --git a/extension/src/vscode/config.ts b/extension/src/vscode/config.ts index 6dc49acf7a..b69fc47642 100644 --- a/extension/src/vscode/config.ts +++ b/extension/src/vscode/config.ts @@ -1,12 +1,13 @@ import { ConfigurationTarget, workspace } from 'vscode' export enum ConfigKey { + DO_NOT_RECOMMEND_ADD_STUDIO_TOKEN = 'dvc.doNotRecommendAddStudioToken', DO_NOT_RECOMMEND_RED_HAT = 'dvc.doNotRecommendRedHatExtension', DO_NOT_SHOW_CLI_UNAVAILABLE = 'dvc.doNotShowCliUnavailable', DO_NOT_SHOW_WALKTHROUGH_AFTER_INSTALL = 'dvc.doNotShowWalkthroughAfterInstall', + DVC_PATH = 'dvc.dvcPath', EXP_TABLE_HEAD_MAX_HEIGHT = 'dvc.experimentsTableHeadMaxHeight', FOCUSED_PROJECTS = 'dvc.focusedProjects', - DVC_PATH = 'dvc.dvcPath', PYTHON_PATH = 'dvc.pythonPath', STUDIO_SHARE_EXPERIMENTS_LIVE = 'dvc.studio.shareExperimentsLive' } diff --git a/extension/src/vscode/title.ts b/extension/src/vscode/title.ts index b06a41427f..51398ffb21 100644 --- a/extension/src/vscode/title.ts +++ b/extension/src/vscode/title.ts @@ -17,7 +17,8 @@ export enum Title { SELECT_COLUMNS = 'Select Columns to Display in the Experiments Table', SELECT_EXPERIMENT = 'Select an Experiment', SELECT_EXPERIMENTS = 'Select Experiments', - SELECT_EXPERIMENTS_REMOVE = 'Select Experiments to Remove', + SELECT_EXPERIMENTS_PUSH = 'Select Experiment(s) to Push', + SELECT_EXPERIMENTS_REMOVE = 'Select Experiment(s) to Remove', SELECT_EXPERIMENTS_TO_PLOT = 'Select up to 7 Experiments to Display in Plots', SELECT_FILTERS_TO_REMOVE = 'Select Filter(s) to Remove', SELECT_FOCUSED_PROJECTS = 'Select Project(s) to Focus (set dvc.focusedProjects)', diff --git a/extension/src/vscode/toast.ts b/extension/src/vscode/toast.ts index d8006950a2..cd4c81579e 100644 --- a/extension/src/vscode/toast.ts +++ b/extension/src/vscode/toast.ts @@ -75,6 +75,22 @@ export class Toast { }) } + static reportProgressError( + error: unknown, + progress: Progress<{ + message?: string | undefined + increment?: number | undefined + }> + ) { + const message = (error as Error)?.message || 'an unexpected error occurred' + + progress.report({ + increment: 0, + message + }) + return Toast.delayProgressClosing(60000) + } + static delayProgressClosing(ms = 5000) { return delay(ms) } diff --git a/extension/src/webview/contract.ts b/extension/src/webview/contract.ts index bc5eae0756..3c712cf445 100644 --- a/extension/src/webview/contract.ts +++ b/extension/src/webview/contract.ts @@ -25,6 +25,7 @@ export enum MessageFromWebviewType { OPEN_PLOTS_WEBVIEW = 'open-plots-webview', OPEN_STUDIO = 'open-studio', OPEN_STUDIO_PROFILE = 'open-studio-profile', + PUSH_EXPERIMENT = 'push-experiment', REMOVE_COLUMN_SORT = 'remove-column-sort', REMOVE_EXPERIMENT = 'remove-experiment', REORDER_COLUMNS = 'reorder-columns', @@ -36,7 +37,6 @@ export enum MessageFromWebviewType { RESIZE_COLUMN = 'resize-column', RESIZE_PLOTS = 'resize-plots', SAVE_STUDIO_TOKEN = 'save-studio-token', - SHARE_EXPERIMENT_TO_STUDIO = 'share-experiment-to-studio', SHOW_EXPERIMENT_LOGS = 'show-experiment-logs', STOP_EXPERIMENT = 'stop-experiment', SORT_COLUMN = 'sort-column', @@ -154,8 +154,8 @@ export type MessageFromWebview = } | { type: MessageFromWebviewType.SHOW_EXPERIMENT_LOGS; payload: string } | { - type: MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO - payload: string + type: MessageFromWebviewType.PUSH_EXPERIMENT + payload: string[] } | { type: MessageFromWebviewType.REMOVE_COLUMN_SORT diff --git a/languageServer/package.json b/languageServer/package.json index 8893572074..dd159c2938 100644 --- a/languageServer/package.json +++ b/languageServer/package.json @@ -25,7 +25,7 @@ "test": "jest --collect-coverage" }, "devDependencies": { - "@swc/core": "1.3.53", + "@swc/core": "1.3.54", "@swc/jest": "0.2.26", "@types/jest": "29.5.1", "clean-webpack-plugin": "4.0.0", diff --git a/package.json b/package.json index 5a52863fa9..bae30c5a6c 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "svgr": "yarn workspace dvc-vscode-webview svgr" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "5.59.0", - "@typescript-eslint/parser": "5.59.0", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", "@vscode/codicons": "0.0.32", "eslint": "8.39.0", "eslint-config-prettier": "8.8.0", @@ -65,7 +65,7 @@ "prettier": "2.8.8", "prettier-config-standard": "5.0.0", "stylelint": "15.6.0", - "stylelint-config-standard-scss": "8.0.0", + "stylelint-config-standard-scss": "9.0.0", "ts-node": "10.9.1", "turbo": "1.9.3", "typescript": "5.0.4" diff --git a/webview/package.json b/webview/package.json index f6040fc9e6..ed34bd91f2 100644 --- a/webview/package.json +++ b/webview/package.json @@ -49,7 +49,7 @@ "@storybook/react-webpack5": "7.0.7", "@storybook/testing-library": "0.1.0", "@svgr/cli": "7.0.0", - "@swc/core": "1.3.53", + "@swc/core": "1.3.54", "@swc/jest": "0.2.26", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.0.0", diff --git a/webview/src/experiments/components/App.test.tsx b/webview/src/experiments/components/App.test.tsx index 2c9925e18b..cd2f6d14c2 100644 --- a/webview/src/experiments/components/App.test.tsx +++ b/webview/src/experiments/components/App.test.tsx @@ -801,6 +801,7 @@ describe('App', () => { }) }) + // eslint-disable-next-line sonarjs/cognitive-complexity describe('Row Context Menu', () => { beforeAll(() => { jest.useFakeTimers() @@ -862,7 +863,7 @@ describe('App', () => { 'Show Logs', 'Apply to Workspace', 'Create new Branch', - 'Share to Studio', + 'Push', 'Modify and Run', 'Modify and Resume', 'Modify and Queue', @@ -932,7 +933,7 @@ describe('App', () => { expect(itemLabels).toContain('Remove') }) - it('should present the Remove option if multiple checkpoint tip rows are selected', () => { + it('should present the remove option if only experiments are selected', () => { renderTableWithoutRunningExperiments() clickRowCheckbox('4fb124a') @@ -944,10 +945,10 @@ describe('App', () => { advanceTimersByTime(100) const menuitems = screen.getAllByRole('menuitem') const itemLabels = menuitems.map(item => item.textContent) - expect(itemLabels).toContain('Remove Selected Rows') + expect(itemLabels).toContain('Remove Selected') const removeOption = menuitems.find(item => - item.textContent?.includes('Remove Selected Rows') + item.textContent?.includes('Remove Selected') ) expect(removeOption).toBeDefined() @@ -960,6 +961,34 @@ describe('App', () => { }) }) + it('should present the push option if only experiments are selected', () => { + renderTableWithoutRunningExperiments() + + clickRowCheckbox('4fb124a') + clickRowCheckbox('42b8736') + + const target = screen.getByText('4fb124a') + fireEvent.contextMenu(target, { bubbles: true }) + + advanceTimersByTime(100) + const menuitems = screen.getAllByRole('menuitem') + const itemLabels = menuitems.map(item => item.textContent) + expect(itemLabels).toContain('Push Selected') + + const pushOption = menuitems.find(item => + item.textContent?.includes('Push Selected') + ) + + expect(pushOption).toBeDefined() + + pushOption && fireEvent.click(pushOption) + + expect(sendMessage).toHaveBeenCalledWith({ + payload: ['exp-e7a67', 'test-branch'], + type: MessageFromWebviewType.PUSH_EXPERIMENT + }) + }) + it('should present the stop option if only running experiments are selected', () => { renderTable() @@ -1014,7 +1043,7 @@ describe('App', () => { }) }) - it('should enable the user to share an experiment to Studio', () => { + it('should enable the user to share an experiment', () => { renderTableWithoutRunningExperiments() const target = screen.getByText('4fb124a') @@ -1023,10 +1052,10 @@ describe('App', () => { advanceTimersByTime(100) const menuitems = screen.getAllByRole('menuitem') const itemLabels = menuitems.map(item => item.textContent) - expect(itemLabels).toContain('Share to Studio') + expect(itemLabels).toContain('Push') const shareOption = menuitems.find(item => - item.textContent?.includes('Share to Studio') + item.textContent?.includes('Push') ) expect(shareOption).toBeDefined() @@ -1034,8 +1063,8 @@ describe('App', () => { shareOption && fireEvent.click(shareOption) expect(sendMessage).toHaveBeenCalledWith({ - payload: 'exp-e7a67', - type: MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO + payload: ['exp-e7a67'], + type: MessageFromWebviewType.PUSH_EXPERIMENT }) }) @@ -1106,8 +1135,13 @@ describe('App', () => { fireEvent.contextMenu(target, { bubbles: true }) advanceTimersByTime(100) - const clearOption = screen.getByText('Clear row selection') - fireEvent.click(clearOption) + + const menuitems = screen.getAllByRole('menuitem') + + const clearOption = menuitems.find(item => + item.textContent?.includes('Clear') + ) + clearOption && fireEvent.click(clearOption) advanceTimersByTime(100) expect(selectedRows().length).toBe(0) diff --git a/webview/src/experiments/components/table/body/RowContextMenu.tsx b/webview/src/experiments/components/table/body/RowContextMenu.tsx index 3778b525c1..f2000e5fcd 100644 --- a/webview/src/experiments/components/table/body/RowContextMenu.tsx +++ b/webview/src/experiments/components/table/body/RowContextMenu.tsx @@ -50,12 +50,12 @@ const getMultiSelectMenuOptions = ( const selectedIds = selectedRowsList.map(value => value.row.original.id) - const removableRowIds = selectedRowsList + const experimentRowIds = selectedRowsList .filter(value => value.row.depth === 1) .map(value => value.row.original.id) - const hideRemoveOption = - removableRowIds.length !== selectedRowsList.length || hasRunningExperiment + const hideExperimentOnlyOption = + experimentRowIds.length !== selectedRowsList.length || hasRunningExperiment const stoppableRows = selectedRowsList .filter(value => isRunning(value.row.original.status)) @@ -105,17 +105,24 @@ const getMultiSelectMenuOptions = ( true ), experimentMenuOption( - removableRowIds, - 'Remove Selected Rows', + experimentRowIds, + 'Push Selected', + MessageFromWebviewType.PUSH_EXPERIMENT, + hideExperimentOnlyOption, + true + ), + experimentMenuOption( + experimentRowIds, + 'Remove Selected', MessageFromWebviewType.REMOVE_EXPERIMENT, - hideRemoveOption, + hideExperimentOnlyOption, true ), { divider: true, id: 'clear-selection', keyboardShortcut: 'Esc', - label: 'Clear row selection' + label: 'Clear' } ] } @@ -200,9 +207,11 @@ const getSingleSelectMenuOptions = ( 'Create new Branch', MessageFromWebviewType.CREATE_BRANCH_FROM_EXPERIMENT ), - hideIfRunningOrNotExperiment( - 'Share to Studio', - MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO, + experimentMenuOption( + [id], + 'Push', + MessageFromWebviewType.PUSH_EXPERIMENT, + isNotExperiment, true ), ...getRunResumeOptions( diff --git a/yarn.lock b/yarn.lock index 0f482d7dcc..2b303aa6c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4572,71 +4572,71 @@ deepmerge "^4.3.1" svgo "^3.0.2" -"@swc/core-darwin-arm64@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.53.tgz#0dabb2ecfeea3aea8e66f5cbf9736128169e7055" - integrity sha512-JvWwV/duzdQ60iwWYceDhDk75LmdrLoPC7myX3Src3gl/bJtETMq7uHS9uY8m0GQOqbct7XGR3q5Ff21YxkSzg== - -"@swc/core-darwin-x64@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.53.tgz#01ef02573d2743478a5400a1f7b397901a13202a" - integrity sha512-UuIGZtCfUPJM2Q01bRIFzmucOMg8UZ+mY3kh5xB8kl/VrLltBlraSWGjjJzYmUeUxiF8+CtMfeSYav5QfU2v3g== - -"@swc/core-linux-arm-gnueabihf@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.53.tgz#d76fdcf34e835244f670b1696b320287187da80f" - integrity sha512-LupAjTErteyLmowYIfiQeTz3uVh7/SPYv/EuG1PYrajNoUYomt7WA0rQUoyglF9VtwVyNqxptWEO5So32ApTHA== - -"@swc/core-linux-arm64-gnu@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.53.tgz#00713c4477249192b23a39b30666043b47079eff" - integrity sha512-kREfZdiJH/O8GtJJ22wVN9DVzz/+CPAkw5Mn5te2KQg0xJHMWaESU5XeYMWvtwyOQVmb31b6zCGFy3pnBWWfGw== - -"@swc/core-linux-arm64-musl@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.53.tgz#f310ec81dc566f039520fe1dd583ce1c5a6ec636" - integrity sha512-VeAgomBr6BVuBRjZjRHmvp5gKp1nZgbbd441ca1AvsPd2c+ZyhyHLxTWeHOzBDa/vYnmi9BCwx3QJzFqbAFPVw== - -"@swc/core-linux-x64-gnu@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.53.tgz#a5e7113fbc778019960ca7276f104085adefd513" - integrity sha512-LFX5+QpQkESPkmx860C40pIiYf1utEqoA+WDtmKnUz3DucYvw3eGlXCBdyklP7UBWwJktKIcPlIqr7yROY5VlQ== - -"@swc/core-linux-x64-musl@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.53.tgz#dacc29541f983e9b2d801bf80b1b9e9ac81d5673" - integrity sha512-O0lbJgeaM0VEsG8wFYvpF+Iuf0IENv+LnXHoygkAsv67sVW54+gFxav2sEdkftD5qYe9ku4tmtTVYRZlFgC84Q== - -"@swc/core-win32-arm64-msvc@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.53.tgz#8d0b4cc7530512d9d390f6f90faecba8f4c93ada" - integrity sha512-7PgvPl0aNLaFZSK+rIi4DB1g0aW2qOsTIJQSJGRszsCP8pze/traXymyuSG2I3y9Hx7Z+bP5ycJydyAgCw88WA== - -"@swc/core-win32-ia32-msvc@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.53.tgz#88fc90d99620bd2f18ccb2bc6ee36bcee6dcee44" - integrity sha512-T+OacGm69t8+1mt1sHlwhREiFiFgSeIGL3h11FIs8o2zKnOr5z2H9myzR432X8WuHGVQAOCMvDu53LCMBD0ZzQ== - -"@swc/core-win32-x64-msvc@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.53.tgz#6dda89cdaa5fad14a25a8e2ed0076497ba3a8906" - integrity sha512-uV1/GhROJ/SXzj+f+kKcVtR2GuAiggvbqepzZS46+G47okf6229hr2T1fjmiwYyA75w9R3Bj/wil4UhodohOLg== - -"@swc/core@1.3.53": - version "1.3.53" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.53.tgz#22c20bbd328e4a091eb015285aeffca4c66fd32c" - integrity sha512-OM5nCfKDZXr1HjxD072Jlx5463tPX7xeY7NDSRE3X4KFlkRDFdyMWAyV3pet1oouOfUNrzzoVTAR4XSU8ytO6Q== +"@swc/core-darwin-arm64@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.54.tgz#fc44ebc4ea8f7145b817d6cf43c6533e1d3b5673" + integrity sha512-B1nTlkhTfzt95C/p+iWRaqwHTqqVWcqpHatnieIKxMCaceuZjvd7PeddbHmmrFFfjQqfs8UNzwrTVpgsFwCJWg== + +"@swc/core-darwin-x64@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.54.tgz#4151b59ed71eb84997cc4e7d5dc66129ba5f9241" + integrity sha512-FM+46dRyvJySCrDIDARamycjl/LB9taQCFQg5QpQmQFwIkZ7GIaiQUZO9cdrZUBGPJJCsm7o2FG4ufwnVsCHHA== + +"@swc/core-linux-arm-gnueabihf@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.54.tgz#40eb4ffdff1c0c89351e4f18203b3bea0c537ad9" + integrity sha512-5EfxTKllNm+ucjuo3WNhwHfY417UwqCsaqjJnveXgn5DU+6/ft96skdpD+xrBp5ejMNzem8xYambOrBLzs2QDw== + +"@swc/core-linux-arm64-gnu@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.54.tgz#a11d87c027bdd21eb118a8e999f801dc25b78dde" + integrity sha512-4AmBlRaelUYcW9aR/q3z8UiNYsQ/1JtinETCZBye08wnwK81ITriYcHTBPP8Ix8IZtkcvpsN/SsGAnLNcrpRBQ== + +"@swc/core-linux-arm64-musl@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.54.tgz#e3f05554bc31acc63a1b8c47f5e40ee399d4fddf" + integrity sha512-twgmC6wfH2WW5mlG/rHk4GnbDZ2UDetF0PMJt/ICmgBwJsDSf71WSI9CA5b/F1wE9DE0ofyo+yukjuj9TtPvOQ== + +"@swc/core-linux-x64-gnu@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.54.tgz#d8ea5752aad99063f218c8abc3f534dabc1e7265" + integrity sha512-/dE+z0pF1R0vvXPTQ43+cypltfbZDI0C3WuDMkb1KIaQS7yiaufzzWIVGfJisoKGxpyk3ABSSPcStEqedpvJ+w== + +"@swc/core-linux-x64-musl@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.54.tgz#6bbf414f678d8425fe6fdd016f2eb531efd2e175" + integrity sha512-/dGU5pCaPYEaaPAPt+Fa+9cCTNuhmtdTpxwUX0ZF3VlWAf3QPoTcPYkUDEOvWcl3g8RpEr7ZGp9beVCp2NpLpA== + +"@swc/core-win32-arm64-msvc@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.54.tgz#5a902ca2ed299d023927a4e23b03ce68175c731d" + integrity sha512-gpqOqHB7fHqTApS8pvW1/oMCcfEH1LxRU81KqUGY/MXR3DDwpnDY3ycyCzRB4tpNr91ZpgzUtrlx8OQCHq75uw== + +"@swc/core-win32-ia32-msvc@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.54.tgz#d1ca31ef8c086e78173ed9f85d71680f25fd30d9" + integrity sha512-2h+EnYoEDZcPfoGCKU4F/gbDz0Dw+vQG7b5rxwo6IWFhB2mGOMiYZzbIo6LVyFRoSXNVNa88Yov18pgfeM9gMw== + +"@swc/core-win32-x64-msvc@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.54.tgz#747598f954ee1183d1ccfb31bcf0418b519ef013" + integrity sha512-//W/mqIVuOxMRVI3ojCF6jKLpugu6sSSsCaBpM9GdDVGAaAt0UdivKzecqHIt2PGoN6l1mT73Zy+083PvkPhHQ== + +"@swc/core@1.3.54": + version "1.3.54" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.54.tgz#dc55291d97985cd9408f9d38fbe2a6a3f8cf3058" + integrity sha512-/BCIn698CHkQz3d3VY5W5BHagbwkKen295mDM+JPfDepLsLy3lKtD1+GZqZYKVQDebUj90eAn7OD+qBrAxPtaA== optionalDependencies: - "@swc/core-darwin-arm64" "1.3.53" - "@swc/core-darwin-x64" "1.3.53" - "@swc/core-linux-arm-gnueabihf" "1.3.53" - "@swc/core-linux-arm64-gnu" "1.3.53" - "@swc/core-linux-arm64-musl" "1.3.53" - "@swc/core-linux-x64-gnu" "1.3.53" - "@swc/core-linux-x64-musl" "1.3.53" - "@swc/core-win32-arm64-msvc" "1.3.53" - "@swc/core-win32-ia32-msvc" "1.3.53" - "@swc/core-win32-x64-msvc" "1.3.53" + "@swc/core-darwin-arm64" "1.3.54" + "@swc/core-darwin-x64" "1.3.54" + "@swc/core-linux-arm-gnueabihf" "1.3.54" + "@swc/core-linux-arm64-gnu" "1.3.54" + "@swc/core-linux-arm64-musl" "1.3.54" + "@swc/core-linux-x64-gnu" "1.3.54" + "@swc/core-linux-x64-musl" "1.3.54" + "@swc/core-win32-arm64-msvc" "1.3.54" + "@swc/core-win32-ia32-msvc" "1.3.54" + "@swc/core-win32-x64-msvc" "1.3.54" "@swc/jest@0.2.26": version "0.2.26" @@ -5513,15 +5513,15 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.0.tgz#c0e10eeb936debe5d1c3433cf36206a95befefd0" - integrity sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw== +"@typescript-eslint/eslint-plugin@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08" + integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/type-utils" "5.59.0" - "@typescript-eslint/utils" "5.59.0" + "@typescript-eslint/scope-manager" "5.59.1" + "@typescript-eslint/type-utils" "5.59.1" + "@typescript-eslint/utils" "5.59.1" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -5536,14 +5536,14 @@ dependencies: "@typescript-eslint/utils" "5.25.0" -"@typescript-eslint/parser@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.0.tgz#0ad7cd019346cc5d150363f64869eca10ca9977c" - integrity sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w== +"@typescript-eslint/parser@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4" + integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g== dependencies: - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/typescript-estree" "5.59.0" + "@typescript-eslint/scope-manager" "5.59.1" + "@typescript-eslint/types" "5.59.1" + "@typescript-eslint/typescript-estree" "5.59.1" debug "^4.3.4" "@typescript-eslint/scope-manager@5.17.0": @@ -5570,13 +5570,21 @@ "@typescript-eslint/types" "5.59.0" "@typescript-eslint/visitor-keys" "5.59.0" -"@typescript-eslint/type-utils@5.59.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.0.tgz#8e8d1420fc2265989fa3a0d897bde37f3851e8c9" - integrity sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA== +"@typescript-eslint/scope-manager@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz#8a20222719cebc5198618a5d44113705b51fd7fe" + integrity sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA== dependencies: - "@typescript-eslint/typescript-estree" "5.59.0" - "@typescript-eslint/utils" "5.59.0" + "@typescript-eslint/types" "5.59.1" + "@typescript-eslint/visitor-keys" "5.59.1" + +"@typescript-eslint/type-utils@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111" + integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw== + dependencies: + "@typescript-eslint/typescript-estree" "5.59.1" + "@typescript-eslint/utils" "5.59.1" debug "^4.3.4" tsutils "^3.21.0" @@ -5595,6 +5603,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== +"@typescript-eslint/types@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d" + integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg== + "@typescript-eslint/typescript-estree@5.17.0": version "5.17.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.17.0.tgz#a7cba7dfc8f9cc2ac78c18584e684507df4f2488" @@ -5634,6 +5647,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c" + integrity sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA== + dependencies: + "@typescript-eslint/types" "5.59.1" + "@typescript-eslint/visitor-keys" "5.59.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.25.0": version "5.25.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.25.0.tgz#272751fd737733294b4ab95e16c7f2d4a75c2049" @@ -5646,17 +5672,17 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/utils@5.59.0", "@typescript-eslint/utils@^5.58.0": - version "5.59.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.0.tgz#063d066b3bc4850c18872649ed0da9ee72d833d5" - integrity sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA== +"@typescript-eslint/utils@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473" + integrity sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.0" - "@typescript-eslint/types" "5.59.0" - "@typescript-eslint/typescript-estree" "5.59.0" + "@typescript-eslint/scope-manager" "5.59.1" + "@typescript-eslint/types" "5.59.1" + "@typescript-eslint/typescript-estree" "5.59.1" eslint-scope "^5.1.1" semver "^7.3.7" @@ -5672,6 +5698,20 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" +"@typescript-eslint/utils@^5.58.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.0.tgz#063d066b3bc4850c18872649ed0da9ee72d833d5" + integrity sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.59.0" + "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/typescript-estree" "5.59.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + "@typescript-eslint/visitor-keys@5.17.0": version "5.17.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.17.0.tgz#52daae45c61b0211b4c81b53a71841911e479128" @@ -5696,6 +5736,14 @@ "@typescript-eslint/types" "5.59.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.59.1": + version "5.59.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058" + integrity sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA== + dependencies: + "@typescript-eslint/types" "5.59.1" + eslint-visitor-keys "^3.3.0" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -18731,34 +18779,34 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== -stylelint-config-recommended-scss@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-10.0.0.tgz#06c5c6ad893d2641d7207994de3a5aa2fdcb4078" - integrity sha512-+YvPgUHi0W5mCJCKdupBCIsWPYNbWuJcRmFtSYujwNg+41ljFknhO9bpY6C+oahv659zW7W1AT7i6DQvJYYr1A== +stylelint-config-recommended-scss@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-11.0.0.tgz#7b933ecac99cd3b52d14d1746e3ecd36f421b4b6" + integrity sha512-EDghTDU7aOv2LTsRZvcT1w8mcjUaMhuy+t38iV5I/0Qiu6ixdkRwhLEMul3K/fnB2v9Nwqvb3xpvJfPH+HduDw== dependencies: postcss-scss "^4.0.6" - stylelint-config-recommended "^11.0.0" + stylelint-config-recommended "^12.0.0" stylelint-scss "^4.6.0" -stylelint-config-recommended@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-11.0.0.tgz#b1cb7d71bd92f9b8593f93c2ca6df16ed7d61522" - integrity sha512-SoGIHNI748OCZn6BxFYT83ytWoYETCINVHV3LKScVAWQQauWdvmdDqJC5YXWjpBbxg2E761Tg5aUGKLFOVhEkA== +stylelint-config-recommended@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-12.0.0.tgz#d0993232fca017065fd5acfcb52dd8a188784ef4" + integrity sha512-x6x8QNARrGO2sG6iURkzqL+Dp+4bJorPMMRNPScdvaUK8PsynriOcMW7AFDKqkWAS5wbue/u8fUT/4ynzcmqdQ== -stylelint-config-standard-scss@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-8.0.0.tgz#6540562178436346298bd63f67bb3a7d311388f0" - integrity sha512-TDT/gJD/0LUDoUgkjF1uoI/4DfczXHxg7gJVcWT4/JbE6k5hszVuI14reNX+tEwSyMNhcK2BA7izrK+uVAz7XA== +stylelint-config-standard-scss@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-9.0.0.tgz#70c66e1179612519fdf6ca1dbff23c804def1b6b" + integrity sha512-yPKpJsrZn4ybuQZx/DkEHuCjw7pJginErE/47dFhCnrvD48IJ4UYec8tSiCuJWMA3HRjbIa3nh5ZeSauDGuVAg== dependencies: - stylelint-config-recommended-scss "^10.0.0" - stylelint-config-standard "^32.0.0" + stylelint-config-recommended-scss "^11.0.0" + stylelint-config-standard "^33.0.0" -stylelint-config-standard@^32.0.0: - version "32.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-32.0.0.tgz#97179035e967f22a7b7e27f14a74f5d5fc0f0bd6" - integrity sha512-UnGJxYDyYFrIE9CjDMZRkrNh2o4lOtO+MVZ9qG5b8yARfsWho0GMx4YvhHfsv8zKKgHeWX2wfeyxmuoqcaYZ4w== +stylelint-config-standard@^33.0.0: + version "33.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-33.0.0.tgz#1f7bb299153a53874073e93829e37a475842f0f9" + integrity sha512-eyxnLWoXImUn77+ODIuW9qXBDNM+ALN68L3wT1lN2oNspZ7D9NVGlNHb2QCUn4xDug6VZLsh0tF8NyoYzkgTzg== dependencies: - stylelint-config-recommended "^11.0.0" + stylelint-config-recommended "^12.0.0" stylelint-scss@^4.6.0: version "4.6.0"