Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate remaining share experiment command with the extension (exp push) #3781

Merged
merged 6 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand All @@ -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

Expand Down
15 changes: 10 additions & 5 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,8 @@
"icon": "$(play)"
},
{
"title": "Share to Studio",
"command": "dvc.views.experiments.shareExperimentToStudio",
"title": "Push",
"command": "dvc.views.experiments.pushExperiment",
"category": "DVC",
"icon": "$(repo-push)"
},
Expand Down Expand Up @@ -564,6 +564,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",
Expand Down Expand Up @@ -599,7 +604,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
}
Expand Down Expand Up @@ -883,7 +888,7 @@
"when": "false"
},
{
"command": "dvc.views.experiments.shareExperimentToStudio",
"command": "dvc.views.experiments.pushExperiment",
"when": "false"
},
{
Expand Down Expand Up @@ -1147,7 +1152,7 @@
"when": "view == dvc.views.experimentsTree && dvc.commands.available && viewItem =~ /^(experiment|queued)$/ && !dvc.experiment.running"
},
{
"command": "dvc.views.experiments.shareExperimentToStudio",
"command": "dvc.views.experiments.pushExperiment",
"group": "1_share@0",
"when": "view == dvc.views.experimentsTree && dvc.commands.available && viewItem == experiment && !dvc.experiment.running"
},
Expand Down
1 change: 0 additions & 1 deletion extension/src/commands/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,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',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EXPERIMENT_VIEW_PUSH was already set above 😕

SETUP_SHOW_STUDIO_CONNECT = 'dvc.showStudioConnect',
SETUP_SHOW_STUDIO_SETTINGS = 'dvc.showStudioSettings',

Expand Down
69 changes: 54 additions & 15 deletions extension/src/experiments/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,76 @@ 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 =
(experiments: WorkspaceExperiments) =>
(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 }) => {
const studioAccessToken = setup.getStudioAccessToken()
if (!studioAccessToken) {
return commands.executeCommand(RegisteredCommands.SETUP_SHOW)
if (
!(
getConfigValue(ConfigKey.DO_NOT_RECOMMEND_ADD_STUDIO_TOKEN) ||
studioAccessToken
)
) {
void promptToAddStudioToken()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[F] This is non-blocking. User can push as many experiments as they like without setting the token.

}

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 ${id}...` })

const remainingProgress = 75

const stdout = await internalCommands.executeCommand(
AvailableCommands.EXP_PUSH,
dvcRoot,
id
)

progress.report({
increment: remainingProgress,
message: convertUrlTextToLink(stdout)
})

return Toast.delayProgressClosing(15000)
})
}
11 changes: 4 additions & 7 deletions extension/src/experiments/commands/register.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -292,9 +289,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(
Expand Down
4 changes: 2 additions & 2 deletions extension/src/experiments/webview/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ 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,
RegisteredCliCommands.EXPERIMENT_VIEW_PUSH,
{ dvcRoot: this.dvcRoot, id: message.payload }
)

Expand Down
1 change: 0 additions & 1 deletion extension/src/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,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
Expand Down
51 changes: 41 additions & 10 deletions extension/src/test/suite/experiments/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
workspace,
Uri,
QuickPickItem,
ViewColumn
ViewColumn,
CancellationToken
} from 'vscode'
import { buildExperiments, stubWorkspaceExperimentsGetters } from './util'
import { Disposable } from '../../../extension'
Expand All @@ -34,6 +35,7 @@ import {
import {
buildInternalCommands,
buildMockExperimentsData,
bypassProgressCloseDelay,
closeAllEditors,
configurationChangeEvent,
experimentsUpdatedEvent,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand All @@ -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,
Expand All @@ -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
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()
Expand All @@ -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
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 () => {
Expand Down
3 changes: 2 additions & 1 deletion extension/src/vscode/config.ts
Original file line number Diff line number Diff line change
@@ -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'
}
Expand Down
4 changes: 2 additions & 2 deletions extension/src/webview/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -154,7 +154,7 @@ export type MessageFromWebview =
}
| { type: MessageFromWebviewType.SHOW_EXPERIMENT_LOGS; payload: string }
| {
type: MessageFromWebviewType.SHARE_EXPERIMENT_TO_STUDIO
type: MessageFromWebviewType.PUSH_EXPERIMENT
payload: string
}
| {
Expand Down
Loading