feat(studio): module uninstall + Doctor daemon control (v0.17.0)
UI parity for Windows operators who never touch a shell:
- Module sheet gains an Uninstall button at the bottom.
Two-step confirmation, then calls UninstallModule + closes
the sheet + refreshes the Modules page.
- Doctor page picks up two new sections:
* Daemon files — log / config / audit DB / modules /
flows / pid, each with a one-click "Open" button that
shells out to the OS handler (open / xdg-open /
explorer).
* Daemon control — Restart / Stop / Status buttons that
spawn `fai daemon …`. Captured stdout/stderr renders
inline so the operator sees what happened.
- Update banner gains an "Apply update" button that spawns
`fai update apply --channel <c>`. The previous version
showed the command as text — Windows users had no way to
execute it.
- Event log panel gains a "Verify now" button that re-runs
the chain check (via the existing doctor refresh).
New SystemActions helper resolves the `fai` binary via
$FAI_BIN → PATH → `~/.fai/bin/fai` (or the Windows
equivalent), so the buttons work whether the operator
restarted their shell after installing or not.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
d9aabfd00a
commit
73e394b39f
8 changed files with 624 additions and 49 deletions
|
|
@ -15,9 +15,11 @@ class FaiModuleSheet extends StatefulWidget {
|
|||
|
||||
const FaiModuleSheet({super.key, required this.moduleName});
|
||||
|
||||
/// Convenience launcher used from the Modules list.
|
||||
static Future<void> show(BuildContext context, String name) {
|
||||
return showModalBottomSheet<void>(
|
||||
/// Convenience launcher used from the Modules list. Resolves
|
||||
/// to `true` when the operator uninstalled the module — the
|
||||
/// caller (Modules page) refreshes its list on that signal.
|
||||
static Future<bool> show(BuildContext context, String name) async {
|
||||
final r = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -28,6 +30,7 @@ class FaiModuleSheet extends StatefulWidget {
|
|||
),
|
||||
builder: (_) => FaiModuleSheet(moduleName: name),
|
||||
);
|
||||
return r ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -36,6 +39,7 @@ class FaiModuleSheet extends StatefulWidget {
|
|||
|
||||
class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
||||
late final Future<ModuleDetail> _future;
|
||||
bool _uninstalling = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -43,6 +47,50 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
_future = HubService.instance.moduleInfo(widget.moduleName);
|
||||
}
|
||||
|
||||
Future<void> _confirmAndUninstall(ModuleDetail detail) async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Uninstall module?'),
|
||||
content: Text(
|
||||
'Removes ${detail.name} v${detail.version} from this hub. '
|
||||
'Flows that reference it will fail at the next run. '
|
||||
'A `module.uninstalled` audit event is recorded.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||||
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
||||
),
|
||||
child: const Text('Uninstall'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || !mounted) return;
|
||||
setState(() => _uninstalling = true);
|
||||
try {
|
||||
final r = await HubService.instance.uninstallModule(detail.name);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Uninstalled ${r.name} v${r.version}.')),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _uninstalling = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Uninstall failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -73,7 +121,13 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
),
|
||||
)
|
||||
else
|
||||
Flexible(child: _Body(detail: snapshot.data!)),
|
||||
Flexible(
|
||||
child: _Body(
|
||||
detail: snapshot.data!,
|
||||
uninstalling: _uninstalling,
|
||||
onUninstall: () => _confirmAndUninstall(snapshot.data!),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
@ -102,8 +156,14 @@ class _Handle extends StatelessWidget {
|
|||
|
||||
class _Body extends StatelessWidget {
|
||||
final ModuleDetail detail;
|
||||
final bool uninstalling;
|
||||
final VoidCallback onUninstall;
|
||||
|
||||
const _Body({required this.detail});
|
||||
const _Body({
|
||||
required this.detail,
|
||||
required this.uninstalling,
|
||||
required this.onUninstall,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -196,6 +256,29 @@ class _Body extends StatelessWidget {
|
|||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.xxl),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
OutlinedButton.icon(
|
||||
onPressed: uninstalling ? null : onUninstall,
|
||||
icon: uninstalling
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.delete_outline, size: 16),
|
||||
label: Text(uninstalling ? 'Uninstalling…' : 'Uninstall'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.error.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue