p5 0.10.0

A tui client for Pulumi
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
use p5::controller::Action;
use pulumi_automation::{
    event::EngineEvent,
    local::{LocalStack, LocalWorkspace},
    stack::StackChangeSummary,
    workspace::{Deployment, OutputMap, StackSettings, StackSummary},
};
use tokio::sync::mpsc;

use crate::{
    AppContext, AppState,
    state::{
        Loadable, OperationContext, OperationEvents, OperationOptions, OperationProgress,
        ProgramOperation, StackContext, StackOutputs,
    },
    tasks::{AppTask, stack::StackTask, workspace::WorkspaceTask},
};

#[derive(Clone)]
pub enum AppAction {
    Exit,
    SubmitCommand(String),
    ToastError(String),
    PopContext,
    PushContext(AppContext),
    WorkspaceAction(WorkspaceAction),
    StackAction(StackAction),
    ListWorkspaces,
    PersistWorkspaces(Vec<LocalWorkspace>),
    NavigateDown,
    NavigateUp,
    NavigateLeft,
    NavigateRight,
}

#[derive(Clone)]
pub enum WorkspaceAction {
    SelectWorkspace(String),
    PersistWorkspace(LocalWorkspace),
    SelectStack(LocalWorkspace, String),
    ListStacks(LocalWorkspace),
    PersistStacks(LocalWorkspace, Vec<StackSummary>),
    PersistStack(LocalWorkspace, LocalStack),
    PersistStackOutputs(LocalWorkspace, LocalStack, OutputMap),
    PersistStackConfig(LocalWorkspace, LocalStack, StackSettings),
    PersistStackState(LocalWorkspace, LocalStack, Deployment),
    LoadStackState(LocalWorkspace, LocalStack),
    LoadStackOutputs(LocalWorkspace, LocalStack),
    LoadStackConfig(LocalWorkspace, LocalStack),
}

#[derive(Clone)]
pub enum StackAction {
    RunProgram(ProgramOperation, LocalStack, OperationOptions),
    BeginOperation(ProgramOperation, LocalStack, OperationOptions),
    PersistChangeSummary(ProgramOperation, LocalStack, StackChangeSummary),
    PersistEvent(ProgramOperation, LocalStack, EngineEvent),
    PersistOperationDone(ProgramOperation, LocalStack),
}

impl Action for AppAction {
    type State = AppState;
    type Task = AppTask;

