-
Notifications
You must be signed in to change notification settings - Fork 29
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
Enable saving of Studio access token #3235
Changes from 9 commits
ed5dd88
efdccee
555d6c6
3443e47
158c556
a09bbb3
17fdc2e
1112443
b1eebd9
39beedc
3ca4c8b
8d56106
b603b78
926ca41
41e4087
6709213
c277d93
605763b
a78e1ef
bc9cf3a
72dd056
02a0fc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ | |
"featurize", | ||
"hardlinks", | ||
"Interactors", | ||
"isat", | ||
"isdir", | ||
"isempty", | ||
"isequal", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,6 +88,11 @@ | |
} | ||
], | ||
"commands": [ | ||
{ | ||
"title": "Connect to Studio", | ||
"command": "dvc.studioConnect", | ||
"category": "DVC" | ||
}, | ||
{ | ||
"title": "%command.addExperimentsTableFilter%", | ||
"command": "dvc.addExperimentsTableFilter", | ||
|
@@ -590,6 +595,15 @@ | |
"description": "%config.pythonPath.description%", | ||
"type": "string", | ||
"default": null | ||
}, | ||
"dvc.studioAccessToken": { | ||
"title": "%config.studioAccessToken.title%", | ||
"description": "%config.studioAccessToken.description%", | ||
"type": "string", | ||
"pattern": "^isat_", | ||
"minLength": 54, | ||
"maxLength": 54, | ||
"default": null | ||
} | ||
} | ||
}, | ||
|
@@ -786,6 +800,10 @@ | |
"command": "dvc.stopQueuedExperiments", | ||
"when": "dvc.commands.available && dvc.project.available" | ||
}, | ||
{ | ||
"command": "dvc.studioConnect", | ||
"when": "dvc.commands.available && dvc.project.available && !dvc.studio.connected" | ||
}, | ||
{ | ||
"command": "dvc.selectForCompare", | ||
"when": "false" | ||
|
@@ -1319,6 +1337,11 @@ | |
"name": "Views", | ||
"when": "true" | ||
}, | ||
{ | ||
"id": "dvc.views.studio", | ||
"name": "Studio", | ||
"when": "!dvc.studio.connected" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [F] We only show the tree view when there is no Studio token available in the secrets store. It can still be hidden by the user. We also give them the option to remove the token and save another one. |
||
}, | ||
{ | ||
"id": "dvc.views.experimentsColumnsTree", | ||
"name": "Columns", | ||
|
@@ -1356,6 +1379,11 @@ | |
"contents": "[Show Experiments](command:dvc.showExperiments)\n[Show Plots](command:dvc.showPlots)\n[Show Experiments and Plots](command:dvc.showExperimentsAndPlots)", | ||
"when": "dvc.commands.available && dvc.project.available && dvc.project.hasData" | ||
}, | ||
{ | ||
"view": "dvc.views.studio", | ||
"contents": "[$(plug) Connect](command:dvc.studioConnect)", | ||
"when": "!dvc.studio.connected" | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [I] Double check on initial view placement. |
||
{ | ||
"view": "dvc.views.webviews", | ||
"contents": "[Show Experiments](command:dvc.showSetup)\n[Show Plots](command:dvc.showSetup)\n[Show Experiments and Plots](command:dvc.showSetup)", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { commands, workspace } from 'vscode' | ||
import { isStudioAccessToken, validateTokenInput } from './input' | ||
import { STUDIO_URL } from './webview/contract' | ||
import { Resource } from '../resourceLocator' | ||
import { ViewKey } from '../webview/constants' | ||
import { MessageFromWebview, MessageFromWebviewType } from '../webview/contract' | ||
import { BaseRepository } from '../webview/repository' | ||
import { Logger } from '../common/logger' | ||
import { getInput, getValidInput } from '../vscode/inputBox' | ||
import { Title } from '../vscode/title' | ||
import { ConfigKey, getConfigValue, setUserConfigValue } from '../vscode/config' | ||
import { openUrl } from '../vscode/external' | ||
import { ContextKey, setContextValue } from '../vscode/context' | ||
|
||
export class Connect extends BaseRepository<undefined> { | ||
public readonly viewKey = ViewKey.CONNECT | ||
|
||
constructor(webviewIcon: Resource) { | ||
super('', webviewIcon) | ||
|
||
this.dispose.track( | ||
this.onDidReceivedWebviewMessage(message => | ||
this.handleMessageFromWebview(message) | ||
) | ||
) | ||
|
||
void this.setContext() | ||
|
||
this.dispose.track( | ||
workspace.onDidChangeConfiguration(e => { | ||
if (!e.affectsConfiguration(ConfigKey.STUDIO_ACCESS_TOKEN)) { | ||
return | ||
} | ||
return this.setContext() | ||
}) | ||
) | ||
} | ||
|
||
public sendInitialWebviewData(): void {} | ||
|
||
private handleMessageFromWebview(message: MessageFromWebview) { | ||
switch (message.type) { | ||
case MessageFromWebviewType.OPEN_STUDIO: | ||
return openUrl(STUDIO_URL) | ||
case MessageFromWebviewType.OPEN_STUDIO_PROFILE: | ||
return this.openStudioProfile() | ||
case MessageFromWebviewType.SAVE_STUDIO_TOKEN: | ||
return this.saveStudioToken() | ||
default: | ||
Logger.error(`Unexpected message: ${JSON.stringify(message)}`) | ||
} | ||
} | ||
|
||
private async openStudioProfile() { | ||
const username = await getInput(Title.ENTER_STUDIO_USERNAME) | ||
if (!username) { | ||
return | ||
} | ||
return openUrl(`${STUDIO_URL}/user/${username}/profile`) | ||
} | ||
|
||
private async saveStudioToken() { | ||
const token = await getValidInput( | ||
mattseddon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Title.ENTER_STUDIO_TOKEN, | ||
validateTokenInput | ||
) | ||
if (!token) { | ||
return | ||
} | ||
|
||
await setUserConfigValue(ConfigKey.STUDIO_ACCESS_TOKEN, token) | ||
return commands.executeCommand( | ||
'workbench.action.openSettings', | ||
'dvc.studioAccessToken' | ||
) | ||
} | ||
|
||
private async setContext() { | ||
if ( | ||
isStudioAccessToken(await getConfigValue(ConfigKey.STUDIO_ACCESS_TOKEN)) | ||
) { | ||
this.webview?.dispose() | ||
return setContextValue(ContextKey.STUDIO_CONNECTED, true) | ||
} | ||
|
||
return setContextValue(ContextKey.STUDIO_CONNECTED, false) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { validateTokenInput } from './input' | ||
|
||
describe('validateTokenInput', () => { | ||
const mockedStudioAccessToken = | ||
'isat_1Z4T0zVHvq9Cu03XEe9Zjvx2vkBihfGPdY7FfmEMAagOXfQxU' | ||
it('should return the warning if the input is not valid', () => { | ||
expect( | ||
validateTokenInput(mockedStudioAccessToken.slice(0, -1)) | ||
).not.toBeNull() | ||
expect( | ||
validateTokenInput( | ||
mockedStudioAccessToken.slice(1, mockedStudioAccessToken.length) | ||
) | ||
).not.toBeNull() | ||
}) | ||
|
||
it('should return null if the input is valid', () => { | ||
expect(validateTokenInput(mockedStudioAccessToken)).toBeNull() | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const isStudioAccessToken = (text?: string): boolean => { | ||
if (!text) { | ||
return false | ||
} | ||
return text.startsWith('isat_') && text.length === 54 | ||
} | ||
|
||
export const validateTokenInput = (input: string | undefined) => { | ||
if (!isStudioAccessToken(input)) { | ||
return 'please enter a valid Studio access token' | ||
} | ||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const STUDIO_URL = 'https://studio.iterative.ai' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { env, Uri } from 'vscode' | ||
|
||
export const openUrl = (url: string) => env.openExternal(Uri.parse(url)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[I] Need to change to use the
SecretStorage
API and not save the token in the open...