Skip to content

Commit

Permalink
Merge pull request #12 from oclif/mdonnalley/skip-to
Browse files Browse the repository at this point in the history
feat: implement .skipTo and refactor .stop
  • Loading branch information
iowillhoit authored Aug 30, 2024
2 parents e76864e + f4ed5d2 commit f5e5a8a
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 27 deletions.
94 changes: 83 additions & 11 deletions src/multi-stage-output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
StagesProps,
} from './components/stages.js'
import {Design, RequiredDesign, constructDesignParams} from './design.js'
import {StageTracker} from './stage-tracker.js'
import {StageStatus, StageTracker} from './stage-tracker.js'
import {readableTime} from './utils.js'

// Taken from https://github.com/sindresorhus/is-in-ci
Expand Down Expand Up @@ -281,6 +281,24 @@ export class MultiStageOutput<T extends Record<string, unknown>> implements Disp
}
}

/**
* Stop multi-stage output from running with a failed status.
*/
public error(): void {
this.stop('failed')
}

/**
* Go to a stage, marking any stages in between the current stage and the provided stage as completed.
*
* If the stage does not exist or is before the current stage, nothing will happen.
*
* If the stage is the same as the current stage, the data will be updated.
*
* @param stage Stage to go to
* @param data - Optional data to pass to the next stage.
* @returns void
*/
public goto(stage: string, data?: Partial<T>): void {
if (this.stopped) return

Expand All @@ -290,29 +308,73 @@ export class MultiStageOutput<T extends Record<string, unknown>> implements Disp
// prevent going to a previous stage
if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current ?? this.stages[0])) return

this.update(stage, data)
this.update(stage, 'completed', data)
}

/**
* Moves to the next stage of the process.
*
* @param data - Optional data to pass to the next stage.
* @returns void
*/
public next(data?: Partial<T>): void {
if (this.stopped) return

const nextStageIndex = this.stages.indexOf(this.stageTracker.current ?? this.stages[0]) + 1
if (nextStageIndex < this.stages.length) {
this.update(this.stages[nextStageIndex], data)
this.update(this.stages[nextStageIndex], 'completed', data)
}
}

public stop(error?: Error): void {
/**
* Go to a stage, marking any stages in between the current stage and the provided stage as skipped.
*
* If the stage does not exist or is before the current stage, nothing will happen.
*
* If the stage is the same as the current stage, the data will be updated.
*
* @param stage Stage to go to
* @param data - Optional data to pass to the next stage.
* @returns void
*/
public skipTo(stage: string, data?: Partial<T>): void {
if (this.stopped) return

// ignore non-existent stages
if (!this.stages.includes(stage)) return

// prevent going to a previous stage
if (this.stages.indexOf(stage) < this.stages.indexOf(this.stageTracker.current ?? this.stages[0])) return

this.update(stage, 'skipped', data)
}

/**
* Stop multi-stage output from running.
*
* The stage currently running will be changed to the provided `finalStatus`.
*
* @param finalStatus - The status to set the current stage to.
* @returns void
*/
public stop(finalStatus: StageStatus = 'completed'): void {
if (this.stopped) return
this.stopped = true

this.stageTracker.refresh(this.stageTracker.current ?? this.stages[0], {hasError: Boolean(error), isStopping: true})
this.stageTracker.refresh(this.stageTracker.current ?? this.stages[0], {
finalStatus,
})

if (isInCi) {
this.ciInstance?.stop(this.stageTracker)
return
}

// The underlying components expect an Error, although they don't currently use anything on the error - they check if it exists.
// Instead of refactoring the components to take a boolean, we pass in a placeholder Error,
// which, gives us the flexibility in the future to pass in an actual Error if we want
const error = finalStatus === 'failed' ? new Error('Error') : undefined

const stagesInput = {...this.generateStagesInput(), ...(error ? {error} : {})}

this.inkInstance?.rerender(<Stages {...stagesInput} />)
Expand All @@ -323,11 +385,17 @@ export class MultiStageOutput<T extends Record<string, unknown>> implements Disp
this.inkInstance?.unmount()
}

/**
* Updates the data of the component.
*
* @param data - The partial data object to update the component's data with.
* @returns void
*/
public updateData(data: Partial<T>): void {
if (this.stopped) return
this.data = {...this.data, ...data} as T

this.update(this.stageTracker.current ?? this.stages[0], data)
this.rerender()
}

private formatKeyValuePairs(infoBlock: InfoBlock<T> | StageInfoBlock<T> | undefined): FormattedKeyValue[] {
Expand Down Expand Up @@ -361,15 +429,19 @@ export class MultiStageOutput<T extends Record<string, unknown>> implements Disp
}
}

