Skip to content

Commit

Permalink
feat: improve UX and states lifecycle
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangabriele committed Jul 2, 2023
1 parent 254d251 commit f255498
Show file tree
Hide file tree
Showing 20 changed files with 467 additions and 274 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "5.60.1",
"@vscode/test-electron": "2.3.3",
"dayjs": "1.11.9",
"eslint": "8.44.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-typescript": "17.0.0",
Expand Down
18 changes: 9 additions & 9 deletions src/commands/linkWorkspaceToHerokuApp.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { $ } from 'execa'
import { window, workspace } from 'vscode'

import { HerokuApp } from './types'
import { type HerokuApp } from './types'
import { exec } from '../helpers/exec'
import { handleError } from '../helpers/handleError'
import { showProgressNotification } from '../helpers/showProgressNotification'
import { statusBarItemManager } from '../libs/StatusBarItemManager'
import { UserError } from '../libs/UserError'

export async function linkWorkspaceToHerokuApp() {
try {
if (!workspace.workspaceFolders) {
const firstWorkspaceFolder = workspace.workspaceFolders?.at(0)
if (!firstWorkspaceFolder) {
return
}

const currentWorkspaceDirectoryPath = workspace.workspaceFolders[0].uri.fsPath
const currentWorkspaceDirectoryPath = firstWorkspaceFolder.uri.fsPath

const herokuAppsNames = await showProgressNotification('Listing current Heroku apps...', async () => {
const { stderr, stdout: herokuAppsAsJson } = await $({
cwd: currentWorkspaceDirectoryPath,
})`heroku apps -A --json`
if (stderr) {
throw new UserError('An error happened while trying to list your currents Heroku apps.', stderr)
}
const { stdout: herokuAppsAsJson } = await exec(`heroku apps -A --json`, { shouldThrowOnStderr: true })

const herokuAppsJson = JSON.parse(herokuAppsAsJson.trim()) as HerokuApp[]

Expand All @@ -44,6 +42,8 @@ export async function linkWorkspaceToHerokuApp() {
}
})

statusBarItemManager.load()

window.showInformationMessage(`Your current workspace is now linked to the "${herokuAppName}" Heroku app.`)
} catch (err) {
handleError(err)
Expand Down
3 changes: 3 additions & 0 deletions src/commands/logInToHerokuCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EOL } from 'os'
import { window } from 'vscode'

import { handleError } from '../helpers/handleError'
import { statusBarItemManager } from '../libs/StatusBarItemManager'

export async function logInToHerokuCli() {
try {
Expand All @@ -17,6 +18,8 @@ export async function logInToHerokuCli() {
child.stdin.end()
})

statusBarItemManager.load()

window.showInformationMessage('You are now logged in to Heroku CLI.')
} catch (err) {
handleError(err)
Expand Down
3 changes: 3 additions & 0 deletions src/commands/logOutOfHerokuCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { window } from 'vscode'
import { exec } from '../helpers/exec'
import { handleError } from '../helpers/handleError'
import { showProgressNotification } from '../helpers/showProgressNotification'
import { statusBarItemManager } from '../libs/StatusBarItemManager'

export async function logOutOfHerokuCli() {
try {
await showProgressNotification('Logging out from Heroku...', async () => {
await exec('heroku logout')
})

statusBarItemManager.load()

window.showInformationMessage('You are now logged out of Heroku CLI.')
} catch (err) {
handleError(err)
Expand Down
55 changes: 7 additions & 48 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import isCommand from 'is-command'
import { commands, ExtensionContext, window, workspace } from 'vscode'
import { commands, type ExtensionContext, workspace } from 'vscode'

import { linkWorkspaceToHerokuApp } from './commands/linkWorkspaceToHerokuApp'
import { logInToHerokuCli } from './commands/logInToHerokuCli'
import { logOutOfHerokuCli } from './commands/logOutOfHerokuCli'
import { ACTION } from './constants'
import { handleError } from './helpers/handleError'
import { isHerokuCliAuthenticated } from './helpers/isHerokuCliAuthenticated'
import { HerokuStatus } from './libs/HerokuStatus'
import { statusBarItemManager } from './libs/StatusBarItemManager'

export async function activate(context: ExtensionContext) {
try {
Expand All @@ -18,49 +15,6 @@ export async function activate(context: ExtensionContext) {
return
}

// -------------------------------------------------------------------------
// Is Heroku CLI available?

if (!(await isCommand('heroku'))) {
const action = await window.showWarningMessage(
"Heroku CLI doesn't seem to be installed. Please install it and reload VS Code.",
ACTION.NO_HEROKU_CLI.label,
)

if (action === ACTION.NO_HEROKU_CLI.label) {
commands.executeCommand('vscode.open', ACTION.NO_HEROKU_CLI.uri)
}

return
}

// -------------------------------------------------------------------------
// Is Heroku CLI authenticated?

if (!(await isHerokuCliAuthenticated())) {
const action = await window.showWarningMessage(
'You are not logged in to Heroku CLI. Do you want to log in?',
ACTION.NO_HEROKU_AUTH.label,
)

if (action === ACTION.NO_HEROKU_AUTH.label) {
await logInToHerokuCli()

if (!(await isHerokuCliAuthenticated())) {
return
}
}

return
}

// -------------------------------------------------------------------------
// Launch Heroku Status Bar

const herokuStatus = new HerokuStatus()

herokuStatus.start()

// -------------------------------------------------------------------------
// Register Heroku commands

Expand All @@ -80,6 +34,11 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(linkWorkspaceToHerokuAppDisposable)
context.subscriptions.push(logInToHerokuCliDisposable)
context.subscriptions.push(logOutOfHerokuCliDisposable)

// -------------------------------------------------------------------------
// Launch Heroku Status Bar Item

statusBarItemManager.load(true)
} catch (err) {
handleError(err)
}
Expand Down
16 changes: 9 additions & 7 deletions src/helpers/exec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execa, ExecaReturnValue, Options } from 'execa'
import { execa, type ExecaReturnValue, type Options } from 'execa'
import { workspace } from 'vscode'

import { InternalError } from '../libs/InternalError'
Expand All @@ -13,21 +13,23 @@ const DEFAULT_OPTIONS: ExecOptions = {
}

export async function exec(statement: string, options: ExecOptions = {}): Promise<ExecaReturnValue<string>> {
if (!workspace.workspaceFolders) {
throw new InternalError('`workspace.workspaceFolders` is undefined.')
}
if (workspace.workspaceFolders.length === 0) {
throw new InternalError('`workspace.workspaceFolders` is empty.')
const firstWorkspaceFolder = workspace.workspaceFolders?.at(0)
if (!firstWorkspaceFolder) {
throw new InternalError('`firstWorkspaceFolder` is undefined.')
}

const controlledOptions = {
...DEFAULT_OPTIONS,
cwd: workspace.workspaceFolders[0].uri.fsPath,
cwd: firstWorkspaceFolder.uri.fsPath,
...options,
}
const { shouldThrowOnStderr, ...execaOptions } = controlledOptions

const [command, ...args] = statement.split(' ')
if (!command) {
throw new InternalError('`command` is undefined.')
}

const execaChildProcess = await execa(command, args, execaOptions)

if (shouldThrowOnStderr && execaChildProcess.stderr.length > 0) {
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/isEmpty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function isEmpty(value: string | null | undefined) {
// eslint-disable-next-line no-null/no-null
return value === undefined || value === null || value.trim() === ''
}
36 changes: 36 additions & 0 deletions src/libs/HerokuReleaseManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { mapHerokuApiReleaseToHerokuRelease } from './utils'
import { InternalError } from '../InternalError'

import type { HerokuRelease } from './types'
import type { HerokuApi } from '../../types'

export class HerokuReleaseManager {
#lastRelease: HerokuRelease | undefined
#releases: HerokuRelease[]

get lastRelease(): HerokuRelease | undefined {
return this.#lastRelease
}

constructor(stdoutJson: string) {
this.#releases = []

this.updateReleases(stdoutJson)
}

updateReleases(stdoutJson: string): void {
try {
const herokuApiReleases: HerokuApi.Release[] = JSON.parse(stdoutJson.trim())

this.#releases = herokuApiReleases.map(mapHerokuApiReleaseToHerokuRelease)
} catch (err) {
throw new InternalError(`An error happened while parsing:\n\`${stdoutJson}\`.`, err)
}

this.#updateLastRelease()
}

#updateLastRelease(): void {
this.#lastRelease = this.#releases.filter(({ isSlugLess }) => !isSlugLess).at(0)
}
}
12 changes: 12 additions & 0 deletions src/libs/HerokuReleaseManager/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HerokuApi } from '../../types'

export interface HerokuRelease {
readonly apiData: HerokuApi.Release
readonly createdAt: Date
readonly description: string
readonly isCurrent: boolean
readonly isSlugLess: boolean
readonly status: HerokuApi.ReleaseStatus
readonly updatedAt: Date
readonly version: number
}
16 changes: 16 additions & 0 deletions src/libs/HerokuReleaseManager/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { HerokuRelease } from './types'
import type { HerokuApi } from '../../types'

export function mapHerokuApiReleaseToHerokuRelease(herokuApiRelease: HerokuApi.Release): HerokuRelease {
return Object.freeze({
apiData: Object.freeze(herokuApiRelease),
createdAt: new Date(herokuApiRelease.created_at),
description: herokuApiRelease.description,
isCurrent: herokuApiRelease.current,
// eslint-disable-next-line no-null/no-null
isSlugLess: herokuApiRelease.slug === null,
status: herokuApiRelease.status,
updatedAt: new Date(herokuApiRelease.updated_at),
version: herokuApiRelease.version,
})
}
47 changes: 0 additions & 47 deletions src/libs/HerokuStatus/constants.ts

This file was deleted.

Loading

0 comments on commit f255498

Please sign in to comment.