    #[tracing::instrument(skip(self, state, task_tx, action_tx))]
    fn handle_action(
        &self,
        state: &mut Self::State,
        task_tx: &mpsc::Sender<Self::Task>,
        action_tx: &mpsc::Sender<Self>,
        cancel_token: &tokio_util::sync::CancellationToken,
    ) -> crate::Result<()> {
        match self {
            AppAction::Exit => {
                cancel_token.cancel();
                Ok(())
            }
            AppAction::NavigateDown => {
                match state.current_context() {
                    AppContext::WorkspaceList => state.workspace_list_state.select_next(),
                    AppContext::StackList => state.stack_list_state.select_next(),
                    AppContext::Stack(stack_context) => match stack_context {
                        StackContext::Resources | StackContext::Operation(_) => {
                            if let Some((_, selection)) = state.stack_resource_state() {
                                selection.scrollable_state.list_state.select_next();
                            }
                        }
                        _ => {}
                    },
                    _ => {}
                }
                Ok(())
            }
            AppAction::NavigateUp => {
                match state.current_context() {
                    AppContext::WorkspaceList => state.workspace_list_state.select_previous(),
                    AppContext::StackList => state.stack_list_state.select_previous(),
                    AppContext::Stack(stack_context) => match stack_context {
                        StackContext::Resources | StackContext::Operation(_) => {
                            if let Some((_, selection)) = state.stack_resource_state() {
                                selection.scrollable_state.list_state.select_previous();
                            }
                        }
                        _ => {}
                    },
                    _ => {}
                }
                Ok(())
            }
            AppAction::NavigateLeft => {
                match state.current_context() {
                    _ => {
                        action_tx.try_send(AppAction::PopContext)?;
                    }
                }

                Ok(())
            }
            AppAction::NavigateRight => {
                match state.current_context() {
                    AppContext::WorkspaceList => {
                        if let Some(workspace) = state.try_selected_workspace() {
                            action_tx.try_send(AppAction::WorkspaceAction(
                                WorkspaceAction::SelectWorkspace(workspace.cwd.clone()),
                            ))?;
                        }
                    }
                    AppContext::StackList => {
                        if let Some(stack) = state.try_selected_stack() {
                            if let Some(workspace) = state.workspace().as_option() {
                                action_tx.try_send(AppAction::WorkspaceAction(
                                    WorkspaceAction::SelectStack(
                                        workspace.clone(),
                                        stack.name.clone(),
                                    ),
                                ))?;
                            }
                        }
                    }
                    _ => {}
                }
                Ok(())
            }
            AppAction::PushContext(context) => {
                state.push_context(context.clone());

                match context {
                    AppContext::Stack(stack_context) => {
                        let workspace = state.workspace();
                        let stack = state.stack();

                        if let (Loadable::Loaded(workspace), Loadable::Loaded(stack)) =
                            (workspace, stack)
                        {
                            match stack_context {
                                crate::state::StackContext::Outputs => {
                                    action_tx.try_send(AppAction::WorkspaceAction(
                                        WorkspaceAction::LoadStackOutputs(
                                            workspace.clone(),
                                            stack.clone(),
                                        ),
                                    ))?;
                                }
                                crate::state::StackContext::Config => {
                                    action_tx.try_send(AppAction::WorkspaceAction(
                                        WorkspaceAction::LoadStackConfig(
                                            workspace.clone(),
                                            stack.clone(),
                                        ),
                                    ))?;
                                }
                                crate::state::StackContext::Resources => {
                                    action_tx.try_send(AppAction::WorkspaceAction(
                                        WorkspaceAction::LoadStackState(
                                            workspace.clone(),
                                            stack.clone(),
                                        ),
                                    ))?;
                                }
                                crate::state::StackContext::Operation(_) => {}
                            }
                        }
                    }
                    AppContext::WorkspaceList => {
                        action_tx.try_send(AppAction::ListWorkspaces)?;
                    }
                    _ => {}
                }

                Ok(())
            }
            AppAction::PopContext => {
                if !state.context_stack.is_empty() {
                    state.context_stack.pop();
                } else {
                    action_tx.try_send(AppAction::Exit)?;
                }
                Ok(())
            }
            AppAction::SubmitCommand(input) => {
                if let Some(action) = crate::command::parse_command_to_action(input, state)? {
                    action_tx.try_send(action)?;
                }
                state.command_prompt.reset();
                if let AppContext::CommandPrompt = state.current_context() {
                    if state.context_stack.len() > 1 {
                        // If we are in the command prompt context, pop it to return to the previous context
                        state.context_stack.pop();
                    }
                }
                Ok(())
            }
            AppAction::ListWorkspaces => {
                state.workspaces = Loadable::Loading;
                task_tx.try_send(AppTask::ListWorkspaces)?;
                Ok(())
            }
            AppAction::PersistWorkspaces(workspaces) => {
                state.workspaces = Loadable::Loaded(workspaces.clone());
                Ok(())
            }
            AppAction::WorkspaceAction(action) => match action {
                WorkspaceAction::ListStacks(workspace) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Loadable::Loading,
                            stack_store: Default::default(),
                        });
                    task_tx.try_send(AppTask::WorkspaceTask(WorkspaceTask::ListStacks(
                        workspace.clone(),
                    )))?;
                    Ok(())
                }
                WorkspaceAction::PersistStacks(workspace, stacks) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .and_modify(|w| {
                            w.stacks = Loadable::Loaded(stacks.clone());
                        })
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Loadable::Loaded(stacks.clone()),
                            stack_store: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::SelectWorkspace(cwd) => {
                    state.select_workspace_by_cwd(cwd.as_str());
                    state.workspace_store.entry(cwd.clone()).or_insert_with(|| {
                        crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loading,
                            stacks: Loadable::Loading,
                            stack_store: Default::default(),
                        }
                    });
                    task_tx.try_send(AppTask::WorkspaceTask(WorkspaceTask::SelectWorkspace(
                        cwd.clone(),
                    )))?;
                    action_tx.try_send(AppAction::PushContext(AppContext::StackList))?;
                    Ok(())
                }
                WorkspaceAction::PersistWorkspace(workspace) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .and_modify(|w| {
                            if let Loadable::Loading = w.workspace {
                                *w = crate::state::WorkspaceOutputs {
                                    workspace: Loadable::Loaded(workspace.clone()),
                                    stacks: Loadable::Loading,
                                    stack_store: Default::default(),
                                };
                            }
                        })
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Loadable::Loading,
                            stack_store: Default::default(),
                        });

                    action_tx.try_send(AppAction::WorkspaceAction(WorkspaceAction::ListStacks(
                        workspace.clone(),
                    )))?;

                    Ok(())
                }
                WorkspaceAction::SelectStack(workspace, name) => {
                    state.select_stack_by_name_and_cwd(name.as_str(), workspace.cwd.as_str());
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(name.clone())
                        .and_modify(|s| {
                            s.stack = Loadable::Loading;
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loading,
                            config: Loadable::Loading,
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Default::default(),
                        });
                    task_tx.try_send(AppTask::WorkspaceTask(WorkspaceTask::SelectStack(
                        workspace.clone(),
                        name.clone(),
                    )))?;
                    action_tx.try_send(AppAction::PushContext(AppContext::Stack(
                        Default::default(),
                    )))?;
                    Ok(())
                }
                WorkspaceAction::PersistStack(workspace, stack) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.stack = Loadable::Loaded(stack.clone());
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            config: Loadable::Loading,
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::PersistStackOutputs(workspace, stack, outputs) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.outputs = Loadable::Loaded(outputs.clone());
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            outputs: Loadable::Loaded(outputs.clone()),
                            config: Default::default(),
                            state: Default::default(),
                            operation: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::PersistStackConfig(workspace, stack, config) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.config = Loadable::Loaded(config.clone());
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            config: Loadable::Loaded(config.clone()),
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::PersistStackState(workspace, stack, stack_state) => {
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.state = Loadable::Loaded(stack_state.clone());
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            config: Default::default(),
                            outputs: Default::default(),
                            state: Loadable::Loaded(stack_state.clone()),
                            operation: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::LoadStackState(workspace, stack) => {
                    task_tx.try_send(AppTask::WorkspaceTask(WorkspaceTask::GetStackState(
                        workspace.clone(),
                        stack.clone(),
                    )))?;
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.state = Loadable::Loading;
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            config: Default::default(),
                            outputs: Default::default(),
                            state: Loadable::Loading,
                            operation: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::LoadStackOutputs(workspace, stack) => {
                    task_tx.try_send(AppTask::WorkspaceTask(WorkspaceTask::GetStackOutputs(
                        workspace.clone(),
                        stack.clone(),
                    )))?;
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.outputs = Loadable::Loading;
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            config: Default::default(),
                            outputs: Loadable::Loading,
                            state: Default::default(),
                            operation: Default::default(),
                        });
                    Ok(())
                }
                WorkspaceAction::LoadStackConfig(workspace, stack) => {
                    task_tx.try_send(AppTask::WorkspaceTask(WorkspaceTask::GetStackConfig(
                        workspace.clone(),
                        stack.clone(),
                    )))?;
                    state
                        .workspace_store
                        .entry(workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(stack.name.clone())
                        .and_modify(|s| {
                            s.config = Loadable::Loading;
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(stack.clone()),
                            config: Loadable::Loading,
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Default::default(),
                        });
                    Ok(())
                }
            },
            AppAction::StackAction(action) => match action {
                StackAction::RunProgram(operation, local_stack, options) => {
                    action_tx.try_send(AppAction::PushContext(AppContext::Stack(
                        StackContext::Operation(OperationContext::Summary),
                    )))?;
                    action_tx.try_send(AppAction::StackAction(StackAction::BeginOperation(
                        operation.clone(),
                        local_stack.clone(),
                        options.clone(),
                    )))?;

                    Ok(())
                }
                StackAction::BeginOperation(operation, local_stack, options) => {
                    task_tx.try_send(AppTask::StackTask(StackTask::RunOperation(
                        operation.clone(),
                        local_stack.clone(),
                        options.clone(),
                    )))?;
                    state
                        .workspace_store
                        .entry(local_stack.workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(local_stack.workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(local_stack.name.clone())
                        .and_modify(|s| {
                            s.operation = Some(OperationProgress {
                                operation: operation.clone(),
                                options: Some(options.clone()),
                                change_summary: Loadable::Loading,
                                events: Default::default(),
                            });
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(local_stack.clone()),
                            config: Default::default(),
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Some(OperationProgress {
                                operation: operation.clone(),
                                options: Some(options.clone()),
                                change_summary: Loadable::Loading,
                                events: Default::default(),
                            }),
                        });
                    Ok(())
                }
                StackAction::PersistChangeSummary(operation, local_stack, stack_change_summary) => {
                    state
                        .workspace_store
                        .entry(local_stack.workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(local_stack.workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(local_stack.name.clone())
                        .and_modify(|s| {
                            if let Some(op) = &mut s.operation {
                                op.change_summary = Loadable::Loaded(stack_change_summary.clone());
                            } else {
                                s.operation = Some(OperationProgress {
                                    operation: operation.clone(),
                                    change_summary: Loadable::Loaded(stack_change_summary.clone()),
                                    events: Default::default(),
                                    options: Default::default(),
                                });
                            }
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(local_stack.clone()),
                            config: Default::default(),
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Some(OperationProgress {
                                operation: operation.clone(),
                                change_summary: Loadable::Loaded(stack_change_summary.clone()),
                                events: Default::default(),
                                options: Default::default(),
                            }),
                        });
                    Ok(())
                }
                StackAction::PersistEvent(operation, local_stack, engine_event) => {
                    let outputs = state
                        .workspace_store
                        .entry(local_stack.workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(local_stack.workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(local_stack.name.clone())
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(local_stack.clone()),
                            config: Default::default(),
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Some(OperationProgress {
                                operation: operation.clone(),
                                change_summary: Loadable::default(),
                                events: Loadable::Loaded(OperationEvents {
                                    events: vec![],
                                    states: vec![],
                                    done: false,
                                }),
                                options: Default::default(),
                            }),
                        });

                    let operation = outputs
                        .operation
                        .as_mut()
                        .expect("Operation should be present");

                    let events = operation.events.as_mut_or_default(OperationEvents {
                        events: vec![],
                        states: vec![],
                        done: false,
                    });

                    if events.events.is_empty() {
                        action_tx.try_send(AppAction::PushContext(AppContext::Stack(
                            StackContext::Operation(OperationContext::Events),
                        )))?;
                    }

                    if let Err(err) = events.apply_event(engine_event.clone()) {
                        tracing::error!("Failed to apply engine event: {}", err);
                    }

                    Ok(())
                }
                StackAction::PersistOperationDone(op, local_stack) => {
                    state
                        .workspace_store
                        .entry(local_stack.workspace.cwd.clone())
                        .or_insert_with(|| crate::state::WorkspaceOutputs {
                            workspace: Loadable::Loaded(local_stack.workspace.clone()),
                            stacks: Default::default(),
                            stack_store: Default::default(),
                        })
                        .stack_store
                        .entry(local_stack.name.clone())
                        .and_modify(|s| {
                            match &mut s.operation {
                                Some(op) => {
                                    let events = op.events.as_mut_or_default(OperationEvents {
                                        events: vec![],
                                        states: vec![],
                                        done: true,
                                    });
                                    events.done = true;
                                    op.events = Loadable::Loaded(events.clone());
                                    op
                                }
                                None => {
                                    s.operation = Some(OperationProgress {
                                        operation: op.clone(),
                                        change_summary: Loadable::default(),
                                        events: Loadable::Loaded(OperationEvents {
                                            events: vec![],
                                            states: vec![],
                                            done: true,
                                        }),
                                        options: Default::default(),
                                    });
                                    s.operation.as_mut().unwrap()
                                }
                            };
                        })
                        .or_insert_with(|| StackOutputs {
                            stack: Loadable::Loaded(local_stack.clone()),
                            config: Default::default(),
                            outputs: Default::default(),
                            state: Default::default(),
                            operation: Some(OperationProgress {
                                operation: op.clone(),
                                change_summary: Loadable::default(),
                                events: Loadable::Loaded(OperationEvents {
                                    events: vec![],
                                    states: vec![],
                                    done: true,
                                }),
                                options: Default::default(),
                            }),
                        });
                    Ok(())
                }
            },
            AppAction::ToastError(message) => {
                let expr_dt = chrono::Utc::now() + chrono::Duration::seconds(3);
                state.toast = Some((expr_dt, message.clone()));
                Ok(())
            }
        }
    }
}