Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
132 changes: 67 additions & 65 deletions src/plugins/status-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,66 @@ 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",
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 (shouldShowLocalStackStart()) {
commands.push({
label: "Start LocalStack",
command: "localstack.start",
});
}

if (shouldShowLocalStackStop()) {
commands.push({
label: "Stop LocalStack",
command: "localstack.stop",
});
}

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

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

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

// 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) {
commands.push({
label: "Configure AWS Profiles",
command: "localstack.configureAwsProfiles",
});
}

return commands;
Expand All @@ -74,38 +72,39 @@ export default createPlugin(
});

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

context.subscriptions.push(
commands.registerCommand("localstack.refreshStatusBar", () => {
// TODO
Comment thread
skyrpex marked this conversation as resolved.
Outdated
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