Skip to content

Add URL history and CODER_URL #200

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

Merged
merged 2 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 66 additions & 29 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,79 @@ 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 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()

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

/**
* 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