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
8 changes: 8 additions & 0 deletions src/plugins/logs.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { spawn } from "node:child_process";
import type { ChildProcess } from "node:child_process";

import { commands } from "vscode";

import { createPlugin } from "../plugins.ts";
import { pipeToLogOutputChannel } from "../utils/spawn.ts";

export default createPlugin(
"logs",
({ context, outputChannel, containerStatusTracker }) => {
context.subscriptions.push(
commands.registerCommand("localstack.viewLogs", () => {
outputChannel.show(true);
}),
);

let logsProcess: ChildProcess | undefined;

const startLogging = () => {
Expand Down
6 changes: 0 additions & 6 deletions src/plugins/manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@
export default createPlugin(
"manage",
({ context, outputChannel, telemetry, localStackStatusTracker }) => {
context.subscriptions.push(
commands.registerCommand("localstack.viewLogs", () => {
outputChannel.show(true);
}),
);

context.subscriptions.push(
commands.registerCommand("localstack.start", async () => {
if (localStackStatusTracker.status() !== "stopped") {
window.showInformationMessage("LocalStack is already running.");

Check warning on line 16 in src/plugins/manage.ts

View workflow job for this annotation

GitHub Actions / Lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
return;
}
localStackStatusTracker.forceContainerStatus("running");
Expand All @@ -34,7 +28,7 @@
context.subscriptions.push(
commands.registerCommand("localstack.stop", () => {
if (localStackStatusTracker.status() !== "running") {
window.showInformationMessage("LocalStack is not running.");

Check warning on line 31 in src/plugins/manage.ts

View workflow job for this annotation

GitHub Actions / Lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
return;
}
localStackStatusTracker.forceContainerStatus("stopping");
Expand Down
130 changes: 66 additions & 64 deletions src/plugins/status-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,69 @@ import { commands, QuickPickItemKind, ThemeColor, window } from "vscode";
import type { QuickPickItem } from "vscode";

import { createPlugin } from "../plugins.ts";
import { checkIsProfileConfigured } from "../utils/configure-aws.ts";
import { checkLocalstackInstalled } from "../utils/install.ts";

export default createPlugin(
"status-bar",
({ context, statusBarItem, localStackStatusTracker, setupStatusTracker }) => {
({
context,
statusBarItem,
localStackStatusTracker,
setupStatusTracker,
outputChannel,
}) => {
context.subscriptions.push(
commands.registerCommand("localstack.showCommands", async () => {
const getCommands = async () => {
const shouldShowLocalStackStart = () =>
setupStatusTracker.statuses().isInstalled &&
localStackStatusTracker.status() === "stopped";
const shouldShowLocalStackStop = () =>
setupStatusTracker.statuses().isInstalled &&
localStackStatusTracker.status() === "running";
const shouldShowRunSetupWizard = () =>
setupStatusTracker.status() === "setup_required";

const getCommands = () => {
const commands: (QuickPickItem & { command: string })[] = [];

commands.push({
label: "Manage",
label: "Configure",
command: "",
kind: QuickPickItemKind.Separator,
});
const setupStatus = setupStatusTracker.status();

if (setupStatus === "ok") {
if (localStackStatusTracker.status() === "stopped") {
commands.push({
label: "Start LocalStack",
command: "localstack.start",
});
} else {
commands.push({
label: "Stop LocalStack",
command: "localstack.stop",
});
}

if (shouldShowRunSetupWizard()) {
commands.push({
label: "Run LocalStack Setup Wizard",
command: "localstack.setup",
});
}

commands.push({
label: "Configure",
label: "Manage",
command: "",
kind: QuickPickItemKind.Separator,
});

if (setupStatus === "setup_required") {
if (shouldShowLocalStackStart()) {
commands.push({
label: "Run LocalStack setup Wizard",
command: "localstack.setup",
label: "Start LocalStack",
command: "localstack.start",
});

// show start command if stopped or stop command when running, even if setup_required (in authentication, or profile)
if (localStackStatusTracker.status() === "stopped") {
commands.push({
label: "Start LocalStack",
command: "localstack.start",
});
} else if (localStackStatusTracker.status() === "running") {
commands.push({
label: "Stop LocalStack",
command: "localstack.stop",
});
}
}

const isProfileConfigured = await checkIsProfileConfigured();
if (!isProfileConfigured) {
if (shouldShowLocalStackStop()) {
commands.push({
label: "Configure AWS Profiles",
command: "localstack.configureAwsProfiles",
label: "Stop LocalStack",
command: "localstack.stop",
});
}

commands.push({
label: "View Logs",
command: "localstack.viewLogs",
});

return commands;
};

Expand All @@ -74,38 +73,38 @@ export default createPlugin(
});

if (selected) {
commands.executeCommand(selected.command);
void commands.executeCommand(selected.command);
}
}),
);

context.subscriptions.push(
commands.registerCommand("localstack.refreshStatusBar", () => {
const setupStatus = setupStatusTracker.status();

if (setupStatus === "setup_required") {
statusBarItem.command = "localstack.showCommands";
statusBarItem.text = "$(error) LocalStack";
statusBarItem.backgroundColor = new ThemeColor(
"statusBarItem.errorBackground",
);
} else {
statusBarItem.command = "localstack.showCommands";
statusBarItem.backgroundColor = undefined;
const localStackStatus = localStackStatusTracker.status();
if (
localStackStatus === "starting" ||
localStackStatus === "stopping"
) {
statusBarItem.text = `$(sync~spin) LocalStack (${localStackStatus})`;
} else if (
localStackStatus === "running" ||
localStackStatus === "stopped"
) {
statusBarItem.text = `$(localstack-logo) LocalStack (${localStackStatus})`;
}
}

const localStackStatus = localStackStatusTracker.status();
const localStackInstalled = setupStatusTracker.statuses().isInstalled;

statusBarItem.command = "localstack.showCommands";
statusBarItem.backgroundColor =
setupStatus === "setup_required"
? new ThemeColor("statusBarItem.errorBackground")
: undefined;

const shouldSpin =
localStackStatus === "starting" || localStackStatus === "stopping";
const icon =
setupStatus === "setup_required"
? "$(error)"
: shouldSpin
? "$(sync~spin)"
: "$(localstack-logo)";

const statusText = localStackInstalled
? `${localStackStatus}`
: "not installed";
statusBarItem.text = `${icon} LocalStack: ${statusText}`;

statusBarItem.tooltip = "Show LocalStack commands";
statusBarItem.show();
}),
);
Expand All @@ -119,6 +118,7 @@ export default createPlugin(
});
}
};

context.subscriptions.push({
dispose() {
clearImmediate(refreshStatusBarImmediateId);
Expand All @@ -128,10 +128,12 @@ export default createPlugin(
refreshStatusBarImmediate();

localStackStatusTracker.onChange(() => {
outputChannel.trace("[status-bar]: localStackStatusTracker changed");
refreshStatusBarImmediate();
});

setupStatusTracker.onChange(() => {
outputChannel.trace("[status-bar]: setupStatusTracker changed");
refreshStatusBarImmediate();
});
},
Expand Down
5 changes: 5 additions & 0 deletions src/utils/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ export function minDelay<T>(
MIN_TIME_BETWEEN_STEPS_MS,
);
}

/**
* Extracts the resolved type from a Promise.
*/
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
16 changes: 14 additions & 2 deletions src/utils/setup-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import ms from "ms";
import type { Disposable, LogOutputChannel } from "vscode";

import { createEmitter } from "./emitter.ts";
import { checkIsSetupRequired } from "./setup.ts";
import type { UnwrapPromise } from "./promises.ts";
import { checkSetupStatus } from "./setup.ts";
import type { TimeTracker } from "./time-tracker.ts";

export type SetupStatus = "ok" | "setup_required";

export interface SetupStatusTracker extends Disposable {
status(): SetupStatus;
statuses(): UnwrapPromise<ReturnType<typeof checkSetupStatus>>;
onChange(callback: (status: SetupStatus) => void): void;
}

Expand All @@ -20,6 +22,7 @@ export async function createSetupStatusTracker(
timeTracker: TimeTracker,
): Promise<SetupStatusTracker> {
const start = Date.now();
let statuses: UnwrapPromise<ReturnType<typeof checkSetupStatus>> | undefined;
let status: SetupStatus | undefined;
const emitter = createEmitter<SetupStatus>(outputChannel);
const end = Date.now();
Expand All @@ -29,13 +32,18 @@ export async function createSetupStatusTracker(

let timeout: NodeJS.Timeout | undefined;
const startChecking = async () => {
const setupRequired = await checkIsSetupRequired(outputChannel);
statuses = await checkSetupStatus(outputChannel);

const setupRequired = Object.values(statuses).some(
(check) => check === false,
);
const newStatus = setupRequired ? "setup_required" : "ok";
if (status !== newStatus) {
status = newStatus;
await emitter.emit(status);
}

// TODO: Find a smarter way to check the status (e.g. watch for changes in AWS credentials or LocalStack installation)
timeout = setTimeout(() => void startChecking(), 1_000);
};

Expand All @@ -48,6 +56,10 @@ export async function createSetupStatusTracker(
// biome-ignore lint/style/noNonNullAssertion: false positive
return status!;
},
statuses() {
// biome-ignore lint/style/noNonNullAssertion: false positive
return statuses!;
},
onChange(callback) {
emitter.on(callback);
if (status) {
Expand Down
13 changes: 7 additions & 6 deletions src/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { checkIsProfileConfigured } from "./configure-aws.ts";
import { checkLocalstackInstalled } from "./install.ts";
import { checkIsLicenseValid } from "./license.ts";

export async function checkIsSetupRequired(
outputChannel: LogOutputChannel,
): Promise<boolean> {
export async function checkSetupStatus(outputChannel: LogOutputChannel) {
const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] =
await Promise.all([
checkLocalstackInstalled(outputChannel),
Expand All @@ -16,7 +14,10 @@ export async function checkIsSetupRequired(
checkIsProfileConfigured(),
]);

return (
!isInstalled || !isAuthenticated || !isLicenseValid || !isProfileConfigured
);
return {
isInstalled,
isAuthenticated,
isLicenseValid,
isProfileConfigured,
};
}
Loading