Skip to content

Commit ec22bcd

Browse files
authored
Add upgrade dvc button to setup when incompatible (#3904)
1 parent 30935d8 commit ec22bcd

File tree

10 files changed

+215
-22
lines changed

10 files changed

+215
-22
lines changed

extension/src/setup/autoInstall.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,31 @@ export const findPythonBinForInstall = async (): Promise<
1616
)
1717
}
1818

19+
const showUpgradeProgress = (
20+
root: string,
21+
pythonBinPath: string
22+
): Thenable<unknown> =>
23+
Toast.showProgress('Upgrading DVC', async progress => {
24+
progress.report({ increment: 0 })
25+
26+
progress.report({ increment: 25, message: 'Updating packages...' })
27+
28+
try {
29+
await Toast.runCommandAndIncrementProgress(
30+
async () => {
31+
await installPackages(root, pythonBinPath, 'dvc')
32+
return 'Upgraded successfully'
33+
},
34+
progress,
35+
75
36+
)
37+
38+
return Toast.delayProgressClosing()
39+
} catch (error: unknown) {
40+
return Toast.reportProgressError(error, progress)
41+
}
42+
})
43+
1944
const showInstallProgress = (
2045
root: string,
2146
pythonBinPath: string
@@ -52,7 +77,9 @@ const showInstallProgress = (
5277
}
5378
})
5479

55-
export const autoInstallDvc = async (): Promise<unknown> => {
80+
const getArgsAndRunCommand = async (
81+
command: (root: string, pythonBinPath: string) => Thenable<unknown>
82+
): Promise<unknown> => {
5683
const pythonBinPath = await findPythonBinForInstall()
5784
const root = getFirstWorkspaceFolder()
5885

@@ -68,5 +95,13 @@ export const autoInstallDvc = async (): Promise<unknown> => {
6895
)
6996
}
7097

71-
return showInstallProgress(root, pythonBinPath)
98+
return command(root, pythonBinPath)
99+
}
100+
101+
export const autoInstallDvc = (): Promise<unknown> => {
102+
return getArgsAndRunCommand(showInstallProgress)
103+
}
104+
105+
export const autoUpgradeDvc = (): Promise<unknown> => {
106+
return getArgsAndRunCommand(showUpgradeProgress)
72107
}

