Skip to content

Commit 84dbe86

Browse files
committed
feat: WIP transitioned apply and prompts to the new model
1 parent e836872 commit 84dbe86

File tree

5 files changed

+66
-71
lines changed

5 files changed

+66
-71
lines changed

src/ui/components/default-component.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function DefaultComponent(props: {
3131
const [importResult, setImportResult] = useState<ImportResult | null>(null);
3232
const [sudoAttemptCount, setSudoAttemptCount] = useState(0);
3333
const [confirmationMessage, setConfirmationMessage] = useState('');
34+
const [disableSudoPrompt, setDisableSudoPrompt] = useState(false);
3435

3536
const [{ status: renderStatus, data: renderData }] = useAtom(store.renderState);
3637
const [progressState] = useAtom(store.progressState);
@@ -97,9 +98,13 @@ export function DefaultComponent(props: {
9798
setRequiredParametersForImport(null);
9899
setShowImportParametersPrompt(false);
99100
})
101+
102+
emitter.on(RenderEvent.DISABLE_SUDO_PROMPT, (isDisabled) => {
103+
setDisableSudoPrompt(isDisabled);
104+
})
100105
}, []);
101106

102-
// console.log(renderStatus);
107+
console.log(renderStatus);
103108
// console.log(renderData);
104109
//
105110
// console.log(renderStatus);
@@ -138,12 +143,12 @@ export function DefaultComponent(props: {
138143
)
139144
}
140145
{
141-
showSudoPrompt && (
146+
renderStatus === RenderStatus.SUDO_PROMPT && (
142147
<Box flexDirection="column">
143148
<Text>Password:</Text>
144149
{/* Use sudoAttemptCount as a hack to reset password input between attempts */}
145-
<PasswordInput key={sudoAttemptCount} onSubmit={(password) => {
146-
emitter.emit(RenderEvent.PROMPT_SUDO_RESULT, password);
150+
<PasswordInput isDisabled={disableSudoPrompt} key={renderData as number} onSubmit={(password) => {
151+
emitter.emit(RenderEvent.SUDO_PROMPT_RESULT, password);
147152
}}/>
148153
</Box>
149154
)

src/ui/reporters/default-reporter.tsx

Lines changed: 48 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import chalk from 'chalk';
2-
import { SudoRequestData , SudoRequestResponseData } from 'codify-schemas';
2+
import { SudoRequestData, SudoRequestResponseData } from 'codify-schemas';
33
import { render } from 'ink';
44
import { EventEmitter } from 'node:events';
55
import React from 'react';
66

77
import { Plan } from '../../entities/plan.js';
8-
import { Event, ProcessName, SubProcessName, ctx } from '../../events/context.js';
8+
import { ctx, Event, ProcessName, SubProcessName } from '../../events/context.js';
99
import { ImportResult, RequiredParameters, UserSuppliedParameters } from '../../orchestrators/import.js';
1010
import { SudoUtils } from '../../utils/sudo.js';
1111
import { DefaultComponent } from '../components/default-component.js';
1212
import { ProgressState, ProgressStatus } from '../components/progress/progress-display.js';
1313
import { RenderStatus, store } from '../store/index.js';
14-
import { DisplayPlanStateTransition, RenderEvent, RenderState, Reporter } from './reporter.js';
14+
import { RenderEvent, RenderState, Reporter } from './reporter.js';
15+
import { sleep } from '../../utils/index.js';
1516

1617
const ProgressLabelMapping = {
1718
[ProcessName.APPLY]: 'Codify apply',
@@ -75,57 +76,36 @@ export class DefaultReporter implements Reporter {
7576
password = await this.getUserPassword();
7677
}
7778

78-
const result = await SudoUtils.runCommand(data.command, data.options, secureMode, pluginName, password)
79-
this.renderEmitter.emit(RenderEvent.PROMPT_SUDO_GRANTED);
80-
81-
return result;
79+
return SudoUtils.runCommand(data.command, data.options, secureMode, pluginName, password)
8280
}
8381

8482
displayPlan(plan: Plan): void {
85-
store.set(store.renderState, { status: RenderStatus.DISPLAY_PLAN, data: plan });
83+
this.updateRenderState(RenderStatus.DISPLAY_PLAN, plan)
8684
store.set(store.progressState, null);
8785
this.progressState = null;
88-
89-
// this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, {
90-
// nextState: RenderState.DISPLAY_PLAN,
91-
// plan,
92-
// } as DisplayPlanStateTransition);
9386
}
9487

9588
async promptConfirmation(message: string): Promise<boolean> {
96-
const result = await Promise.all([
97-
new Promise<boolean>((resolve) => {
98-
this.renderEmitter.once(RenderEvent.PROMPT_CONFIRMATION_RESULT, (isConfirmed) => resolve(isConfirmed as boolean));
99-
}),
100-
101-
store.set(store.renderState, { status: RenderStatus.PROMPT_CONFIRMATION, data: message })
102-
// this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, {
103-
// nextState: RenderState.PROMPT_CONFIRMATION,
104-
// message,
105-
// }),
106-
])
107-
108-
const continueApply = result[0];
89+
const continueApply = await this.updateStateAndAwaitEvent<boolean>(
90+
() => this.updateRenderState(RenderStatus.PROMPT_CONFIRMATION, message),
91+
RenderEvent.PROMPT_CONFIRMATION_RESULT
92+
)
10993

11094
if (continueApply) {
11195
// this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, {
11296
// nextState: RenderState.APPLYING,
11397
// });
11498

115-
store.set(store.renderState, { status: RenderStatus.PROGRESS });
116-
99+
this.updateRenderState(RenderStatus.PROGRESS)
117100
this.log(`${message} -> "Yes"`)
118101
}
119102

120103
return continueApply;
121104
}
122105

123-
displayApplyComplete(messages: string[]): Promise<void> | void {
124-
store.set(store.renderState, { status: RenderStatus.APPLY_COMPLETE });
125-
126-
// this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, {
127-
// nextState: RenderState.APPLY_COMPLETE,
128-
// });
106+
async displayApplyComplete(messages: string[]): Promise<void> {
107+
this.updateRenderState(RenderStatus.APPLY_COMPLETE, messages);
108+
await sleep(100); // This gives the renderer enough time to complete before the prompt exits
129109
}
130110

131111
private log(args: string): void {
@@ -143,45 +123,33 @@ export class DefaultReporter implements Reporter {
143123
};
144124

145125
this.log(`${label} started`)
146-
147-
store.set(store.progressState, structuredClone(this.progressState));
148-
// this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState);
126+
store.set(store.progressState, this.progressState);
149127
}
150128

151129
private onProcessFinishEvent(name: ProcessName): void {
152130
const label = ProgressLabelMapping[name];
153-
154-
this.progressState!.status = ProgressStatus.FINISHED;
155-
156131
this.log(`${label} finished successfully`)
157132

158-
store.internal.set(store.progressState, structuredClone(this.progressState));
133+
this.progressState!.status = ProgressStatus.FINISHED;
134+
store.set(store.progressState, structuredClone(this.progressState));
159135
// this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState);
160136
}
161137

162138
private onSubprocessStartEvent(name: SubProcessName, additionalName?: string): void {
163-
const label = ProgressLabelMapping[name] + (additionalName
164-
? ' ' + additionalName
165-
: ''
166-
);
139+
const label = ProgressLabelMapping[name] + (additionalName ? ' ' + additionalName : '');
140+
this.log(`${label} started`)
167141

168142
this.progressState?.subProgresses?.push({
169143
label,
170144
name: name + additionalName,
171145
status: ProgressStatus.IN_PROGRESS,
172146
});
173-
174-
this.log(`${label} started`)
175-
176147
store.set(store.progressState, structuredClone(this.progressState));
177148
// this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState);
178149
}
179150

180151
private onSubprocessFinishEvent(name: SubProcessName, additionalName?: string): void {
181-
const label = ProgressLabelMapping[name] + (additionalName
182-
? ' ' + additionalName
183-
: ''
184-
);
152+
const label = ProgressLabelMapping[name] + (additionalName ? ' ' + additionalName : '');
185153

186154
const subProgress = this.progressState
187155
?.subProgresses
@@ -202,12 +170,17 @@ export class DefaultReporter implements Reporter {
202170
let attemptCount = 0;
203171

204172
while (attemptCount < 3) {
205-
const passwordAttempt = await this.renderSudoPrompt(attemptCount);
173+
this.renderEmitter.emit(RenderEvent.DISABLE_SUDO_PROMPT, false);
174+
const passwordAttempt = await this.updateStateAndAwaitEvent<string>(
175+
() => this.updateRenderState(RenderStatus.SUDO_PROMPT, attemptCount),
176+
RenderEvent.SUDO_PROMPT_RESULT,
177+
);
178+
this.renderEmitter.emit(RenderEvent.DISABLE_SUDO_PROMPT, true);
206179

207180
// Validates that the password works
208181
if (SudoUtils.validate(passwordAttempt)) {
209-
this.renderEmitter.emit(RenderEvent.PROMPT_SUDO_GRANTED);
210-
return passwordAttempt
182+
this.updateRenderState(RenderStatus.PROGRESS)
183+
return passwordAttempt;
211184
}
212185

213186
if (attemptCount + 1 < 3) {
@@ -218,19 +191,11 @@ export class DefaultReporter implements Reporter {
218191
attemptCount++;
219192
}
220193

221-
this.renderEmitter.emit(RenderEvent.PROMPT_SUDO_ERROR);
194+
this.updateRenderState(null)
195+
store.set(store.renderState, { status: null });
222196
throw new Error('sudo: 3 incorrect password attempts')
223197
}
224198

225-
private async renderSudoPrompt(attemptCount: number): Promise<string> {
226-
return new Promise((resolve) => {
227-
this.renderEmitter.emit(RenderEvent.PROMPT_SUDO, attemptCount);
228-
this.renderEmitter.on(RenderEvent.PROMPT_SUDO_RESULT, (password) => {
229-
resolve(password)
230-
})
231-
})
232-
}
233-
234199
private extractUserSuppliedParametersFromResult(result: object): Map<string, Record<string, unknown>> {
235200
const resources = Object.entries(result)
236201
.map(([key, value]) => {
@@ -251,4 +216,22 @@ export class DefaultReporter implements Reporter {
251216

252217
return new Map(Object.entries(resources));
253218
}
219+
220+
private updateRenderState(status: RenderStatus | null, data?: unknown): void {
221+
store.set(store.renderState, { status, data });
222+
}
223+
224+
// This is needed if we await any prompts. It makes the UI feel a lot more fluid since the await is no longer blocking
225+
private async updateStateAndAwaitEvent<T>(fn: () => void, eventName: string): Promise<T> {
226+
return (await Promise.all([
227+
fn(),
228+
this.awaitEvent(eventName)
229+
])).at(1) as T;
230+
}
231+
232+
private awaitEvent<T>(name: string): Promise<T> {
233+
return new Promise((resolve) => {
234+
this.renderEmitter.once(name, resolve)
235+
});
236+
}
254237
}

src/ui/reporters/reporter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ export enum RenderEvent {
1111
PROGRESS_UPDATE = 'progressUpdate',
1212
PROMPT_CONFIRMATION_RESULT = 'promptConfirmation',
1313
PROMPT_SUDO = 'promptSudo',
14+
DISABLE_SUDO_PROMPT = 'disableSudoPrompt',
1415
PROMPT_IMPORT_PARAMETERS = 'promptImportParameters',
1516
PROMPT_IMPORT_PARAMETERS_RESULT = 'promptImportParametersResult',
1617
PROMPT_SUDO_ERROR = 'promptSudoError',
1718
PROMPT_SUDO_GRANTED = 'promptSudoGranted',
18-
PROMPT_SUDO_RESULT = 'promptSudoResult',
19+
SUDO_PROMPT_RESULT = 'promptSudoResult',
1920
STATE_TRANSITION = 'stateTransition',
2021
}
2122

src/ui/store/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { atom, createStore, getDefaultStore, Setter, Getter, Atom, WritableAtom
33
import { ProgressState } from '../components/progress/progress-display.js';
44

55
export interface RenderState {
6-
status: RenderStatus;
6+
status: RenderStatus | null;
77
data?: unknown;
88
}
99

src/utils/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ export function getTypeAndNameFromId(id: string): { type: string; name: string |
2323
export function getId(type: string, name?: string): string {
2424
return name ? `${type}.${name}` : type;
2525
}
26+
27+
export function sleep(ms: number): Promise<void> {
28+
return new Promise(resolve => {
29+
setTimeout(resolve, ms)
30+
});
31+
}

0 commit comments

Comments
 (0)