diff --git a/src/plugins/logs.ts b/src/plugins/logs.ts index e7c1671..8b1faae 100644 --- a/src/plugins/logs.ts +++ b/src/plugins/logs.ts @@ -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 = () => { diff --git a/src/plugins/manage.ts b/src/plugins/manage.ts index 0f02549..126cd74 100644 --- a/src/plugins/manage.ts +++ b/src/plugins/manage.ts @@ -10,12 +10,6 @@ import { 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") { diff --git a/src/plugins/status-bar.ts b/src/plugins/status-bar.ts index 16d9332..3f69330 100644 --- a/src/plugins/status-bar.ts +++ b/src/plugins/status-bar.ts @@ -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; }; @@ -74,7 +73,7 @@ export default createPlugin( }); if (selected) { - commands.executeCommand(selected.command); + void commands.executeCommand(selected.command); } }), ); @@ -82,30 +81,30 @@ export default createPlugin( 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(); }), ); @@ -119,6 +118,7 @@ export default createPlugin( }); } }; + context.subscriptions.push({ dispose() { clearImmediate(refreshStatusBarImmediateId); @@ -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(); }); }, diff --git a/src/utils/promises.ts b/src/utils/promises.ts index 157133a..725e59e 100644 --- a/src/utils/promises.ts +++ b/src/utils/promises.ts @@ -32,3 +32,8 @@ export function minDelay( MIN_TIME_BETWEEN_STEPS_MS, ); } + +/** + * Extracts the resolved type from a Promise. + */ +export type UnwrapPromise = T extends Promise ? U : T; diff --git a/src/utils/setup-status.ts b/src/utils/setup-status.ts index ffff689..7470c57 100644 --- a/src/utils/setup-status.ts +++ b/src/utils/setup-status.ts @@ -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>; onChange(callback: (status: SetupStatus) => void): void; } @@ -20,6 +22,7 @@ export async function createSetupStatusTracker( timeTracker: TimeTracker, ): Promise { const start = Date.now(); + let statuses: UnwrapPromise> | undefined; let status: SetupStatus | undefined; const emitter = createEmitter(outputChannel); const end = Date.now(); @@ -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); }; @@ -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) { diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 2a46ab5..721b79c 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -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 { +export async function checkSetupStatus(outputChannel: LogOutputChannel) { const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] = await Promise.all([ checkLocalstackInstalled(outputChannel), @@ -16,7 +14,10 @@ export async function checkIsSetupRequired( checkIsProfileConfigured(), ]); - return ( - !isInstalled || !isAuthenticated || !isLicenseValid || !isProfileConfigured - ); + return { + isInstalled, + isAuthenticated, + isLicenseValid, + isProfileConfigured, + }; }