private update(stage: string, data?: Partial<T>): void {
this.data = {...this.data, ...data} as Partial<T>

this.stageTracker.refresh(stage)

private rerender(): void {
if (isInCi) {
this.ciInstance?.update(this.stageTracker, this.data)
} else {
this.inkInstance?.rerender(<Stages {...this.generateStagesInput()} />)
}
}

private update(stage: string, bypassStatus: StageStatus, data?: Partial<T>): void {
this.data = {...this.data, ...data} as Partial<T>

this.stageTracker.refresh(stage, {bypassStatus})

this.rerender()
}
}
22 changes: 8 additions & 14 deletions src/stage-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,16 @@ export class StageTracker {
return this.map.get(stage)
}

public refresh(nextStage: string, opts?: {hasError?: boolean; isStopping?: boolean}): void {
public refresh(nextStage: string, opts?: {finalStatus?: StageStatus; bypassStatus?: StageStatus}): void {
const stages = [...this.map.keys()]

for (const stage of stages) {
if (this.map.get(stage) === 'skipped') continue
if (this.map.get(stage) === 'failed') continue

// .stop() was called with an error => set the stage to failed
if (nextStage === stage && opts?.hasError) {
this.set(stage, 'failed')
this.stopMarker(stage)
continue
}

// .stop() was called without an error => set the stage to completed
if (nextStage === stage && opts?.isStopping) {
this.set(stage, 'completed')
// .stop() was called with a finalStatus
if (nextStage === stage && opts?.finalStatus) {
this.set(stage, opts.finalStatus)
this.stopMarker(stage)
continue
}
Expand All @@ -50,13 +44,13 @@ export class StageTracker {
continue
}

// any stage before the current stage should be marked as skipped if it's still pending
// any pending stage before the current stage should be marked using opts.bypassStatus
if (stages.indexOf(stage) < stages.indexOf(nextStage) && this.map.get(stage) === 'pending') {
this.set(stage, 'skipped')
this.set(stage, opts?.bypassStatus ?? 'completed')
continue
}

// any stage before the current stage should be as completed (if it hasn't been marked as skipped or failed yet)
// any stage before the current stage should be marked as completed (if it hasn't been marked as skipped or failed yet)
if (stages.indexOf(nextStage) > stages.indexOf(stage)) {
this.set(stage, 'completed')
this.stopMarker(stage)
Expand Down
24 changes: 22 additions & 2 deletions test/stage-tracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,33 @@ describe('StageTracker', () => {

it("should set the current stage to error when there's an error", () => {
const tracker = new StageTracker(['one', 'two', 'three'])
tracker.refresh('two', {hasError: true})
tracker.refresh('two', {finalStatus: 'failed'})
expect(tracker.get('two')).to.equal('failed')
})

it('should set the current stage to completed when stopping', () => {
const tracker = new StageTracker(['one', 'two', 'three'])
tracker.refresh('two', {isStopping: true})
tracker.refresh('two', {finalStatus: 'completed'})
expect(tracker.get('two')).to.equal('completed')
})

it('should mark bypassed steps as completed', () => {
const tracker = new StageTracker(['one', 'two', 'three'])
tracker.refresh('three', {bypassStatus: 'completed'})
expect(tracker.get('two')).to.equal('completed')
})

it('should mark bypassed steps as skipped', () => {
const tracker = new StageTracker(['one', 'two', 'three'])
tracker.refresh('three', {bypassStatus: 'skipped'})
expect(tracker.get('two')).to.equal('skipped')
})

it('should mark previous current step as completed', () => {
const tracker = new StageTracker(['one', 'two', 'three'])
tracker.refresh('one')
tracker.refresh('two')
expect(tracker.get('one')).to.equal('completed')
expect(tracker.get('two')).to.equal('current')
})
})

0 comments on commit f5e5a8a

Please sign in to comment.