Skip to content

Commit d06c0e3

Browse files
authored
Add Remotes section to setup webview (#3901)
* add Remote section to setup webview * handle workspace with multiple dvc projects * fix integration test * refactor out duplication * reinstate blatted test * make sending data to webview more explicit for config watcher * add stories and tests * remove duplication * add unit test for remote collection * address review comment
1 parent e95cc0e commit d06c0e3

File tree

30 files changed

+573
-126
lines changed

30 files changed

+573
-126
lines changed

extension/src/cli/dvc/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export enum Command {
3030
PUSH = 'push',
3131
QUEUE = 'queue',
3232
REMOVE = 'remove',
33+
REMOTE = 'remote',
3334
ROOT = 'root',
3435
PARAMS = 'params',
3536
METRICS = 'metrics',

extension/src/cli/dvc/executor.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
ExperimentSubCommand,
88
Flag,
99
GcPreserveFlag,
10-
QueueSubCommand
10+
QueueSubCommand,
11+
SubCommand
1112
} from './constants'
1213
import { addStudioAccessToken } from './options'
1314
import { CliResult, CliStarted, typeCheckCommands } from '..'
@@ -35,6 +36,7 @@ export const autoRegisteredCommands = {
3536
QUEUE_KILL: 'queueKill',
3637
QUEUE_START: 'queueStart',
3738
QUEUE_STOP: 'queueStop',
39+
REMOTE: 'remote',
3840
REMOVE: 'remove'
3941
} as const
4042

@@ -196,6 +198,10 @@ export class DvcExecutor extends DvcCli {
196198
return this.blockAndExecuteProcess(cwd, Command.REMOVE, ...args)
197199
}
198200

201+
public remote(cwd: string, arg: typeof SubCommand.LIST) {
202+
return this.executeDvcProcess(cwd, Command.REMOTE, arg)
203+
}
204+
199205
private executeExperimentProcess(cwd: string, ...args: Args) {
200206
return this.executeDvcProcess(cwd, Command.EXPERIMENT, ...args)
201207
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { collectRemoteList } from './collect'
2+
import { dvcDemoPath } from '../test/util'
3+
import { join } from '../test/util/path'
4+
5+
describe('collectRemoteList', () => {
6+
it('should return the expected data structure', async () => {
7+
const mockedRoot = join('some', 'other', 'root')
8+
const remoteList = await collectRemoteList(
9+
[dvcDemoPath, 'mockedOtherRoot', mockedRoot],
10+
root =>
11+
Promise.resolve(
12+
{
13+
[dvcDemoPath]:
14+
'storage s3://dvc-public/remote/mnist-vscode\nbackup gdrive://appDataDir\nurl https://remote.dvc.org/mnist-vscode',
15+
mockedOtherRoot: '',
16+
[mockedRoot]: undefined
17+
}[root]
18+
)
19+
)
20+
expect(remoteList).toStrictEqual({
21+
[dvcDemoPath]: {
22+
backup: 'gdrive://appDataDir',
23+
storage: 's3://dvc-public/remote/mnist-vscode',
24+
url: 'https://remote.dvc.org/mnist-vscode'
25+
},
26+
mockedOtherRoot: undefined,
27+
[mockedRoot]: undefined
28+
})
29+
})
30+
})

extension/src/setup/collect.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { DEFAULT_SECTION_COLLAPSED, SetupSection } from './webview/contract'
1+
import {
2+
DEFAULT_SECTION_COLLAPSED,
3+
RemoteList,
4+
SetupSection
5+
} from './webview/contract'
6+
import { trimAndSplit } from '../util/stdout'
27

38
export const collectSectionCollapsed = (
49
focusedSection?: SetupSection
@@ -16,3 +21,27 @@ export const collectSectionCollapsed = (
1621

1722
return acc
1823
}
24+
25+
export const collectRemoteList = async (
26+
dvcRoots: string[],
27+
getRemoteList: (cwd: string) => Promise<string | undefined>
28+
): Promise<NonNullable<RemoteList>> => {
29+
const acc: NonNullable<RemoteList> = {}
30+
31+
for (const dvcRoot of dvcRoots) {
32+
const remoteList = await getRemoteList(dvcRoot)
33+
if (!remoteList) {
34+
acc[dvcRoot] = undefined
35+
continue
36+
}
37+
const remotes = trimAndSplit(remoteList)
38+
const dvcRootRemotes: { [alias: string]: string } = {}
39+
for (const remote of remotes) {
40+
const [alias, url] = remote.split(/\s+/)
41+
dvcRootRemotes[alias] = url
42+
}
43+
acc[dvcRoot] = dvcRootRemotes
44+
}
45+
46+
return acc
47+
}

extension/src/setup/index.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
SetupSection,
88
SetupData as TSetupData
99
} from './webview/contract'
10-
import { collectSectionCollapsed } from './collect'
10+
import { collectRemoteList, collectSectionCollapsed } from './collect'
1111
import { WebviewMessages } from './webview/messages'
1212
import { validateTokenInput } from './inputBox'
1313
import { findPythonBinForInstall } from './autoInstall'
@@ -47,7 +47,8 @@ import {
4747
Flag,
4848
ConfigKey as DvcConfigKey,
4949
DOT_DVC,
50-
Args
50+
Args,
51+
SubCommand
5152
} from '../cli/dvc/constants'
5253
import { GLOBAL_WEBVIEW_DVCROOT } from '../webview/factory'
5354
import {
@@ -229,7 +230,7 @@ export class Setup
229230
) {
230231
this.cliCompatible = compatible
231232
this.cliVersion = version
232-
void this.updateIsStudioConnected()
233+
void this.updateStudioAndSend()
233234
const incompatible = compatible === undefined ? undefined : !compatible
234235
void setContextValue(ContextKey.CLI_INCOMPATIBLE, incompatible)
235236
}
@@ -339,7 +340,7 @@ export class Setup
339340
}
340341

341342
await this.accessConfig(cwd, Flag.GLOBAL, DvcConfigKey.STUDIO_TOKEN, token)
342-
return this.updateIsStudioConnected()
343+
return this.updateStudioAndSend()
343344
}
344345

