Skip to content

Commit 093833d

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 093833d

File tree

3 files changed

+119
-39
lines changed

3 files changed

+119
-39
lines changed

src/commands.ts

+69-29
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,81 @@ 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).map((url) => ({
29+
alwaysShow: true,
30+
label: url,
31+
}))
32+
33+
// Quick picks do not allow arbitrary values, so we add the value itself as
34+
// an option in case the user wants to connect to something that is not in
35+
// the list.
36+
quickPick.onDidChangeValue((value) => {
37+
quickPick.items = this.storage.withUrlHistory(process.env.CODER_URL, value).map((url) => ({
38+
alwaysShow: true,
39+
label: url,
40+
}))
41+
})
42+
43+
quickPick.show()
44+
45+
return new Promise<string | undefined>((resolve) => {
46+
quickPick.onDidHide(() => resolve(undefined))
47+
quickPick.onDidChangeSelection((selected) => resolve(selected[0]?.label))
48+
})
49+
}
50+
51+
/**
52+
* Ask the user for the URL if it was not provided, letting them choose from a
53+
* list of recent URLs or CODER_URL or enter a new one, and normalizes the
54+
* returned URL. Undefined means the user aborted.
55+
*/
56+
public async maybeAskUrl(providedUrl: string | undefined | null, lastUsedUrl?: string): Promise<string | undefined> {
57+
let url = providedUrl || (await askURL(lastUsedUrl))
58+
if (!url) {
59+
// User aborted.
60+
return undefined
61+
}
62+
63+
// Normalize URL.
64+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
65+
// Default to HTTPS if not provided!
66+
// https://github.com/coder/vscode-coder/issues/44
67+
url = "https://" + url
68+
}
69+
while (url.endsWith("/")) {
70+
url = url.substring(0, url.length - 1)
71+
}
72+
return url
73+
}
74+
75+
/**
76+
* Log into the provided deployment. If the deployment URL is not specified,
77+
* ask for it first with a menu showing recent URLs and CODER_URL, if set.
78+
*/
4579
public async login(...args: string[]): Promise<void> {
46-
const url = await maybeAskUrl(args[0])
80+
const url = await this.maybeAskUrl(args[0])
81+
if (!url) {
82+
vscode.window.showWarningMessage("Aborting login because no URL was provided.")
83+
return
84+
}
85+
4786
let token: string | undefined = args.length >= 2 ? args[1] : undefined
4887
if (!token) {
4988
const opened = await vscode.env.openExternal(vscode.Uri.parse(`${url}/cli-auth`))
@@ -91,6 +130,7 @@ export class Commands {
91130
})
92131
}
93132
if (!token) {
133+
vscode.window.showWarningMessage("Aborting login because no token was provided.")
94134
return
95135
}
96136

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

+49-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,57 @@ 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 ? Array.from(urls).slice(urls.size - MAX_URLS, urls.size) : Array.from(urls)
80+
}
81+
4582
public setSessionToken(sessionToken?: string): Thenable<void> {
4683
if (!sessionToken) {
4784
return this.secrets.delete("sessionToken").then(() => {
@@ -323,8 +360,11 @@ export class Storage {
323360
// attention to it.
324361
}
325362

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

0 commit comments

Comments
 (0)