Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
42 changes: 16 additions & 26 deletions src/api/coderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type Workspace,
type WorkspaceAgent,
} from "coder/site/src/api/typesGenerated";
import { type WorkspaceConfiguration } from "vscode";
import * as vscode from "vscode";
import { type ClientOptions } from "ws";

import { CertificateError } from "../error";
Expand All @@ -33,17 +33,12 @@ import { createHttpAgent } from "./utils";

const coderSessionTokenHeader = "Coder-Session-Token";

type WorkspaceConfigurationProvider = () => WorkspaceConfiguration;

/**
* Unified API class that includes both REST API methods from the base Api class
* and WebSocket methods for real-time functionality.
*/
export class CoderApi extends Api {
private constructor(
private readonly output: Logger,
private readonly configProvider: WorkspaceConfigurationProvider,
) {
private constructor(private readonly output: Logger) {
super();
}

Expand All @@ -55,15 +50,14 @@ export class CoderApi extends Api {
baseUrl: string,
token: string | undefined,
output: Logger,
configProvider: WorkspaceConfigurationProvider,
): CoderApi {
const client = new CoderApi(output, configProvider);
const client = new CoderApi(output);
client.setHost(baseUrl);
if (token) {
client.setSessionToken(token);
}

setupInterceptors(client, baseUrl, output, configProvider);
setupInterceptors(client, baseUrl, output);
return client;
}

Expand Down Expand Up @@ -127,7 +121,7 @@ export class CoderApi extends Api {
coderSessionTokenHeader
] as string | undefined;

const httpAgent = createHttpAgent(this.configProvider());
const httpAgent = createHttpAgent(vscode.workspace.getConfiguration());
const webSocket = new OneWayWebSocket<TData>({
location: baseUrl,
...configs,
Expand Down Expand Up @@ -174,14 +168,13 @@ function setupInterceptors(
client: CoderApi,
baseUrl: string,
output: Logger,
configProvider: WorkspaceConfigurationProvider,
): void {
addLoggingInterceptors(client.getAxiosInstance(), output, configProvider);
addLoggingInterceptors(client.getAxiosInstance(), output);

client.getAxiosInstance().interceptors.request.use(async (config) => {
const headers = await getHeaders(
baseUrl,
getHeaderCommand(configProvider()),
getHeaderCommand(vscode.workspace.getConfiguration()),
output,
);
// Add headers from the header command.
Expand All @@ -192,7 +185,7 @@ function setupInterceptors(
// Configure proxy and TLS.
// Note that by default VS Code overrides the agent. To prevent this, set
// `http.proxySupport` to `on` or `off`.
const agent = createHttpAgent(configProvider());
const agent = createHttpAgent(vscode.workspace.getConfiguration());
config.httpsAgent = agent;
config.httpAgent = agent;
config.proxy = false;
Expand All @@ -209,38 +202,35 @@ function setupInterceptors(
);
}

function addLoggingInterceptors(
client: AxiosInstance,
logger: Logger,
configProvider: WorkspaceConfigurationProvider,
) {
function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
client.interceptors.request.use(
(config) => {
const configWithMeta = config as RequestConfigWithMeta;
configWithMeta.metadata = createRequestMeta();
logRequest(logger, configWithMeta, getLogLevel(configProvider()));
logRequest(logger, configWithMeta, getLogLevel());
return config;
},
(error: unknown) => {
logError(logger, error, getLogLevel(configProvider()));
logError(logger, error, getLogLevel());
return Promise.reject(error);
},
);

client.interceptors.response.use(
(response) => {
logResponse(logger, response, getLogLevel(configProvider()));
logResponse(logger, response, getLogLevel());
return response;
},
(error: unknown) => {
logError(logger, error, getLogLevel(configProvider()));
logError(logger, error, getLogLevel());
return Promise.reject(error);
},
);
}

function getLogLevel(cfg: WorkspaceConfiguration): HttpClientLogLevel {
const logLevelStr = cfg
function getLogLevel(): HttpClientLogLevel {
const logLevelStr = vscode.workspace
.getConfiguration()
.get(
"coder.httpClientLogLevel",
HttpClientLogLevel[HttpClientLogLevel.BASIC],
Expand Down
4 changes: 1 addition & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,7 @@ export class Commands {
token: string,
isAutologin: boolean,
): Promise<{ user: User; token: string } | null> {
const client = CoderApi.create(url, token, this.logger, () =>
vscode.workspace.getConfiguration(),
);
const client = CoderApi.create(url, token, this.logger);
if (!needToken(vscode.workspace.getConfiguration())) {
try {
const user = await client.getAuthenticatedUser();
Expand Down
2 changes: 1 addition & 1 deletion src/core/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SecretsManager } from "./secretsManager";
* Service container for dependency injection.
* Centralizes the creation and management of all core services.
*/
export class ServiceContainer {
export class ServiceContainer implements vscode.Disposable {
private readonly logger: vscode.LogOutputChannel;
private readonly pathResolver: PathResolver;
private readonly mementoManager: MementoManager;
Expand Down
134 changes: 77 additions & 57 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
}

const serviceContainer = new ServiceContainer(ctx, vscodeProposed);
ctx.subscriptions.push(serviceContainer);

const output = serviceContainer.getLogger();
const mementoManager = serviceContainer.getMementoManager();
const secretsManager = serviceContainer.getSecretsManager();
Expand All @@ -72,7 +74,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
url || "",
await secretsManager.getSessionToken(),
output,
() => vscode.workspace.getConfiguration(),
);

const myWorkspacesProvider = new WorkspaceProvider(
Expand All @@ -81,33 +82,47 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
output,
5,
);
ctx.subscriptions.push(myWorkspacesProvider);

const allWorkspacesProvider = new WorkspaceProvider(
WorkspaceQuery.All,
client,
output,
);
ctx.subscriptions.push(allWorkspacesProvider);

// createTreeView, unlike registerTreeDataProvider, gives us the tree view API
// (so we can see when it is visible) but otherwise they have the same effect.
const myWsTree = vscode.window.createTreeView(MY_WORKSPACES_TREE_ID, {
treeDataProvider: myWorkspacesProvider,
});
ctx.subscriptions.push(myWsTree);
myWorkspacesProvider.setVisibility(myWsTree.visible);
myWsTree.onDidChangeVisibility((event) => {
myWorkspacesProvider.setVisibility(event.visible);
});
myWsTree.onDidChangeVisibility(
(event) => {
myWorkspacesProvider.setVisibility(event.visible);
},
undefined,
ctx.subscriptions,
);

const allWsTree = vscode.window.createTreeView(ALL_WORKSPACES_TREE_ID, {
treeDataProvider: allWorkspacesProvider,
});
ctx.subscriptions.push(allWsTree);
allWorkspacesProvider.setVisibility(allWsTree.visible);
allWsTree.onDidChangeVisibility((event) => {
allWorkspacesProvider.setVisibility(event.visible);
});
allWsTree.onDidChangeVisibility(
(event) => {
allWorkspacesProvider.setVisibility(event.visible);
},
undefined,
ctx.subscriptions,
);

// Handle vscode:// URIs.
vscode.window.registerUriHandler({
const uriHandler = vscode.window.registerUriHandler({
handleUri: async (uri) => {
const cliManager = serviceContainer.getCliManager();
const params = new URLSearchParams(uri.query);
if (uri.path === "/open") {
const owner = params.get("owner");
Expand Down Expand Up @@ -253,59 +268,63 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
}
},
});

const cliManager = serviceContainer.getCliManager();
ctx.subscriptions.push(uriHandler);

// Register globally available commands. Many of these have visibility
// controlled by contexts, see `when` in the package.json.
const commands = new Commands(serviceContainer, client);
vscode.commands.registerCommand("coder.login", commands.login.bind(commands));
vscode.commands.registerCommand(
"coder.logout",
commands.logout.bind(commands),
);
vscode.commands.registerCommand("coder.open", commands.open.bind(commands));
vscode.commands.registerCommand(
"coder.openDevContainer",
commands.openDevContainer.bind(commands),
);
vscode.commands.registerCommand(
"coder.openFromSidebar",
commands.openFromSidebar.bind(commands),
);
vscode.commands.registerCommand(
"coder.openAppStatus",
commands.openAppStatus.bind(commands),
);
vscode.commands.registerCommand(
"coder.workspace.update",
commands.updateWorkspace.bind(commands),
);
vscode.commands.registerCommand(
"coder.createWorkspace",
commands.createWorkspace.bind(commands),
);
vscode.commands.registerCommand(
"coder.navigateToWorkspace",
commands.navigateToWorkspace.bind(commands),
);
vscode.commands.registerCommand(
"coder.navigateToWorkspaceSettings",
commands.navigateToWorkspaceSettings.bind(commands),
);
vscode.commands.registerCommand("coder.refreshWorkspaces", () => {
myWorkspacesProvider.fetchAndRefresh();
allWorkspacesProvider.fetchAndRefresh();
});
vscode.commands.registerCommand(
"coder.viewLogs",
commands.viewLogs.bind(commands),
);
vscode.commands.registerCommand("coder.searchMyWorkspaces", async () =>
showTreeViewSearch(MY_WORKSPACES_TREE_ID),
);
vscode.commands.registerCommand("coder.searchAllWorkspaces", async () =>
showTreeViewSearch(ALL_WORKSPACES_TREE_ID),
ctx.subscriptions.push(
vscode.commands.registerCommand(
"coder.login",
commands.login.bind(commands),
),
vscode.commands.registerCommand(
"coder.logout",
commands.logout.bind(commands),
),
vscode.commands.registerCommand("coder.open", commands.open.bind(commands)),
vscode.commands.registerCommand(
"coder.openDevContainer",
commands.openDevContainer.bind(commands),
),
vscode.commands.registerCommand(
"coder.openFromSidebar",
commands.openFromSidebar.bind(commands),
),
vscode.commands.registerCommand(
"coder.openAppStatus",
commands.openAppStatus.bind(commands),
),
vscode.commands.registerCommand(
"coder.workspace.update",
commands.updateWorkspace.bind(commands),
),
vscode.commands.registerCommand(
"coder.createWorkspace",
commands.createWorkspace.bind(commands),
),
vscode.commands.registerCommand(
"coder.navigateToWorkspace",
commands.navigateToWorkspace.bind(commands),
),
vscode.commands.registerCommand(
"coder.navigateToWorkspaceSettings",
commands.navigateToWorkspaceSettings.bind(commands),
),
vscode.commands.registerCommand("coder.refreshWorkspaces", () => {
myWorkspacesProvider.fetchAndRefresh();
allWorkspacesProvider.fetchAndRefresh();
}),
vscode.commands.registerCommand(
"coder.viewLogs",
commands.viewLogs.bind(commands),
),
vscode.commands.registerCommand("coder.searchMyWorkspaces", async () =>
showTreeViewSearch(MY_WORKSPACES_TREE_ID),
),
vscode.commands.registerCommand("coder.searchAllWorkspaces", async () =>
showTreeViewSearch(ALL_WORKSPACES_TREE_ID),
),
);

// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
Expand All @@ -325,6 +344,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
isFirstConnect,
);
if (details) {
ctx.subscriptions.push(details);
// Authenticate the plugin client which is used in the sidebar to display
// workspaces belonging to this deployment.
client.setHost(details.url);
Expand Down
Loading