345346
public getStudioLiveShareToken() {
@@ -375,14 +376,28 @@ export class Setup
375376
}
376377
}
377378

379+
private async getRemoteList() {
380+
await this.config.isReady()
381+
382+
if (!this.hasRoots()) {
383+
return undefined
384+
}
385+
386+
return collectRemoteList(this.dvcRoots, (cwd: string) =>
387+
this.accessRemote(cwd, SubCommand.LIST)
388+
)
389+
}
390+
378391
private async sendDataToWebview() {
379392
const projectInitialized = this.hasRoots()
380393
const hasData = this.getHasData()
381394

382-
const [isPythonExtensionUsed, dvcCliDetails] = await Promise.all([
383-
this.isPythonExtensionUsed(),
384-
this.getDvcCliDetails()
385-
])
395+
const [isPythonExtensionUsed, dvcCliDetails, remoteList] =
396+
await Promise.all([
397+
this.isPythonExtensionUsed(),
398+
this.getDvcCliDetails(),
399+
this.getRemoteList()
400+
])
386401

387402
const needsGitInitialized =
388403
!projectInitialized && !!(await this.needsGitInit())
@@ -405,6 +420,7 @@ export class Setup
405420
needsGitInitialized,
406421
projectInitialized,
407422
pythonBinPath: getBinDisplayText(pythonBinPath),
423+
remoteList,
408424
sectionCollapsed: collectSectionCollapsed(this.focusedSection),
409425
shareLiveToStudio: getConfigValue(
410426
ExtensionConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE
@@ -644,16 +660,16 @@ export class Setup
644660
}
645661
}
646662

663+
private async updateStudioAndSend() {
664+
await this.updateIsStudioConnected()
665+
return this.sendDataToWebview()
666+
}
667+
647668
private async updateIsStudioConnected() {
648669
await this.setStudioAccessToken()
649670
const storedToken = this.getStudioAccessToken()
650671
const isConnected = isStudioAccessToken(storedToken)
651-
return this.setStudioIsConnected(isConnected)
652-
}
653-
654-
private setStudioIsConnected(isConnected: boolean) {
655672
this.studioIsConnected = isConnected
656-
void this.sendDataToWebview()
657673
return setContextValue(ContextKey.STUDIO_CONNECTED, isConnected)
658674
}
659675

@@ -667,7 +683,7 @@ export class Setup
667683
path.endsWith(join('dvc', 'config')) ||
668684
path.endsWith(join('dvc', 'config.local'))
669685
) {
670-
void this.updateIsStudioConnected()
686+
void this.updateStudioAndSend()
671687
}
672688
}
673689
)
@@ -705,13 +721,23 @@ export class Setup
705721
)
706722
}
707723

