Skip to content

Commit 4dd2349

Browse files
authored
Add copy shareable Studio link to experiments table (#4557)
* implement shareable link for completed and pushed experiments * remove updates triggered by studio.offline * update aria-labels * change link icon to progress ring briefly when link is copied * send null in place of refs (temp)
1 parent 1893310 commit 4dd2349

File tree

26 files changed

+365
-24
lines changed

26 files changed

+365
-24
lines changed

extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,6 +1713,7 @@
17131713
"lodash.isequal": "4.5.0",
17141714
"lodash.merge": "4.6.2",
17151715
"lodash.omit": "4.5.0",
1716+
"node-fetch": "2.6.13",
17161717
"process-exists": "4.1.0",
17171718
"tree-kill": "1.2.2",
17181719
"uuid": "9.0.0",

extension/src/cli/git/reader.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ export class GitReader extends GitCli {
7171
}
7272

7373
public async getRemoteUrl(cwd: string): Promise<string> {
74-
const options = getOptions({ args: [Command.LS_REMOTE, Flag.GET_URL], cwd })
74+
const options = getOptions({
75+
args: [Command.LS_REMOTE, Flag.GET_URL, DEFAULT_REMOTE],
76+
cwd
77+
})
7578
try {
7679
return await this.executeProcess(options)
7780
} catch {

extension/src/experiments/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { pickSortsToRemove, pickSortToAdd } from './model/sortBy/quickPick'
2828
import { ColumnsModel } from './columns/model'
2929
import { ExperimentsData } from './data'
3030
import { stopWorkspaceExperiment } from './processExecution'
31+
import { Studio } from './studio'
3132
import { Experiment, ColumnType, TableData, Column } from './webview/contract'
3233
import { WebviewMessages } from './webview/messages'
3334
import { DecorationProvider } from './model/decorationProvider'
@@ -72,6 +73,7 @@ export class Experiments extends BaseRepository<TableData> {
7273
private readonly data: ExperimentsData
7374
private readonly experiments: ExperimentsModel
7475
private readonly columns: ColumnsModel
76+
private readonly studio: Studio
7577

7678
private readonly experimentsFileFocused = this.dispose.track(
7779
new EventEmitter<string | undefined>()
@@ -145,6 +147,8 @@ export class Experiments extends BaseRepository<TableData> {
145147
)
146148
)
147149

150+
this.studio = this.dispose.track(new Studio(this.dvcRoot, internalCommands))
151+
148152
this.data = this.dispose.track(
149153
data ||
150154
new ExperimentsData(
@@ -589,6 +593,12 @@ export class Experiments extends BaseRepository<TableData> {
589593
return this.data.update()
590594
}
591595

596+
public async setStudioBaseUrl(studioToken: string | undefined) {
597+
await this.isReady()
598+
await this.studio.setBaseUrl(studioToken)
599+
return this.webviewMessages.sendWebviewMessage()
600+
}
601+
592602
protected sendInitialWebviewData() {
593603
return this.webviewMessages.sendWebviewMessage()
594604
}
@@ -630,6 +640,7 @@ export class Experiments extends BaseRepository<TableData> {
630640
this.experiments,
631641
this.columns,
632642
this.pipeline,
643+
this.studio,
633644
() => this.getWebview(),
634645
() => this.notifyChanged(),
635646
() => this.selectColumns(),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import fetch from 'node-fetch'
2+
import { STUDIO_URL } from '../setup/webview/contract'
3+
import { Disposable } from '../class/dispose'
4+
import { AvailableCommands, InternalCommands } from '../commands/internal'
5+
6+
export class Studio extends Disposable {
7+
private readonly dvcRoot: string
8+
private readonly internalCommands: InternalCommands
9+
10+
private baseUrl?: string
11+
12+
constructor(dvcRoot: string, internalCommands: InternalCommands) {
13+
super()
14+
this.dvcRoot = dvcRoot
15+
this.internalCommands = internalCommands
16+
}
17+
18+
public isConnected() {
19+
return !!this.baseUrl
20+
}
21+
22+
public async setBaseUrl(studioToken: string | undefined) {
23+
if (!studioToken) {
24+
this.baseUrl = undefined
25+
return
26+
}
27+
28+
const gitRemote = await this.internalCommands.executeCommand(
29+
AvailableCommands.GIT_GET_REMOTE_URL,
30+
this.dvcRoot
31+
)
32+
33+
this.baseUrl = await this.getBaseUrl(studioToken, gitRemote)
34+
}
35+
36+
public getLink(sha: string) {
37+
if (!this.baseUrl) {
38+
return ''
39+
}
40+
return `${this.baseUrl}?showOnlySelected=1&experimentReferences=${sha}&activeExperimentReferences=${sha}%3Aprimary`
41+
}
42+
43+
private async getBaseUrl(studioToken: string, gitRemoteUrl: string) {
44+
try {
45+
const response = await fetch(`${STUDIO_URL}/webhook/dvc`, {
46+
body: JSON.stringify({
47+
client: 'vscode',
48+
refs: [null],
49+
repo_url: gitRemoteUrl
50+
}),
51+
headers: {
52+
Authorization: `token ${studioToken}`,
53+
'Content-Type': 'application/json'
54+
},
55+
method: 'POST'
56+
})
57+
58+
const { url } = (await response.json()) as { url: string }
59+
return url
60+
} catch {}
61+
}
62+
}

extension/src/experiments/webview/contract.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export type TableData = {
106106
hasConfig: boolean
107107
hasMoreCommits: Record<string, boolean>
108108
hasRunningWorkspaceExperiment: boolean
109+
isStudioConnected: boolean
109110
isShowingMoreCommits: Record<string, boolean>
110111
rows: Commit[]
111112
showOnlyChanged: boolean

extension/src/experiments/webview/messages.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ import { collectColumnsWithChangedValues } from '../columns/collect'
2626
import { ColumnLike } from '../columns/like'
2727
import { getFilterId } from '../model/filterBy'
2828
import { writeToClipboard } from '../../vscode/clipboard'
29+
import { Studio } from '../studio'
2930

3031
export class WebviewMessages {
3132
private readonly dvcRoot: string
3233

3334
private readonly experiments: ExperimentsModel
3435
private readonly columns: ColumnsModel
3536
private readonly pipeline: Pipeline
37+
private readonly studio: Studio
3638

3739
private readonly getWebview: () => BaseWebview<TableData> | undefined
3840
private readonly notifyChanged: () => void
@@ -50,6 +52,7 @@ export class WebviewMessages {
5052
experiments: ExperimentsModel,
5153
columns: ColumnsModel,
5254
pipeline: Pipeline,
55+
studio: Studio,
5356
getWebview: () => BaseWebview<TableData> | undefined,
5457
notifyChanged: () => void,
5558
selectColumns: () => Promise<void>,
@@ -64,6 +67,7 @@ export class WebviewMessages {
6467
this.experiments = experiments
6568
this.columns = columns
6669
this.pipeline = pipeline
70+
this.studio = studio
6771
this.getWebview = getWebview
6872
this.notifyChanged = notifyChanged
6973
this.selectColumns = selectColumns
@@ -231,6 +235,9 @@ export class WebviewMessages {
231235
case MessageFromWebviewType.COPY_TO_CLIPBOARD:
232236
return this.copyToClipboard(message.payload)
233237

238+
case MessageFromWebviewType.COPY_STUDIO_LINK:
239+
return this.copyStudioLink(message.payload)
240+
234241
default:
235242
Logger.error(`Unexpected message: ${JSON.stringify(message)}`)
236243
}
@@ -314,6 +321,7 @@ export class WebviewMessages {
314321
hasMoreCommits,
315322
hasRunningWorkspaceExperiment,
316323
isShowingMoreCommits,
324+
isStudioConnected,
317325
rows,
318326
selectedBranches,
319327
selectedForPlotsCount,
@@ -332,6 +340,7 @@ export class WebviewMessages {
332340
this.experiments.getHasMoreCommits(),
333341
this.experiments.hasRunningWorkspaceExperiment(),
334342
this.experiments.getIsShowingMoreCommits(),
343+
this.studio.isConnected(),
335344
this.experiments.getRowData(),
336345
this.experiments.getSelectedBranches(),
337346
this.experiments.getSelectedRevisions().length,
@@ -356,6 +365,7 @@ export class WebviewMessages {
356365
hasMoreCommits,
357366
hasRunningWorkspaceExperiment,
358367
isShowingMoreCommits,
368+
isStudioConnected,
359369
rows,
360370
selectedBranches,
361371
selectedForPlotsCount,
@@ -584,4 +594,23 @@ export class WebviewMessages {
584594
undefined
585595
)
586596
}
597+
598+
private async copyStudioLink(id: string) {
599+
const sha = this.experiments.getExperiments().find(exp => exp.id === id)
600+
?.sha
601+
602+
if (!sha) {
603+
return
604+
}
605+
606+
const link = this.studio.getLink(sha)
607+
608+
await writeToClipboard(link, `[Studio link](${link})`)
609+
610+
void sendTelemetryEvent(
611+
EventName.VIEWS_EXPERIMENTS_TABLE_COPY_STUDIO_LINK,
612+
undefined,
613+
undefined
614+
)
615+
}
587616
}

extension/src/experiments/workspace.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export class WorkspaceExperiments extends BaseWorkspaceWebviews<
282282
dvcRoot: string,
283283
subProjects: string[],
284284
pipeline: WorkspacePipeline,
285+
setup: Setup,
285286
resourceLocator: ResourceLocator
286287
) {
287288
const experiments = this.dispose.track(
@@ -298,6 +299,14 @@ export class WorkspaceExperiments extends BaseWorkspaceWebviews<
298299

299300
this.setRepository(dvcRoot, experiments)
300301

302+
void experiments.setStudioBaseUrl(setup.getStudioAccessToken())
303+
304+
experiments.dispose.track(
305+
setup.onDidChangeStudioConnection(() => {
306+
void experiments.setStudioBaseUrl(setup.getStudioAccessToken())
307+
})
308+
)
309+
301310
experiments.dispose.track(
302311
experiments.onDidChangeIsWebviewFocused(
303312
dvcRoot => (this.focusedWebviewDvcRoot = dvcRoot)

extension/src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ class Extension extends Disposable {
289289
dvcRoots,
290290
subProjects,
291291
this.pipelines,
292+
this.setup,
292293
this.resourceLocator
293294
)
294295
this.plots.create(

extension/src/setup/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export class Setup
7979
public readonly initialize: () => Promise<void[]>
8080
public readonly resetMembers: () => void
8181

82+
public readonly onDidChangeStudioConnection: Event<void>
83+
8284
private dvcRoots: string[] = []
8385
private subProjects: { [dvcRoot: string]: string[] } = {}
8486

@@ -101,6 +103,9 @@ export class Setup
101103
new EventEmitter()
102104
)
103105

106+
private readonly studioConnectionChanged: EventEmitter<void> =
107+
this.dispose.track(new EventEmitter())
108+
104109
private readonly onDidChangeWorkspace: Event<void> =
105110
this.workspaceChanged.event
106111

@@ -150,6 +155,7 @@ export class Setup
150155
}
151156

152157
this.collectWorkspaceScale = collectWorkspaceScale
158+
this.onDidChangeStudioConnection = this.studioConnectionChanged.event
153159

154160
this.setCommandsAvailability(false)
155161
this.setProjectAvailability()
@@ -759,16 +765,29 @@ export class Setup
759765
private async setStudioValues() {
760766
const cwd = this.getCwd()
761767

768+
const previousStudioAccessToken = this.studioAccessToken
769+
762770
if (!cwd) {
763771
this.studioAccessToken = undefined
764772
this.shareLiveToStudio = undefined
773+
774+
if (previousStudioAccessToken) {
775+
this.studioConnectionChanged.fire()
776+
}
765777
return
766778
}
767779

768-
;[this.studioAccessToken, this.shareLiveToStudio] = await Promise.all([
780+
const [studioAccessToken, shareLiveToStudio] = await Promise.all([
769781
this.accessConfig(cwd, ConfigKey.STUDIO_TOKEN),
770782
(await this.accessConfig(cwd, ConfigKey.STUDIO_OFFLINE)) !== 'true'
771783
])
784+
785+
this.studioAccessToken = studioAccessToken
786+
this.shareLiveToStudio = shareLiveToStudio
787+
788+
if (previousStudioAccessToken !== this.studioAccessToken) {
789+
this.studioConnectionChanged.fire()
790+
}
772791
}
773792

774793
private async updateStudioOffline(shareLive: boolean) {

extension/src/telemetry/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export const EventName = Object.assign(
3232
VIEWS_EXPERIMENTS_TABLE_CLOSED: 'views.experimentsTable.closed',
3333
VIEWS_EXPERIMENTS_TABLE_COLUMNS_REORDERED:
3434
'views.experimentsTable.columnsReordered',
35+
VIEWS_EXPERIMENTS_TABLE_COPY_STUDIO_LINK:
36+
'views.experimentsTable.copyStudioLink',
3537
VIEWS_EXPERIMENTS_TABLE_COPY_TO_CLIPBOARD:
3638
'views.experimentsTable.copyToClipboard',
3739
VIEWS_EXPERIMENTS_TABLE_CREATED: 'views.experimentsTable.created',
@@ -259,6 +261,7 @@ export interface IEventNamePropertyMapping {
259261
path: string
260262
}
261263
[EventName.VIEWS_EXPERIMENTS_TABLE_RESET_COMMITS]: undefined
264+
[EventName.VIEWS_EXPERIMENTS_TABLE_COPY_STUDIO_LINK]: undefined
262265
[EventName.VIEWS_EXPERIMENTS_TABLE_COPY_TO_CLIPBOARD]: undefined
263266
[EventName.VIEWS_EXPERIMENTS_TABLE_CREATED]: undefined
264267
[EventName.VIEWS_EXPERIMENTS_TABLE_FILTER_COLUMN]: undefined

0 commit comments

Comments
 (0)