Skip to content

Commit e07eb26

Browse files
committed
Add URL history and CODER_URL
We default to the last used URL or CODER_URL, if set.
1 parent ae17065 commit e07eb26

File tree

3 files changed

+125
-39
lines changed

3 files changed

+125
-39
lines changed

src/commands.ts

+73-29
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,85 @@ import { Remote } from "./remote"
88
import { Storage } from "./storage"
99
import { OpenableTreeItem } from "./workspacesProvider"
1010

11-
// maybeAskUrl asks the user for the URL if it was not provided and normalizes
12-
// the returned URL.
13-
export async function maybeAskUrl(
14-
providedUrl: string | undefined | null,
15-
lastUsedUrl?: string,
16-
): Promise<string | undefined> {
17-
let url =
18-
providedUrl ||
19-
(await vscode.window.showInputBox({
20-
title: "Coder URL",
21-
prompt: "Enter the URL of your Coder deployment.",
22-
placeHolder: "https://example.coder.com",
23-
value: lastUsedUrl,
24-
}))
25-
if (!url) {
26-
return undefined
27-
}
28-
if (!url.startsWith("http://") && !url.startsWith("https://")) {
29-
// Default to HTTPS if not provided!
30-
// https://github.com/coder/vscode-coder/issues/44
31-
url = "https://" + url
32-
}
33-
while (url.endsWith("/")) {
34-
url = url.substring(0, url.length - 1)
35-
}
36-
return url
37-
}
38-
3911
export class Commands {
4012
public constructor(
4113
private readonly vscodeProposed: typeof vscode,
4214
private readonly storage: Storage,
4315
) {}
4416

17+
/**
18+
* Ask the user for the URL, letting them choose from a list of recent URLs or
19+
* CODER_URL or enter a new one. Undefined means the user aborted.
20+
*/
21+
private askURL(selection?: string): Promise<string | undefined> {
22+
const quickPick = vscode.window.createQuickPick()
23+
quickPick.value = selection || process.env.CODER_URL || ""
24+
quickPick.placeholder = "https://example.coder.com",
25+
quickPick.title = "Enter the URL of your Coder deployment.",
26+
27+
// Initial items.
28+
quickPick.items = this.storage.withUrlHistory(process.env.CODER_URL)
29+
.map((url) => ({
30+
alwaysShow: true,
31+
label: url
32+
}))
33+
34+
// Quick picks do not allow arbitrary values, so we add the value itself as
35+
// an option in case the user wants to connect to something that is not in
36+
// the list.
37+
quickPick.onDidChangeValue((value) => {
38+
quickPick.items = this.storage.withUrlHistory(process.env.CODER_URL, value)
39+
.map((url) => ({
40+
alwaysShow: true,
41+
label: url,
42+
}))
43+
})
44+
45+
quickPick.show()
46+
47+
return new Promise<string | undefined>((resolve) => {
48+
quickPick.onDidHide(() => resolve(undefined))
49+
quickPick.onDidChangeSelection((selected) => resolve(selected[0]?.label))
50+
})
51+
}
52+
53+
/**
54+
* Ask the user for the URL if it was not provided, letting them choose from a
55+
* list of recent URLs or CODER_URL or enter a new one, and normalizes the
56+
* returned URL. Undefined means the user aborted.
57+
*/
58+
public async maybeAskUrl(
59+
providedUrl: string | undefined | null,
60+
lastUsedUrl?: string,
61+
): Promise<string | undefined> {
62+
let url = providedUrl || await askURL(lastUsedUrl)
63+
if (!url) { // User aborted.
64+
return undefined
65+
}
66+
67+
// Normalize URL.
68+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
69+
// Default to HTTPS if not provided!
70+
// https://github.com/coder/vscode-coder/issues/44
71+
url = "https://" + url
72+
}
73+
while (url.endsWith("/")) {
74+
url = url.substring(0, url.length - 1)
75+
}
76+
return url
77+
}
78+
79+
/**
80+
* Log into the provided deployment. If the deployment URL is not specified,
81+
* ask for it first with a menu showing recent URLs and CODER_URL, if set.
82+
*/
4583
public async login(...args: string[]): Promise<void> {
46-
const url = await maybeAskUrl(args[0])
84+
const url = await this.maybeAskUrl(args[0])
85+
if (!url) {
86+
vscode.window.showWarningMessage("Aborting login because no URL was provided.")
87+
return
88+
}
89+
4790
let token: string | undefined = args.length >= 2 ? args[1] : undefined
4891
if (!token) {
4992
const opened = await vscode.env.openExternal(vscode.Uri.parse(`${url}/cli-auth`))
@@ -91,6 +134,7 @@ export class Commands {
91134
})
92135
}
93136
if (!token) {
137+
vscode.window.showWarningMessage("Aborting login because no token was provided.")
94138
return
95139
}
96140

