Skip to content

Commit 13825ee

Browse files
authored
Dynamic Get Started webview (#2894)
* Make the Get Started webview a class * Add test * Add other test * Make the get started webview dynamic * Flow from not installed to show experiments * Remove interval * Fix timer problems for jest tests * Fix all tests * Add tests for webview * Revert demo project * Revert demo * Add get started test * Replace currently open folder to current workspace
1 parent 1b22385 commit 13825ee

File tree

18 files changed

+354
-68
lines changed

18 files changed

+354
-68
lines changed

extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1375,7 +1375,7 @@
13751375
},
13761376
{
13771377
"view": "dvc.views.welcome",
1378-
"contents": "The currently open folder does not contain a DVC project. You can initialize a project which will enable features powered by DVC.\n[Initialize Project](command:dvc.init)\nTo learn more about how to use DVC please read [our docs](https://dvc.org/doc).",
1378+
"contents": "The current workspace does not contain a DVC project. You can initialize a project which will enable features powered by DVC.\n[Initialize Project](command:dvc.init)\nTo learn more about how to use DVC please read [our docs](https://dvc.org/doc).",
13791379
"when": "dvc.commands.available && !dvc.cli.incompatible && !dvc.project.available"
13801380
},
13811381
{

extension/src/cli/dvc/discovery.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,17 @@ const processVersionDetails = (
159159

160160
const tryGlobalFallbackVersion = async (
161161
extension: IExtension,
162-
cwd: string
162+
cwd: string,
163+
recheck: boolean
163164
): Promise<CanRunCli> => {
164165
const tryGlobal = await getVersionDetails(extension, cwd, true)
165166
const { cliCompatible, isAvailable, isCompatible, version } = tryGlobal
166167

167-
if (extension.hasRoots() && !isCompatible) {
168+
if (!recheck && extension.hasRoots() && !isCompatible) {
168169
warnUserCLIInaccessibleAnywhere(extension, version)
169170
}
170171
if (
172+
!recheck &&
171173
extension.hasRoots() &&
172174
cliCompatible === CliCompatible.YES_MINOR_VERSION_AHEAD_OF_TESTED
173175
) {
@@ -183,7 +185,8 @@ const tryGlobalFallbackVersion = async (
183185

184186
const extensionCanAutoRunCli = async (
185187
extension: IExtension,
186-
cwd: string
188+
cwd: string,
189+
recheck: boolean
187190
): Promise<CanRunCli> => {
188191
const {
189192
cliCompatible: pythonCliCompatible,
@@ -193,7 +196,7 @@ const extensionCanAutoRunCli = async (
193196
} = await getVersionDetails(extension, cwd)
194197

195198
if (pythonCliCompatible === CliCompatible.NO_NOT_FOUND) {
196-
return tryGlobalFallbackVersion(extension, cwd)
199+
return tryGlobalFallbackVersion(extension, cwd, recheck)
197200
}
198201
return processVersionDetails(
199202
extension,
@@ -206,10 +209,11 @@ const extensionCanAutoRunCli = async (
206209

207210
export const extensionCanRunCli = async (
208211
extension: IExtension,
209-
cwd: string
212+
cwd: string,
213+
recheck: boolean
210214
): Promise<CanRunCli> => {
211215
if (await extension.isPythonExtensionUsed()) {
212-
return extensionCanAutoRunCli(extension, cwd)
216+
return extensionCanAutoRunCli(extension, cwd, recheck)
213217
}
214218

215219
const { cliCompatible, isAvailable, isCompatible, version } =

extension/src/extension.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,14 @@ export class Extension extends Disposable implements IExtension {
156156
)
157157

158158
this.getStarted = this.dispose.track(
159-
new GetStarted('', this.internalCommands, this.resourceLocator.dvcIcon)
159+
new GetStarted(
160+
'',
161+
this.resourceLocator.dvcIcon,
162+
() => this.initProject(),
163+
() => this.showExperiments(this.dvcRoots[0]),
164+
() => this.getAvailable(),
165+
() => this.hasRoots()
166+
)
160167
)
161168

162169
this.repositories = this.dispose.track(
@@ -247,10 +254,7 @@ export class Extension extends Disposable implements IExtension {
247254
this.internalCommands.registerExternalCommand(
248255
RegisteredCommands.EXPERIMENT_AND_PLOTS_SHOW,
249256
async (context: VsCodeContext) => {
250-
await this.experiments.showWebview(
251-
getDvcRootFromContext(context),
252-
ViewColumn.Active
253-
)
257+
await this.showExperiments(getDvcRootFromContext(context))
254258
await this.plots.showWebview(
255259
getDvcRootFromContext(context),
256260
ViewColumn.Beside
@@ -301,13 +305,7 @@ export class Extension extends Disposable implements IExtension {
301305

302306
this.internalCommands.registerExternalCliCommand(
303307
RegisteredCliCommands.INIT,
304-
async () => {
305-
const root = getFirstWorkspaceFolder()
306-
if (root) {
307-
await this.dvcExecutor.init(root)
308-
this.workspaceChanged.fire()
309-
}
310-
}
308+
() => this.initProject()
311309
)
312310

313311
registerRepositoryCommands(this.repositories, this.internalCommands)
@@ -389,6 +387,7 @@ export class Extension extends Disposable implements IExtension {
389387
)
390388
this.dvcRoots = nestedRoots.flat().sort()
391389

390+
this.getStarted.sendDataToWebview()
392391
return this.setProjectAvailability()
393392
}
394393

@@ -443,9 +442,26 @@ export class Extension extends Disposable implements IExtension {
443442
this.status.setAvailability(available)
444443
this.setCommandsAvailability(available)
445444
this.cliAccessible = available
445+
this.getStarted.sendDataToWebview()
446446
return available
447447
}
448448

449+
public getAvailable() {
450+
return this.cliAccessible
451+
}
452+
453+
public async initProject() {
454+
const root = getFirstWorkspaceFolder()
455+
if (root) {
456+
await this.dvcExecutor.init(root)
457+
this.workspaceChanged.fire()
458+
}
459+
}
460+
461+
private async showExperiments(dvcRoot: string) {
462+
return await this.experiments.showWebview(dvcRoot, ViewColumn.Active)
463+
}
464+
449465
private setCommandsAvailability(available: boolean) {
450466
setContextValue('dvc.commands.available', available)
451467
}

extension/src/getStarted/index.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,55 @@ import { BaseWebview } from '../webview'
44
import { ViewKey } from '../webview/constants'
55
import { BaseRepository } from '../webview/repository'
66
import { Resource } from '../resourceLocator'
7-
import { AvailableCommands, InternalCommands } from '../commands/internal'
87

98
export type GetStartedWebview = BaseWebview<TGetStartedData>
109

1110
export class GetStarted extends BaseRepository<TGetStartedData> {
1211
public readonly viewKey = ViewKey.GET_STARTED
1312

14-
private internalCommands: InternalCommands
13+
private webviewMessages: WebviewMessages
14+
private initProject: () => void
15+
private showExperiments: () => void
16+
private getCliAccessible: () => boolean
17+
private getHasRoots: () => boolean
1518

1619
constructor(
1720
dvcRoot: string,
18-
internalCommands: InternalCommands,
19-
webviewIcon: Resource
21+
webviewIcon: Resource,
22+
initProject: () => void,
23+
showExperiments: () => void,
24+
getCliAccessible: () => boolean,
25+
getHasRoots: () => boolean
2026
) {
2127
super(dvcRoot, webviewIcon)
2228

23-
this.createWebviewMessageHandler()
24-
this.internalCommands = internalCommands
29+
this.webviewMessages = this.createWebviewMessageHandler()
30+
31+
if (this.webview) {
32+
this.sendDataToWebview()
33+
}
34+
this.getCliAccessible = getCliAccessible
35+
this.getHasRoots = getHasRoots
36+
this.initProject = initProject
37+
this.showExperiments = showExperiments
2538
}
2639

2740
public sendInitialWebviewData() {
28-
return undefined
41+
return this.sendDataToWebview()
42+
}
43+
44+
public sendDataToWebview() {
45+
this.webviewMessages.sendWebviewMessage(
46+
this.getCliAccessible(),
47+
this.getHasRoots()
48+
)
2949
}
3050

3151
private createWebviewMessageHandler() {
3252
const webviewMessages = new WebviewMessages(
3353
() => this.getWebview(),
34-
() => this.initializeProject()
54+
() => this.initProject(),
55+
() => this.showExperiments()
3556
)
3657
this.dispose.track(
3758
this.onDidReceivedWebviewMessage(message =>
@@ -40,8 +61,4 @@ export class GetStarted extends BaseRepository<TGetStartedData> {
4061
)
4162
return webviewMessages
4263
}
43-
44-
private initializeProject() {
45-
this.internalCommands.executeCommand(AvailableCommands.INIT)
46-
}
4764
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export type GetStartedData = undefined
1+
export type GetStartedData = {
2+
cliAccessible: boolean
3+
projectInitialized: boolean
4+
}

extension/src/getStarted/webview/messages.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,35 @@ import { BaseWebview } from '../../webview'
99
export class WebviewMessages {
1010
private readonly getWebview: () => BaseWebview<TGetStartedData> | undefined
1111
private readonly initializeProject: () => void
12+
private readonly openExperiments: () => void
1213

1314
constructor(
1415
getWebview: () => BaseWebview<TGetStartedData> | undefined,
15-
initializeProject: () => void
16+
initializeProject: () => void,
17+
openExperiments: () => void
1618
) {
1719
this.getWebview = getWebview
1820
this.initializeProject = initializeProject
21+
this.openExperiments = openExperiments
1922
}
2023

21-
public sendWebviewMessage() {
22-
this.getWebview()?.show(undefined)
24+
public sendWebviewMessage(
25+
cliAccessible: boolean,
26+
projectInitialized: boolean
27+
) {
28+
this.getWebview()?.show({
29+
cliAccessible,
30+
projectInitialized
31+
})
2332
}
2433

2534
public handleMessageFromWebview(message: MessageFromWebview) {
2635
if (message.type === MessageFromWebviewType.INITIALIZE_PROJECT) {
2736
return this.initializeProject()
2837
}
38+
if (message.type === MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW) {
39+
return this.openExperiments()
40+
}
2941
Logger.error(`Unexpected message: ${JSON.stringify(message)}`)
3042
}
3143
}

extension/src/setup.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jest.mock('./vscode/quickPick')
3232
jest.mock('./vscode/toast')
3333
jest.mock('./vscode/workspaceFolders')
3434
jest.mock('./processExecution')
35+
jest.mock('./setupUtil')
3536

3637
const mockedExtensions = jest.mocked(extensions)
3738
const mockedCommands = jest.mocked(commands)

extension/src/setup.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getFirstWorkspaceFolder } from './vscode/workspaceFolders'
1010
import { getSelectTitle, Title } from './vscode/title'
1111
import { isPythonExtensionInstalled } from './extensions/python'
1212
import { extensionCanRunCli } from './cli/dvc/discovery'
13+
import { willRecheck } from './setupUtil'
1314

1415
const setConfigPath = async (
1516
option: ConfigKey,
@@ -153,20 +154,15 @@ export const setupWorkspace = async (): Promise<boolean> => {
153154
return pickCliPath()
154155
}
155156

156-
export const setup = async (extension: IExtension) => {
157-
const cwd = getFirstWorkspaceFolder()
158-
if (!cwd) {
159-
return
160-
}
161-
162-
await extension.setRoots()
163-
164-
const roots = extension.getRoots()
165-
const dvcRootOrFirstFolder = roots.length > 0 ? roots[0] : cwd
166-
157+
export const checkAvailable = async (
158+
extension: IExtension,
159+
dvcRootOrFirstFolder: string,
160+
recheck = false
161+
) => {
167162
const { isAvailable, isCompatible } = await extensionCanRunCli(
168163
extension,
169-
dvcRootOrFirstFolder
164+
dvcRootOrFirstFolder,
165+
recheck
170166
)
171167

172168
extension.setCliCompatible(isCompatible)
@@ -180,5 +176,20 @@ export const setup = async (extension: IExtension) => {
180176

181177
if (!isAvailable) {
182178
extension.setAvailable(false)
179+
willRecheck(extension, dvcRootOrFirstFolder)
180+
}
181+
}
182+
183+
export const setup = async (extension: IExtension) => {
184+
const cwd = getFirstWorkspaceFolder()
185+
if (!cwd) {
186+
return
183187
}
188+
189+
await extension.setRoots()
190+
191+
const roots = extension.getRoots()
192+
const dvcRootOrFirstFolder = roots.length > 0 ? roots[0] : cwd
193+
194+
return checkAvailable(extension, dvcRootOrFirstFolder)
184195
}

extension/src/setupUtil.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IExtension } from './interfaces'
2+
import { checkAvailable } from './setup'
3+
4+
export const willRecheck = (
5+
extension: IExtension,
6+
dvcRootOrFirstFolder: string
7+
) => {
8+
setTimeout(() => checkAvailable(extension, dvcRootOrFirstFolder, true), 5000)
9+
}

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { buildGetStarted } from './util'
55
import { closeAllEditors, getMessageReceivedEmitter } from '../util'
66
import { WEBVIEW_TEST_TIMEOUT } from '../timeouts'
77
import { MessageFromWebviewType } from '../../../webview/contract'
8-
import { AvailableCommands } from '../../../commands/internal'
98
import { Disposable } from '../../../extension'
109
import { Logger } from '../../../common/logger'
1110

@@ -24,25 +23,37 @@ suite('GetStarted Test Suite', () => {
2423

2524
describe('Get Started', () => {
2625
it('should handle a initialize project message from the webview', async () => {
27-
const { messageSpy, getStarted, internalCommands } =
28-
await buildGetStarted(disposable)
26+
const { messageSpy, getStarted, mockInitializeProject } =
27+
await buildGetStarted(disposable, true)
2928

3029
const webview = await getStarted.showWebview()
3130
await webview.isReady()
3231

3332
const mockMessageReceived = getMessageReceivedEmitter(webview)
34-
const mockInitializeProject = spy(internalCommands, 'executeCommand')
3533

3634
messageSpy.resetHistory()
3735
mockMessageReceived.fire({
3836
type: MessageFromWebviewType.INITIALIZE_PROJECT
3937
})
4038

4139
expect(mockInitializeProject).to.be.calledOnce
40+
}).timeout(WEBVIEW_TEST_TIMEOUT)
4241

43-
expect(mockInitializeProject).to.be.calledWithExactly(
44-
AvailableCommands.INIT
45-
)
42+
it('should handle a open experiments message from the webview', async () => {
43+
const { messageSpy, getStarted, mockOpenExperiments } =
44+
await buildGetStarted(disposable, true)
45+
46+
const webview = await getStarted.showWebview()
47+
await webview.isReady()
48+
49+
const mockMessageReceived = getMessageReceivedEmitter(webview)
50+
51+
messageSpy.resetHistory()
52+
mockMessageReceived.fire({
53+
type: MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW
54+
})
55+
56+
expect(mockOpenExperiments).to.be.calledOnce
4657
}).timeout(WEBVIEW_TEST_TIMEOUT)
4758

4859
it('should log an error message if the message from the webview is anything else than initialize project', async () => {

0 commit comments

Comments
 (0)