extension/src/setup/webview/messages.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { BaseWebview } from '../../webview'
99
import { sendTelemetryEvent } from '../../telemetry'
1010
import { EventName } from '../../telemetry/constants'
1111
import { selectPythonInterpreter } from '../../extensions/python'
12-
import { autoInstallDvc } from '../autoInstall'
12+
import { autoInstallDvc, autoUpgradeDvc } from '../autoInstall'
1313
import {
1414
RegisteredCliCommands,
1515
RegisteredCommands
@@ -79,6 +79,8 @@ export class WebviewMessages {
7979
return this.selectPythonInterpreter()
8080
case MessageFromWebviewType.INSTALL_DVC:
8181
return this.installDvc()
82+
case MessageFromWebviewType.UPGRADE_DVC:
83+
return this.upgradeDvc()
8284
case MessageFromWebviewType.SETUP_WORKSPACE:
8385
return commands.executeCommand(
8486
RegisteredCommands.EXTENSION_SETUP_WORKSPACE
@@ -131,6 +133,12 @@ export class WebviewMessages {
131133
return selectPythonInterpreter()
132134
}
133135

136+
private upgradeDvc() {
137+
sendTelemetryEvent(EventName.VIEWS_SETUP_UPGRADE_DVC, undefined, undefined)
138+
139+
return autoUpgradeDvc()
140+
}
141+
134142
private installDvc() {
135143
sendTelemetryEvent(EventName.VIEWS_SETUP_INSTALL_DVC, undefined, undefined)
136144

extension/src/telemetry/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const EventName = Object.assign(
8383
VIEWS_SETUP_SELECT_PYTHON_INTERPRETER:
8484
'views.setup.selectPythonInterpreter',
8585
VIEWS_SETUP_SHOW_SCM_FOR_COMMIT: 'views.setup.showScmForCommit',
86+
VIEWS_SETUP_UPGRADE_DVC: 'view.setup.upgradeDvc',
8687

8788
VIEWS_TERMINAL_CLOSED: 'views.terminal.closed',
8889
VIEWS_TERMINAL_CREATED: 'views.terminal.created',
@@ -269,6 +270,7 @@ export interface IEventNamePropertyMapping {
269270
[EventName.VIEWS_SETUP_SHOW_SCM_FOR_COMMIT]: undefined
270271
[EventName.VIEWS_SETUP_INIT_GIT]: undefined
271272
[EventName.VIEWS_SETUP_INSTALL_DVC]: undefined
273+
[EventName.VIEWS_SETUP_UPGRADE_DVC]: undefined
272274

273275
[EventName.SETUP_SHOW]: undefined
274276
[EventName.SETUP_SHOW_EXPERIMENTS]: undefined

extension/src/test/suite/setup/autoInstall.test.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { window } from 'vscode'
55
import { Disposable } from '../../../extension'
66
import * as PythonExtension from '../../../extensions/python'
77
import * as Python from '../../../python'
8-
import { autoInstallDvc } from '../../../setup/autoInstall'
8+
import { autoInstallDvc, autoUpgradeDvc } from '../../../setup/autoInstall'
99
import * as WorkspaceFolders from '../../../vscode/workspaceFolders'
1010
import { bypassProgressCloseDelay } from '../util'
1111
import { Toast } from '../../../vscode/toast'
@@ -23,9 +23,98 @@ suite('Auto Install Test Suite', () => {
2323
disposable.dispose()
2424
})
2525

26-
describe('autoInstallDvc', () => {
27-
const defaultPython = getDefaultPython()
26+
const defaultPython = getDefaultPython()
27+
28+
describe('autoUpgradeDvc', () => {
29+
it('should return early if no Python interpreter is found', async () => {
30+
stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined)
31+
stub(Python, 'findPythonBin').resolves(undefined)
32+
const mockInstallPackages = stub(Python, 'installPackages').resolves(
33+
undefined
34+
)
35+
36+
const showProgressSpy = spy(window, 'withProgress')
37+
const showErrorSpy = spy(window, 'showErrorMessage')
38+
39+
await autoUpgradeDvc()
40+
41+
expect(showProgressSpy).not.to.be.called
42+
expect(showErrorSpy).to.be.called
43+
expect(mockInstallPackages).not.to.be.called
44+
})
45+
46+
it('should return early if there is no workspace folder open', async () => {
47+
stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined)
48+
stub(Python, 'findPythonBin').resolves(defaultPython)
49+
const mockInstallPackages = stub(Python, 'installPackages').resolves(
50+
undefined
51+
)
52+
stub(WorkspaceFolders, 'getFirstWorkspaceFolder').returns(undefined)
2853

54+
const showProgressSpy = spy(window, 'withProgress')
55+
const showErrorSpy = spy(window, 'showErrorMessage')
56+
57+
await autoUpgradeDvc()
58+
59+
expect(showProgressSpy).not.to.be.called
60+
expect(showErrorSpy).to.be.called
61+
expect(mockInstallPackages).not.to.be.called
62+
})
63+
64+
it('should install DVC if a Python interpreter is found', async () => {
65+
bypassProgressCloseDelay()
66+
const cwd = __dirname
67+
stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined)
68+
stub(Python, 'findPythonBin').resolves(defaultPython)
69+
const mockInstallPackages = stub(Python, 'installPackages').resolves(
70+
undefined
71+
)
72+
stub(WorkspaceFolders, 'getFirstWorkspaceFolder').returns(cwd)
73+
74+
const showProgressSpy = spy(window, 'withProgress')
75+
const showErrorSpy = spy(window, 'showErrorMessage')
76+
77+
await autoUpgradeDvc()
78+
79+
expect(showProgressSpy).to.be.called
80+
expect(showErrorSpy).not.to.be.called
81+
expect(mockInstallPackages).to.be.called
82+
expect(mockInstallPackages).to.be.calledWithExactly(
83+
cwd,
84+
defaultPython,
85+
'dvc'
86+
)
87+
})
88+
89+
it('should show an error message if DVC fails to install', async () => {
90+
bypassProgressCloseDelay()
91+
const cwd = __dirname
92+
stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined)
93+
stub(Python, 'findPythonBin').resolves(defaultPython)
94+
const mockInstallPackages = stub(Python, 'installPackages')
95+
.onFirstCall()
96+
.rejects(new Error('Network error'))
97+
stub(WorkspaceFolders, 'getFirstWorkspaceFolder').returns(cwd)
98+
99+
const showProgressSpy = spy(window, 'withProgress')
100+
const showErrorSpy = spy(window, 'showErrorMessage')
101+
const reportProgressErrorSpy = spy(Toast, 'reportProgressError')
102+
103+
await autoUpgradeDvc()
104+
105+
expect(showProgressSpy).to.be.called
106+
expect(showErrorSpy).not.to.be.called
107+
expect(reportProgressErrorSpy).to.be.calledOnce
108+
expect(mockInstallPackages).to.be.called
109+
expect(mockInstallPackages).to.be.calledWithExactly(
110+
cwd,
111+
defaultPython,
112+
'dvc'
113+
)
114+
})
115+
})
116+
117+
describe('autoInstallDvc', () => {
29118
it('should return early if no Python interpreter is found', async () => {
30119
stub(PythonExtension, 'getPythonExecutionDetails').resolves(undefined)
31120
stub(Python, 'findPythonBin').resolves(undefined)

extension/src/test/suite/setup/index.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ suite('Setup Test Suite', () => {
138138
expect(mockAutoInstallDvc).to.be.calledOnce
139139
}).timeout(WEBVIEW_TEST_TIMEOUT)
140140

141+
it('should handle an auto upgrade dvc message from the webview', async () => {
142+
const { messageSpy, setup, mockAutoUpgradeDvc } = buildSetup(disposable)
143+
144+
const webview = await setup.showWebview()
145+
await webview.isReady()
146+
147+
const mockMessageReceived = getMessageReceivedEmitter(webview)
148+
149+
messageSpy.resetHistory()
150+
mockMessageReceived.fire({
151+
type: MessageFromWebviewType.UPGRADE_DVC
152+
})
153+
154+
expect(mockAutoUpgradeDvc).to.be.calledOnce
155+
}).timeout(WEBVIEW_TEST_TIMEOUT)
156+
141157
it('should handle a select Python interpreter message from the webview', async () => {
142158
const { messageSpy, mockExecuteCommand, setup } = buildSetup(disposable)
143159
const setInterpreterCommand = 'python.setInterpreter'

extension/src/test/suite/setup/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const buildSetup = (
6262
)
6363

6464
const mockAutoInstallDvc = stub(AutoInstall, 'autoInstallDvc')
65+
const mockAutoUpgradeDvc = stub(AutoInstall, 'autoUpgradeDvc')
6566
stub(AutoInstall, 'findPythonBinForInstall').resolves(undefined)
6667

6768
const mockShowWebview = stub(WorkspaceExperiments.prototype, 'showWebview')
@@ -105,6 +106,7 @@ export const buildSetup = (
105106
internalCommands,
106107
messageSpy,
107108
mockAutoInstallDvc,
109+
mockAutoUpgradeDvc,
108110
mockExecuteCommand,
109111
mockGetGitRepositoryRoot,
110112
mockGlobalVersion,

extension/src/webview/contract.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export enum MessageFromWebviewType {
6262
INITIALIZE_GIT = 'initialize-git',
6363
SHOW_SCM_PANEL = 'show-scm-panel',
6464
INSTALL_DVC = 'install-dvc',
65+
UPGRADE_DVC = 'upgrade-dvc',
6566
SETUP_WORKSPACE = 'setup-workspace',
6667
ZOOM_PLOT = 'zoom-plot',
6768
SHOW_MORE_COMMITS = 'show-more-commits',
@@ -214,6 +215,7 @@ export type MessageFromWebview =
214215
| { type: MessageFromWebviewType.INITIALIZE_GIT }
215216
| { type: MessageFromWebviewType.SHOW_SCM_PANEL }
216217
| { type: MessageFromWebviewType.INSTALL_DVC }
218+
| { type: MessageFromWebviewType.UPGRADE_DVC }
217219
| { type: MessageFromWebviewType.SETUP_WORKSPACE }
218220
| { type: MessageFromWebviewType.OPEN_STUDIO }
219221
| { type: MessageFromWebviewType.OPEN_STUDIO_PROFILE }

webview/src/setup/components/App.test.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ describe('App', () => {
9797
})
9898

9999
expect(screen.getByText('DVC is incompatible')).toBeInTheDocument()
100+
expect(
101+
screen.getByText('Please update your install and try again.')
102+
).toBeInTheDocument()
100103

101104
const button = screen.getByText('Check Compatibility')
102105
expect(button).toBeInTheDocument()
@@ -107,18 +110,27 @@ describe('App', () => {
107110
})
108111
})
109112

110-
it('should show a screen saying that DVC is not installed if the cli is unavailable', () => {
113+
it('should tell the user than they can auto upgrade DVC if DVC is incompatible and python is available', () => {
111114
renderApp({
112-
cliCompatible: undefined,
115+
cliCompatible: false,
113116
dvcCliDetails: {
114117
command: 'dvc',
115-
version: undefined
116-
}
118+
version: '1.0.0'
119+
},
120+
pythonBinPath: 'python'
117121
})
118122

119-
expect(screen.getAllByText('DVC is currently unavailable')).toHaveLength(
120-
3
121-
)
123+
expect(screen.getByText('DVC is incompatible')).toBeInTheDocument()
124+
125+
const compatibityButton = screen.getByText('Check Compatibility')
126+
expect(compatibityButton).toBeInTheDocument()
127+
const upgradeButton = screen.getByText('Upgrade (pip)')
128+
expect(upgradeButton).toBeInTheDocument()
129+
130+
fireEvent.click(upgradeButton)
131+
expect(mockPostMessage).toHaveBeenCalledWith({
132+
type: MessageFromWebviewType.UPGRADE_DVC
133+
})
122134
})
123135

124136
it('should tell the user they cannot install DVC without a Python interpreter', () => {
Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
import React, { PropsWithChildren } from 'react'
2+
import { useSelector } from 'react-redux'
3+
import styles from './styles.module.scss'
24
import { EmptyState } from '../../../shared/components/emptyState/EmptyState'
35
import { Button } from '../../../shared/components/button/Button'
4-
import { checkCompatibility } from '../messages'
6+
import { checkCompatibility, upgradeDvc } from '../messages'
7+
import { SetupState } from '../../store'
58

6-
export const CliIncompatible: React.FC<PropsWithChildren> = ({ children }) => (
7-
<EmptyState isFullScreen={false}>
8-
<div>
9-
<h1>DVC is incompatible</h1>
10-
{children}
9+
export const CliIncompatible: React.FC<PropsWithChildren> = ({ children }) => {
10+
const pythonBinPath = useSelector(
11+
(state: SetupState) => state.dvc.pythonBinPath
12+
)
13+
const canUpgrade = !!pythonBinPath
14+
15+
const conditionalContents = canUpgrade ? (
16+
<>
17+
<div className={styles.sideBySideButtons}>
18+
<Button onClick={upgradeDvc} text="Upgrade (pip)" />
19+
<Button text="Check Compatibility" onClick={checkCompatibility} />
20+
</div>
21+
</>
22+
) : (
23+
<>
1124
<p>Please update your install and try again.</p>
1225
<Button text="Check Compatibility" onClick={checkCompatibility} />
13-
</div>
14-
</EmptyState>
15-
)
26+
</>
27+
)
28+
29+
return (
30+
<EmptyState isFullScreen={false}>
31+
<div>
32+
<h1>DVC is incompatible</h1>
33+
{children}
34+
{conditionalContents}
35+
</div>
36+
</EmptyState>
37+
)
38+
}

webview/src/setup/components/messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const installDvc = () => {
2525
sendMessage({ type: MessageFromWebviewType.INSTALL_DVC })
2626
}
2727

28+
export const upgradeDvc = () => {
29+
sendMessage({ type: MessageFromWebviewType.UPGRADE_DVC })
30+
}
31+
2832
export const selectPythonInterpreter = () => {
2933
sendMessage({ type: MessageFromWebviewType.SELECT_PYTHON_INTERPRETER })
3034
}

0 commit comments

Comments
 (0)