src/extension.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
139139
// queries will default to localhost) so ask for it if missing.
140140
// Pre-populate in case we do have the right URL so the user can just
141141
// hit enter and move on.
142-
const url = await maybeAskUrl(params.get("url"), storage.getURL())
142+
const url = await storage.maybeAskUrl(params.get("url"), storage.getURL())
143143
if (url) {
144144
await storage.setURL(url)
145145
} else {

src/storage.ts

+51-9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import prettyBytes from "pretty-bytes"
1313
import * as vscode from "vscode"
1414
import { getHeaderCommand, getHeaders } from "./headers"
1515

16+
// Maximium number of recent URLs to store.
17+
const MAX_URLS = 10
18+
1619
export class Storage {
1720
public workspace?: Workspace
1821
public workspaceLogPath?: string
@@ -25,23 +28,59 @@ export class Storage {
2528
private readonly logUri: vscode.Uri,
2629
) {}
2730

28-
// init ensures that the storage places values in the
29-
// appropriate default values.
31+
/**
32+
* Set the URL and session token on the Axios client and on disk for the cli
33+
* if they are set.
34+
*/
3035
public async init(): Promise<void> {
31-
await this.updateURL()
36+
await this.updateURL(this.getURL())
3237
await this.updateSessionToken()
3338
}
3439

35-
public setURL(url?: string): Thenable<void> {
36-
return this.memento.update("url", url).then(() => {
37-
return this.updateURL()
38-
})
40+
/**
41+
* Add the URL to the list of recently accessed URLs in global storage, then
42+
* set it as the current URL and update it on the Axios client and on disk for
43+
* the cli.
44+
*
45+
* If the URL is falsey, then remove it as the currently accessed URL and do
46+
* not touch the history.
47+
*/
48+
public async setURL(url?: string): Promise<void> {
49+
await this.memento.update("url", url)
50+
this.updateURL(url)
51+
if (url) {
52+
const history = this.withUrlHistory(url)
53+
await this.memento.update("urlHistory", history)
54+
}
3955
}
4056

57+
/**
58+
* Get the currently configured URL.
59+
*/
4160
public getURL(): string | undefined {
4261
return this.memento.get("url")
4362
}
4463

64+
/**
65+
* Get the most recently accessed URLs (oldest to newest) with the provided
66+
* values appended. Duplicates will be removed.
67+
*/
68+
public withUrlHistory(...append: (string | undefined)[]): string[] {
69+
const val = this.memento.get("urlHistory")
70+
const urls = Array.isArray(val) ? new Set(val) : new Set()
71+
for (const url of append) {
72+
if (url) {
73+
// It might exist; delete first so it gets appended.
74+
urls.delete(url)
75+
urls.add(url)
76+
}
77+
}
78+
// Slice off the head if the list is too large.
79+
return urls.size > MAX_URLS
80+
? Array.from(urls).slice(urls.size - MAX_URLS, urls.size)
81+
: Array.from(urls)
82+
}
83+
4584
public setSessionToken(sessionToken?: string): Thenable<void> {
4685
if (!sessionToken) {
4786
return this.secrets.delete("sessionToken").then(() => {
@@ -323,8 +362,11 @@ export class Storage {
323362
// attention to it.
324363
}
325364

326-
private async updateURL(): Promise<void> {
327-
const url = this.getURL()
365+
/**
366+
* Set the URL on the global Axios client and write the URL to disk which will
367+
* be used by the CLI via --url-file.
368+
*/
369+
private async updateURL(url: string | undefined): Promise<void> {
328370
axios.defaults.baseURL = url
329371
if (url) {
330372
await ensureDir(this.globalStorageUri.fsPath)

0 commit comments

Comments
 (0)