Skip to content

Commit 9605deb

Browse files
committed
Added proper progress display component. Added proper state machine events to organize component
1 parent 3acf413 commit 9605deb

4 files changed

Lines changed: 156 additions & 91 deletions

File tree

src/ui/components/default-component.tsx

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,93 @@
1-
import { Select, Spinner, StatusMessage } from '@inkjs/ui';
1+
import { Select } from '@inkjs/ui';
2+
import { PlanResponseData } from 'codify-schemas';
23
import { Box, Static, Text } from 'ink';
34
import { EventEmitter } from 'node:events';
45
import React, { useEffect, useState } from 'react';
56

6-
import { ProcessState, ProcessStatus } from '../reporters/default-reporter.js';
7-
import { PlanResponseData } from 'codify-schemas';
7+
import { RenderEvent, RenderState } from '../reporters/reporter.js';
88
import { PlanComponent } from './plan/plan.js';
9+
import { ProgressDisplay, ProgressState } from './progress/progress-display.js';
910

1011
export function DefaultComponent(props: {
1112
emitter: EventEmitter
1213
}) {
1314
const { emitter } = props;
1415

15-
const [staticOutput, setStaticOutput] = useState([] as Array<string>);
16-
const [processState, setProcessState] = useState({
17-
process: [],
18-
} as ProcessState);
19-
const [planState, setPlanState] = useState(null as PlanResponseData[] | null);
20-
const [showConfirm, setShowConfirm] = useState(false);
16+
const [state, setState] = useState(RenderState.GENERATING_PLAN);
17+
const [staticOutput, setStaticOutput] = useState([] as Array<Record<string, unknown> | string>);
18+
const [progressState, setProgressState] = useState(null as ProgressState | null);
19+
const [plan, setPlan] = useState(null as PlanResponseData[] | null);
2120
const [confirmValue, setConfirmValue] = useState(null as boolean | null)
2221

2322
useEffect(() => {
24-
emitter.on('static_output', (newValue: any) => {
25-
setStaticOutput([...newValue]);
26-
});
23+
emitter.on(RenderEvent.STATE_TRANSITION, (obj) => {
24+
switch (obj.nextState) {
25+
case RenderState.GENERATING_PLAN: {
26+
setProgressState(obj.progressState);
27+
setState(obj.nextState);
28+
break;
29+
}
30+
31+
case RenderState.DISPLAY_PLAN: {
32+
setPlan(obj.plan);
33+
setState(obj.nextState);
34+
break;
35+
}
36+
37+
case RenderState.ASK_CONFIRMATION: {
38+
setState(obj.nextState);
39+
break;
40+
}
41+
42+
case RenderState.APPLYING: {
43+
break;
44+
}
45+
}
46+
})
2747

28-
emitter.on('process', (state: ProcessState) => {
29-
setProcessState(structuredClone(state));
48+
emitter.once(RenderEvent.LOG, (newValue: string) => {
49+
setStaticOutput([...newValue]);
3050
});
3151

32-
emitter.on('plan', (plan: PlanResponseData[]) => {
33-
setPlanState(plan);
52+
emitter.on(RenderEvent.PROCESS_UPDATE, (state: ProgressState) => {
53+
setProgressState(structuredClone(state));
3454
});
35-
emitter.on('promptConfirmation', () => {
36-
setShowConfirm(true);
37-
})
3855
}, []);
3956

4057
return <Box flexDirection="column">
4158
<Static items={staticOutput}>
4259
{
43-
(text, idx) => <Text color="cyan" key={idx}>{text}</Text>
60+
(text, idx) => <Text color="cyan" key={idx}>{text.toString()}</Text>
4461
}
4562
</Static>
4663
{
47-
processState.process?.map((item, i) =>
48-
<Box flexDirection="column" key={i}>
49-
{
50-
item.status === ProcessStatus.IN_PROGRESS
51-
? <Spinner label={item.name}/>
52-
: <StatusMessage variant="success">{item.name}</StatusMessage>
53-
}
54-
<Box flexDirection="column" marginLeft={2}>
55-
{
56-
item.subprocess?.map((subItem, i) =>
57-
subItem.status === ProcessStatus.IN_PROGRESS
58-
? <Spinner key={i} label={subItem.name}/>
59-
: <StatusMessage key={i} variant="success">{subItem.name}</StatusMessage>
60-
) ?? []
61-
}
62-
</Box>
63-
</Box>
64-
) ?? []
64+
state >= RenderState.DISPLAY_PLAN && plan && <Static items={[plan]}>{
65+
(plan, idx) => <PlanComponent key={idx} plan={plan}/>
66+
}</Static>
6567
}
6668
{
67-
planState
68-
? <Static items={[planState]}>{
69-
(plan, idx) => <PlanComponent key={idx} plan={plan}/>
70-
}</Static>
71-
: <></>
69+
(state === RenderState.GENERATING_PLAN || state === RenderState.APPLYING) && progressState &&
70+
<ProgressDisplay progress={progressState}/>
7271
}
7372
{
74-
showConfirm && (
73+
state === RenderState.ASK_CONFIRMATION && (
7574
confirmValue === null
7675
? <Box flexDirection="column">
7776
<Text>Do you want to apply the above changes?</Text>
78-
<Select options={[
77+
<Select onChange={(value) => {
78+
setConfirmValue(value === 'yes');
79+
emitter.emit(RenderEvent.PROMPT_RESULT, value === 'yes')
80+
}} options={[
7981
{ label: 'Yes', value: 'yes' },
8082
{ label: 'No', value: 'no' },
81-
]} onChange={(value) => {
82-
console.log(value);
83-
setConfirmValue(value === 'yes');
84-
emitter.emit('promptConfirmation_Result', value === 'yes')
85-
}}/>
83+
]}/>
8684
</Box>
8785
: <Box flexDirection="column">
8886
<Text>Do you want to apply the above changes?</Text>
89-
<Select options={[
87+
<Select highlightText={confirmValue ? 'Yes' : 'No'} isDisabled options={[
9088
{ label: 'Yes', value: 'yes' },
9189
{ label: 'No', value: 'no' },
92-
]} isDisabled highlightText={confirmValue ? 'Yes' : 'No'}/>
90+
]}/>
9391
</Box>
9492
)
9593
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Spinner, StatusMessage } from '@inkjs/ui';
2+
import { Box } from 'ink';
3+
import React from 'react';
4+
5+
export enum ProgressStatus {
6+
IN_PROGRESS,
7+
FINISHED,
8+
}
9+
10+
export interface ProgressState {
11+
label: string,
12+
status: ProgressStatus;
13+
subProgress: ProgressState[] | null,
14+
}
15+
16+
export function ProgressDisplay(
17+
props: {
18+
progress: ProgressState,
19+
}
20+
) {
21+
const { label, status, subProgress } = props.progress;
22+
23+
return <Box flexDirection="column">
24+
{
25+
status === ProgressStatus.IN_PROGRESS
26+
? <Spinner label={label}/>
27+
: <StatusMessage variant="success">{label}</StatusMessage>
28+
}
29+
{
30+
subProgress && <Box flexDirection="column" marginLeft={2}>
31+
<SubProgressDisplay subProgresses={subProgress}/>
32+
</Box>
33+
}
34+
</Box>
35+
}
36+
37+
export function SubProgressDisplay(
38+
props: {
39+
subProgresses: ProgressState[],
40+
}
41+
) {
42+
const { subProgresses } = props;
43+
44+
return <>{
45+
subProgresses.map((s, idx) => <Box>
46+
{
47+
s.status === ProgressStatus.IN_PROGRESS
48+
? <Spinner key={idx} label={s.label}/>
49+
: <StatusMessage key={idx} variant="success">{s.label}</StatusMessage>
50+
}
51+
{
52+
s.subProgress && <Box flexDirection="column" marginLeft={2}>
53+
<SubProgressDisplay subProgresses={s.subProgress}/>
54+
</Box>
55+
}
56+
</Box>)
57+
}</>
58+
}

src/ui/reporters/default-reporter.tsx

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,16 @@ import React from 'react';
55

66
import { ctx, Event } from '../../events/context.js';
77
import { DefaultComponent } from '../components/default-component.js';
8-
import { Reporter } from './reporter.js';
9-
10-
export enum ProcessStatus {
11-
NOT_STARTED,
12-
IN_PROGRESS,
13-
FINISHED,
14-
}
15-
16-
export interface ProcessState {
17-
process: Array<{
18-
name: string;
19-
status: ProcessStatus;
20-
subprocess: Array<{
21-
name: string;
22-
status: ProcessStatus;
23-
}>
24-
}>
25-
}
8+
import { DisplayPlanStateTransition, RenderEvent, RenderState, Reporter } from './reporter.js';
269

2710
export class DefaultReporter implements Reporter {
2811

2912
private renderEmitter = new EventEmitter();
3013
private staticOutput = new Array<any>()
31-
private processState = {
32-
process: [],
33-
} as ProcessState
3414

3515
constructor() {
36-
ctx.on(Event.OUTPUT, (...args) => this.onOutputEvent(...args));
37-
ctx.on(Event.PROCESS_START, (name) => this.onProcessStartEvent(name))
16+
ctx.on(Event.OUTPUT, (...args) => this.renderLog(...args));
17+
ctx.on(Event.PROCESS_START, (name) => this.onProcessEvent(name))
3818
ctx.on(Event.PROCESS_FINISH, (name) => this.onProcessFinishEvent(name))
3919
ctx.on(Event.SUB_PROCESS_START, (name, processName) => this.onSubprocessStartEvent(name, processName));
4020
ctx.on(Event.SUB_PROCESS_FINISH, (name, processName) => this.onSubprocessFinishEvent(name, processName))
@@ -46,34 +26,38 @@ export class DefaultReporter implements Reporter {
4626
async promptConfirmation(): Promise<boolean> {
4727
const result = await Promise.all([
4828
new Promise<boolean>((resolve) => {
49-
this.renderEmitter.once('promptConfirmation_Result', (isConfirmed) => resolve(isConfirmed as boolean));
29+
this.renderEmitter.once(RenderEvent.PROMPT_RESULT, (isConfirmed) => resolve(isConfirmed as boolean));
30+
}),
31+
this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, {
32+
nextState: RenderState.ASK_CONFIRMATION,
5033
}),
51-
this.renderEmitter.emit('promptConfirmation'),
5234
])
5335

5436

5537
return result[0];
5638
}
5739

5840
displayPlan(plan: PlanResponseData[]): void {
59-
this.renderEmitter.emit('process', []);
60-
this.renderEmitter.emit('plan', plan);
41+
this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, {
42+
nextState: RenderState.DISPLAY_PLAN,
43+
plan,
44+
} as DisplayPlanStateTransition);
6145
}
6246

63-
private onOutputEvent(...args: unknown[]) {
64-
this.staticOutput.push(...args)
65-
this.renderEmitter.emit('static_output', this.staticOutput);
47+
private renderLog(...args: unknown[]) {
48+
this.staticOutput.push(...args);
49+
this.renderEmitter.emit(RenderEvent.LOG, this.staticOutput);
6650
}
6751

68-
private onProcessStartEvent(name: string): void {
52+
private onProcessEvent(name: string): void {
6953
this.processState.process.push({
7054
name,
7155
status: ProcessStatus.IN_PROGRESS,
7256
subprocess: [],
7357
})
7458

75-
this.onOutputEvent(`${name} started`)
76-
this.renderEmitter.emit('process', this.processState);
59+
this.renderLog(`${name} started`)
60+
this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState);
7761
}
7862

7963
private onProcessFinishEvent(name: string): void {
@@ -85,8 +69,8 @@ export class DefaultReporter implements Reporter {
8569

8670
process.status = ProcessStatus.FINISHED;
8771

88-
this.onOutputEvent(`${name} finished successfully`)
89-
this.renderEmitter.emit('process', this.processState.process);
72+
this.renderLog(`${name} finished successfully`)
73+
this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState.process);
9074

9175
}
9276

@@ -101,8 +85,8 @@ export class DefaultReporter implements Reporter {
10185
status: ProcessStatus.IN_PROGRESS,
10286
})
10387

104-
this.onOutputEvent(`${name} started`)
105-
this.renderEmitter.emit('process', this.processState);
88+
this.renderLog(`${name} started`)
89+
this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState);
10690
}
10791

10892
private onSubprocessFinishEvent(name: string, processName: string): void {
@@ -119,8 +103,8 @@ export class DefaultReporter implements Reporter {
119103

120104
subprocess.status = ProcessStatus.FINISHED;
121105

122-
this.onOutputEvent(`${name} finished successfully`)
123-
this.renderEmitter.emit('process', this.processState);
106+
this.renderLog(`${name} finished successfully`)
107+
this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState);
124108
}
125109

126110
}

src/ui/reporters/reporter.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
import { PlanResponseData } from 'codify-schemas';
22

3+
export enum RenderEvent {
4+
STATE_TRANSITION = 'stateTransition',
5+
LOG = 'log',
6+
PROCESS_UPDATE = 'processUpdate',
7+
PROMPT_RESULT = 'promptResult'
8+
}
9+
10+
/**
11+
* Reporter to component (ink) communication is designed to be a state machine.
12+
*/
13+
export enum RenderState {
14+
GENERATING_PLAN,
15+
DISPLAY_PLAN,
16+
ASK_CONFIRMATION,
17+
APPLYING,
18+
}
19+
20+
export interface StateTransition {
21+
nextState: RenderState;
22+
}
23+
24+
export interface DisplayPlanStateTransition extends StateTransition {
25+
plan: PlanResponseData[];
26+
}
27+
328
export interface Reporter {
429
promptConfirmation(): Promise<boolean>
530

0 commit comments

Comments
 (0)