708-
private async accessConfig(cwd: string, ...args: Args) {
724+
private accessConfig(cwd: string, ...args: Args) {
725+
return this.accessDvc(AvailableCommands.CONFIG, cwd, ...args)
726+
}
727+
728+
private accessRemote(cwd: string, ...args: Args) {
729+
return this.accessDvc(AvailableCommands.REMOTE, cwd, ...args)
730+
}
731+
732+
private async accessDvc(
733+
commandId:
734+
| typeof AvailableCommands.CONFIG
735+
| typeof AvailableCommands.REMOTE,
736+
cwd: string,
737+
...args: Args
738+
) {
709739
try {
710-
return await this.internalCommands.executeCommand(
711-
AvailableCommands.CONFIG,
712-
cwd,
713-
...args
714-
)
740+
return await this.internalCommands.executeCommand(commandId, cwd, ...args)
715741
} catch {}
716742
}
717743
}

extension/src/setup/webview/contract.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ export type DvcCliDetails = {
33
version: string | undefined
44
}
55

6+
export type RemoteList =
7+
| { [dvcRoot: string]: { [alias: string]: string } | undefined }
8+
| undefined
9+
610
export type SetupData = {
711
canGitInitialize: boolean
812
cliCompatible: boolean | undefined
@@ -14,20 +18,23 @@ export type SetupData = {
1418
needsGitInitialized: boolean | undefined
1519
projectInitialized: boolean
1620
pythonBinPath: string | undefined
21+
remoteList: RemoteList
1722
sectionCollapsed: typeof DEFAULT_SECTION_COLLAPSED | undefined
1823
shareLiveToStudio: boolean
1924
}
2025

2126
export enum SetupSection {
27+
DVC = 'dvc',
2228
EXPERIMENTS = 'experiments',
23-
STUDIO = 'studio',
24-
DVC = 'dvc'
29+
REMOTES = 'remotes',
30+
STUDIO = 'studio'
2531
}
2632

2733
export const DEFAULT_SECTION_COLLAPSED = {
34+
[SetupSection.DVC]: false,
2835
[SetupSection.EXPERIMENTS]: false,
29-
[SetupSection.STUDIO]: false,
30-
[SetupSection.DVC]: false
36+
[SetupSection.REMOTES]: false,
37+
[SetupSection.STUDIO]: false
3138
}
3239

3340
export type SectionCollapsed = typeof DEFAULT_SECTION_COLLAPSED

extension/src/setup/webview/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class WebviewMessages {
4040
needsGitInitialized,
4141
projectInitialized,
4242
pythonBinPath,
43+
remoteList,
4344
sectionCollapsed,
4445
shareLiveToStudio
4546
}: SetupData) {
@@ -54,6 +55,7 @@ export class WebviewMessages {
5455
needsGitInitialized,
5556
projectInitialized,
5657
pythonBinPath,
58+
remoteList,
5759
sectionCollapsed,
5860
shareLiveToStudio
5961
})

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ suite('Setup Test Suite', () => {
246246
needsGitInitialized: true,
247247
projectInitialized: false,
248248
pythonBinPath: undefined,
249+
remoteList: undefined,
249250
sectionCollapsed: undefined,
250251
shareLiveToStudio: false
251252
})
@@ -287,6 +288,7 @@ suite('Setup Test Suite', () => {
287288
needsGitInitialized: true,
288289
projectInitialized: false,
289290
pythonBinPath: undefined,
291+
remoteList: undefined,
290292
sectionCollapsed: undefined,
291293
shareLiveToStudio: false
292294
})
@@ -337,6 +339,7 @@ suite('Setup Test Suite', () => {
337339
needsGitInitialized: false,
338340
projectInitialized: false,
339341
pythonBinPath: undefined,
342+
remoteList: undefined,
340343
sectionCollapsed: undefined,
341344
shareLiveToStudio: false
342345
})
@@ -387,6 +390,7 @@ suite('Setup Test Suite', () => {
387390
needsGitInitialized: false,
388391
projectInitialized: true,
389392
pythonBinPath: undefined,
393+
remoteList: { [dvcDemoPath]: undefined },
390394
sectionCollapsed: undefined,
391395
shareLiveToStudio: false
392396
})

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const buildSetup = (
4444

4545
const mockEmitter = disposer.track(new EventEmitter())
4646
stub(dvcReader, 'root').resolves(mockDvcRoot)
47+
stub(dvcExecutor, 'remote').resolves('')
4748
const mockVersion = stub(dvcReader, 'version').resolves(MIN_CLI_VERSION)
4849
const mockGlobalVersion = stub(dvcReader, 'globalVersion').resolves(
4950
MIN_CLI_VERSION

0 commit comments

Comments
 (0)