Skip to content

Commit

Permalink
Add URL history and CODER_URL (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
code-asher authored Jan 31, 2024
1 parent 507e559 commit 5054d75
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 39 deletions.
97 changes: 68 additions & 29 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,81 @@ import { Remote } from "./remote"
import { Storage } from "./storage"
import { OpenableTreeItem } from "./workspacesProvider"

// maybeAskUrl asks the user for the URL if it was not provided and normalizes
// the returned URL.
export async function maybeAskUrl(
providedUrl: string | undefined | null,
lastUsedUrl?: string,
): Promise<string | undefined> {
let url =
providedUrl ||
(await vscode.window.showInputBox({
title: "Coder URL",
prompt: "Enter the URL of your Coder deployment.",
placeHolder: "https://example.coder.com",
value: lastUsedUrl,
}))
if (!url) {
return undefined
}
if (!url.startsWith("http://") && !url.startsWith("https://")) {
// Default to HTTPS if not provided!
// https://github.com/coder/vscode-coder/issues/44
url = "https://" + url
}
while (url.endsWith("/")) {
url = url.substring(0, url.length - 1)
}
return url
}

export class Commands {
public constructor(
private readonly vscodeProposed: typeof vscode,
private readonly storage: Storage,
) {}

/**
* Ask the user for the URL, letting them choose from a list of recent URLs or
* CODER_URL or enter a new one. Undefined means the user aborted.
*/
private async askURL(selection?: string): Promise<string | undefined> {
const quickPick = vscode.window.createQuickPick()
quickPick.value = selection || process.env.CODER_URL || ""
quickPick.placeholder = "https://example.coder.com"
quickPick.title = "Enter the URL of your Coder deployment."

// Initial items.
quickPick.items = this.storage.withUrlHistory(process.env.CODER_URL).map((url) => ({
alwaysShow: true,
label: url,
}))

// Quick picks do not allow arbitrary values, so we add the value itself as
// an option in case the user wants to connect to something that is not in
// the list.
quickPick.onDidChangeValue((value) => {
quickPick.items = this.storage.withUrlHistory(process.env.CODER_URL, value).map((url) => ({
alwaysShow: true,
label: url,
}))
})

quickPick.show()

const selected = await new Promise<string | undefined>((resolve) => {
quickPick.onDidHide(() => resolve(undefined))
quickPick.onDidChangeSelection((selected) => resolve(selected[0]?.label))
})
quickPick.dispose()
return selected
}

/**
* Ask the user for the URL if it was not provided, letting them choose from a
* list of recent URLs or CODER_URL or enter a new one, and normalizes the
* returned URL. Undefined means the user aborted.
*/
public async maybeAskUrl(providedUrl: string | undefined | null, lastUsedUrl?: string): Promise<string | undefined> {
let url = providedUrl || (await this.askURL(lastUsedUrl))
if (!url) {
// User aborted.
return undefined
}

// Normalize URL.
if (!url.startsWith("http://") && !url.startsWith("https://")) {
// Default to HTTPS if not provided so URLs can be typed more easily.
url = "https://" + url
}
while (url.endsWith("/")) {
url = url.substring(0, url.length - 1)
}
return url
}

/**
* Log into the provided deployment. If the deployment URL is not specified,
* ask for it first with a menu showing recent URLs and CODER_URL, if set.
*/
public async login(...args: string[]): Promise<void> {
const url = await maybeAskUrl(args[0])
const url = await this.maybeAskUrl(args[0])
if (!url) {
return
}

let token: string | undefined = args.length >= 2 ? args[1] : undefined
if (!token) {
const opened = await vscode.env.openExternal(vscode.Uri.parse(`${url}/cli-auth`))
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
// queries will default to localhost) so ask for it if missing.
// Pre-populate in case we do have the right URL so the user can just
// hit enter and move on.
const url = await maybeAskUrl(params.get("url"), storage.getURL())
const url = await commands.maybeAskUrl(params.get("url"), storage.getURL())
if (url) {
await storage.setURL(url)
} else {
Expand Down
58 changes: 49 additions & 9 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import prettyBytes from "pretty-bytes"
import * as vscode from "vscode"
import { getHeaderCommand, getHeaders } from "./headers"

// Maximium number of recent URLs to store.
const MAX_URLS = 10

export class Storage {
public workspace?: Workspace
public workspaceLogPath?: string
Expand All @@ -25,23 +28,57 @@ export class Storage {
private readonly logUri: vscode.Uri,
) {}

// init ensures that the storage places values in the
// appropriate default values.
/**
* Set the URL and session token on the Axios client and on disk for the cli
* if they are set.
*/
public async init(): Promise<void> {
await this.updateURL()
await this.updateURL(this.getURL())
await this.updateSessionToken()
}

public setURL(url?: string): Thenable<void> {
return this.memento.update("url", url).then(() => {
return this.updateURL()
})
/**
* Add the URL to the list of recently accessed URLs in global storage, then
* set it as the current URL and update it on the Axios client and on disk for
* the cli.
*
* If the URL is falsey, then remove it as the currently accessed URL and do
* not touch the history.
*/
public async setURL(url?: string): Promise<void> {
await this.memento.update("url", url)
this.updateURL(url)
if (url) {
const history = this.withUrlHistory(url)
await this.memento.update("urlHistory", history)
}
}

/**
* Get the currently configured URL.
*/
public getURL(): string | undefined {
return this.memento.get("url")
}

/**
* Get the most recently accessed URLs (oldest to newest) with the provided
* values appended. Duplicates will be removed.
*/
public withUrlHistory(...append: (string | undefined)[]): string[] {
const val = this.memento.get("urlHistory")
const urls = Array.isArray(val) ? new Set(val) : new Set()
for (const url of append) {
if (url) {
// It might exist; delete first so it gets appended.
urls.delete(url)
urls.add(url)
}
}
// Slice off the head if the list is too large.
return urls.size > MAX_URLS ? Array.from(urls).slice(urls.size - MAX_URLS, urls.size) : Array.from(urls)
}

public setSessionToken(sessionToken?: string): Thenable<void> {
if (!sessionToken) {
return this.secrets.delete("sessionToken").then(() => {
Expand Down Expand Up @@ -323,8 +360,11 @@ export class Storage {
// attention to it.
}

private async updateURL(): Promise<void> {
const url = this.getURL()
/**
* Set the URL on the global Axios client and write the URL to disk which will
* be used by the CLI via --url-file.
*/
private async updateURL(url: string | undefined): Promise<void> {
axios.defaults.baseURL = url
if (url) {
await ensureDir(this.globalStorageUri.fsPath)
Expand Down

0 comments on commit 5054d75

Please sign in to comment.