mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
* fix(systemd): Fix the issue of special characters in unit names In systemd unit processing, filtering of special characters in unit names has been added to prevent command injection and security issues. Additionally, the rendering performance of the unit list has been optimized by merging unnecessary watch calls and removing confirmation dialogs to simplify the operation process. * feat(Systemd): Add a confirmation dialog for systemd unit operations Display a confirmation dialog when stopping or restarting systemd units to prevent accidental operations. For other operations, directly navigate to the SSH page to execute commands. * fix(systemd): Fix the range of characters allowed in unit names Extend the regular expression to allow more valid characters, including dots, @, and colons, in system unit names, to support a broader range of unit naming conventions * fix(systemd): Fix the issue of parsing service names with dots When dealing with service IDs containing multiple dots (such as org.cups.cupsd.service), correctly extract the service name and type. When there are no dots in the service ID, set the type to an empty string.
168 lines
5.4 KiB
Dart
168 lines
5.4 KiB
Dart
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:server_box/core/route.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
import 'package:server_box/data/model/server/systemd.dart';
|
|
import 'package:server_box/data/provider/systemd.dart';
|
|
import 'package:server_box/view/page/ssh/page/page.dart';
|
|
|
|
final class SystemdPage extends ConsumerStatefulWidget {
|
|
final SpiRequiredArgs args;
|
|
|
|
const SystemdPage({super.key, required this.args});
|
|
|
|
static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');
|
|
|
|
@override
|
|
ConsumerState<SystemdPage> createState() => _SystemdPageState();
|
|
}
|
|
|
|
final class _SystemdPageState extends ConsumerState<SystemdPage> {
|
|
late final _pro = systemdProvider(widget.args.spi);
|
|
|
|
late final _notifier = ref.read(_pro.notifier);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: CustomAppBar(
|
|
title: const Text('Systemd'),
|
|
actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _notifier.getUnits)] : null,
|
|
),
|
|
body: RefreshIndicator(onRefresh: _notifier.getUnits, child: _buildBody()),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
final isBusy = ref.watch(_pro.select((pro) => pro.isBusy));
|
|
return CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
_buildScopeFilterChips(),
|
|
AnimatedContainer(
|
|
duration: Durations.medium1,
|
|
curve: Curves.fastEaseInToSlowEaseOut,
|
|
height: isBusy ? SizedLoading.medium.size : 0,
|
|
width: isBusy ? SizedLoading.medium.size : 0,
|
|
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_buildUnitList(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildScopeFilterChips() {
|
|
final currentFilter = ref.watch(_pro.select((p) => p.scopeFilter));
|
|
return Wrap(
|
|
spacing: 8,
|
|
children: SystemdScopeFilter.values.map((filter) {
|
|
final isSelected = filter == currentFilter;
|
|
return FilterChip(
|
|
selected: isSelected,
|
|
label: Text(filter.displayName),
|
|
onSelected: (_) => _notifier.setScopeFilter(filter),
|
|
);
|
|
}).toList(),
|
|
).paddingSymmetric(horizontal: 13, vertical: 8);
|
|
}
|
|
|
|
Widget _buildUnitList() {
|
|
ref.watch(_pro.select((p) => (p.units, p.scopeFilter)));
|
|
final filteredUnits = _notifier.filteredUnits;
|
|
if (filteredUnits.isEmpty) {
|
|
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
|
}
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
|
final unit = filteredUnits[index];
|
|
return ListTile(
|
|
leading: _buildScopeTag(unit.scope),
|
|
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
|
subtitle: Wrap(
|
|
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
|
).paddingOnly(top: 7),
|
|
trailing: _buildUnitFuncs(unit),
|
|
).cardx.paddingSymmetric(horizontal: 13);
|
|
}, childCount: filteredUnits.length),
|
|
);
|
|
}
|
|
|
|
Widget _buildUnitFuncs(SystemdUnit unit) {
|
|
return PopupMenu(
|
|
items: unit.availableFuncs.map(_buildUnitFuncBtn).toList(),
|
|
onSelected: (val) => _handleUnitFuncSelected(unit, val),
|
|
);
|
|
}
|
|
|
|
void _handleUnitFuncSelected(SystemdUnit unit, SystemdUnitFunc func) {
|
|
final cmd = unit.getCmd(func: func, isRoot: widget.args.spi.isRoot);
|
|
|
|
if (func == SystemdUnitFunc.stop || func == SystemdUnitFunc.restart) {
|
|
_showConfirmDialog(cmd);
|
|
} else {
|
|
_navigateToSsh(cmd);
|
|
}
|
|
}
|
|
|
|
void _showConfirmDialog(String cmd) async {
|
|
final sure = await context.showRoundDialog(
|
|
title: libL10n.attention,
|
|
child: SimpleMarkdown(data: '```shell\n$cmd\n```'),
|
|
actions: [
|
|
Btn.cancel(),
|
|
CountDownBtn(
|
|
seconds: 3,
|
|
onTap: () => context.pop(true),
|
|
text: libL10n.ok,
|
|
afterColor: Colors.red,
|
|
),
|
|
],
|
|
);
|
|
if (sure == true) _navigateToSsh(cmd);
|
|
}
|
|
|
|
void _navigateToSsh(String cmd) {
|
|
final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);
|
|
SSHPage.route.go(context, args);
|
|
}
|
|
|
|
PopupMenuEntry _buildUnitFuncBtn(SystemdUnitFunc func) {
|
|
return PopupMenuItem<SystemdUnitFunc>(
|
|
value: func,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [Icon(func.icon, size: 19), const SizedBox(width: 10), Text(func.name.capitalize)],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildScopeTag(SystemdUnitScope scope) {
|
|
return _buildTag(scope.name.capitalize, scope.color, true);
|
|
}
|
|
|
|
Widget _buildStateTag(SystemdUnitState state) {
|
|
return _buildTag(state.name.capitalize, state.color);
|
|
}
|
|
|
|
Widget _buildTypeTag(SystemdUnitType type) {
|
|
return _buildTag(type.name.capitalize);
|
|
}
|
|
|
|
Widget _buildTag(String tag, [Color? color, bool noPad = false]) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: color?.withValues(alpha: 0.7) ?? UIs.halfAlpha,
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
child: Text(tag, style: UIs.text11).paddingSymmetric(horizontal: 5, vertical: 1),
|
|
).paddingOnly(right: noPad ? 0 : 5);
|
|
}
|
|
}
|