feat(studio): drop Modules tab, fold its content into Store detail (v0.35.0)
Modules-as-a-tab was redundant with the Store after the
"Installed" filter and the federation work landed. Two pieces
of unique content kept it alive: the per-module declared
permissions list and the on-disk module directory. Both now
live inside the Store detail sheet.
Changes:
- Sidebar nav loses the Modules entry. `_pages` no longer
carries a `'modules'` slot. The unused `import
'pages/modules.dart'` is dropped from main.dart. Smoke test
updated to skip the Modules-text expectation.
- Store detail sheet (`_StoreDetailSheet`) gains two new
sections, rendered only when the entry is installed and the
hub returned `ModuleDetail` for it:
Declared permissions → same icon-prefixed list the
old Modules sheet shipped
(`net:`, `fs.read:`, `fs.write:`,
`env:`, `hub:`).
Module directory → selectable mono path so the
operator can paste it into a
shell.
An async `moduleInfo` fetch fires from `initState` only when
`widget.item.installed` is true, so the regular
not-installed detail-sheet path takes no extra round-trip.
Failures stay silent — the sections just hide.
- The `FaiModuleSheet` widget stays intact. Cmd+K still uses
it as a quick-info modal for installed modules; the
longer-form Store detail sheet covers the same data plus
the description, screenshots, and docs that operators
reach for in the Store.
Three new ARB keys: `storeSectionPermissions`,
`storeSectionDirectory`, `storeSectionPermissionsNone`.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
3b5fb027c3
commit
a7e2eb101a
9 changed files with 149 additions and 10 deletions
|
|
@ -194,6 +194,9 @@
|
||||||
"storeSectionScreenshots": "Screenshots",
|
"storeSectionScreenshots": "Screenshots",
|
||||||
"storeSectionDocumentation": "Dokumentation",
|
"storeSectionDocumentation": "Dokumentation",
|
||||||
"storeSectionSource": "Quelle",
|
"storeSectionSource": "Quelle",
|
||||||
|
"storeSectionPermissions": "Deklarierte Berechtigungen",
|
||||||
|
"storeSectionDirectory": "Modul-Verzeichnis",
|
||||||
|
"storeSectionPermissionsNone": "(keine — reines Berechnungsmodul)",
|
||||||
"storeInstallingButton": "Installiere…",
|
"storeInstallingButton": "Installiere…",
|
||||||
"storeNotInstallable": "Nicht installierbar",
|
"storeNotInstallable": "Nicht installierbar",
|
||||||
"storeNotInstallableTooltip": "Status „{status}\" — Installations-Pfad noch nicht verdrahtet.",
|
"storeNotInstallableTooltip": "Status „{status}\" — Installations-Pfad noch nicht verdrahtet.",
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,9 @@
|
||||||
"storeSectionScreenshots": "Screenshots",
|
"storeSectionScreenshots": "Screenshots",
|
||||||
"storeSectionDocumentation": "Documentation",
|
"storeSectionDocumentation": "Documentation",
|
||||||
"storeSectionSource": "Source",
|
"storeSectionSource": "Source",
|
||||||
|
"storeSectionPermissions": "Declared permissions",
|
||||||
|
"storeSectionDirectory": "Module directory",
|
||||||
|
"storeSectionPermissionsNone": "(none — pure-computation module)",
|
||||||
"storeInstallingButton": "Installing…",
|
"storeInstallingButton": "Installing…",
|
||||||
"storeNotInstallable": "Not installable",
|
"storeNotInstallable": "Not installable",
|
||||||
"storeNotInstallableTooltip": "Status \"{status}\" — install path not yet wired.",
|
"storeNotInstallableTooltip": "Status \"{status}\" — install path not yet wired.",
|
||||||
|
|
|
||||||
|
|
@ -1004,6 +1004,24 @@ abstract class AppLocalizations {
|
||||||
/// **'Source'**
|
/// **'Source'**
|
||||||
String get storeSectionSource;
|
String get storeSectionSource;
|
||||||
|
|
||||||
|
/// No description provided for @storeSectionPermissions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Declared permissions'**
|
||||||
|
String get storeSectionPermissions;
|
||||||
|
|
||||||
|
/// No description provided for @storeSectionDirectory.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Module directory'**
|
||||||
|
String get storeSectionDirectory;
|
||||||
|
|
||||||
|
/// No description provided for @storeSectionPermissionsNone.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'(none — pure-computation module)'**
|
||||||
|
String get storeSectionPermissionsNone;
|
||||||
|
|
||||||
/// No description provided for @storeInstallingButton.
|
/// No description provided for @storeInstallingButton.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -522,6 +522,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get storeSectionSource => 'Quelle';
|
String get storeSectionSource => 'Quelle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeSectionPermissions => 'Deklarierte Berechtigungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeSectionDirectory => 'Modul-Verzeichnis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeSectionPermissionsNone => '(keine — reines Berechnungsmodul)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeInstallingButton => 'Installiere…';
|
String get storeInstallingButton => 'Installiere…';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -540,6 +540,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get storeSectionSource => 'Source';
|
String get storeSectionSource => 'Source';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeSectionPermissions => 'Declared permissions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeSectionDirectory => 'Module directory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeSectionPermissionsNone => '(none — pure-computation module)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeInstallingButton => 'Installing…';
|
String get storeInstallingButton => 'Installing…';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import 'pages/approvals.dart';
|
||||||
import 'pages/audit.dart';
|
import 'pages/audit.dart';
|
||||||
import 'pages/doctor.dart';
|
import 'pages/doctor.dart';
|
||||||
import 'pages/flows.dart';
|
import 'pages/flows.dart';
|
||||||
import 'pages/modules.dart';
|
|
||||||
import 'pages/store.dart';
|
import 'pages/store.dart';
|
||||||
import 'theme/theme.dart';
|
import 'theme/theme.dart';
|
||||||
import 'theme/tokens.dart';
|
import 'theme/tokens.dart';
|
||||||
|
|
@ -26,7 +25,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.34.0';
|
const String kStudioVersion = '0.35.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -146,12 +145,11 @@ class _StudioShellState extends State<StudioShell> {
|
||||||
selectedIcon: Icons.health_and_safety,
|
selectedIcon: Icons.health_and_safety,
|
||||||
page: DoctorPage(),
|
page: DoctorPage(),
|
||||||
),
|
),
|
||||||
_NavPage(
|
// Modules tab dropped in v0.35.0 — installed-module info
|
||||||
id: 'modules',
|
// (permissions, on-disk path, uninstall) now lives inside
|
||||||
icon: Icons.extension_outlined,
|
// the Store's detail sheet, and the Store with the
|
||||||
selectedIcon: Icons.extension,
|
// "Installed" filter covers the listing role. Cmd+K still
|
||||||
page: ModulesPage(),
|
// opens FaiModuleSheet for quick info.
|
||||||
),
|
|
||||||
_NavPage(
|
_NavPage(
|
||||||
id: 'store',
|
id: 'store',
|
||||||
icon: Icons.storefront_outlined,
|
icon: Icons.storefront_outlined,
|
||||||
|
|
|
||||||
|
|
@ -651,6 +651,21 @@ String _categoryDisplayName(BuildContext ctx, String wire) {
|
||||||
/// Same idea for the `published` / `alpha` / `planned` status
|
/// Same idea for the `published` / `alpha` / `planned` status
|
||||||
/// values. Operators read "stable / experimental / coming
|
/// values. Operators read "stable / experimental / coming
|
||||||
/// soon"; the wire keeps the strict enum it has always had.
|
/// soon"; the wire keeps the strict enum it has always had.
|
||||||
|
/// Map a permission string (`net:api.example.com`,
|
||||||
|
/// `fs.read:/etc/fai`, `env:OPENAI_API_KEY`, …) to a glyph the
|
||||||
|
/// operator can read at a glance. Mirrors the table that used
|
||||||
|
/// to live in `fai_module_sheet.dart` so the Store detail-sheet
|
||||||
|
/// can render permissions in the same shape after the Modules
|
||||||
|
/// page was dropped.
|
||||||
|
IconData _iconForPermission(String permission) {
|
||||||
|
if (permission.startsWith('net:')) return Icons.public;
|
||||||
|
if (permission.startsWith('fs.read:')) return Icons.folder_open;
|
||||||
|
if (permission.startsWith('fs.write:')) return Icons.edit_note;
|
||||||
|
if (permission.startsWith('env:')) return Icons.terminal;
|
||||||
|
if (permission.startsWith('hub:')) return Icons.shield_outlined;
|
||||||
|
return Icons.lock_outline;
|
||||||
|
}
|
||||||
|
|
||||||
String _statusDisplayName(BuildContext ctx, String wire) {
|
String _statusDisplayName(BuildContext ctx, String wire) {
|
||||||
final l = AppLocalizations.of(ctx)!;
|
final l = AppLocalizations.of(ctx)!;
|
||||||
switch (wire) {
|
switch (wire) {
|
||||||
|
|
@ -1796,6 +1811,31 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
String? _toast;
|
String? _toast;
|
||||||
Future<({String errorKind, String text, String sourceUrl})>? _docsFuture;
|
Future<({String errorKind, String text, String sourceUrl})>? _docsFuture;
|
||||||
bool _docsLoaded = false;
|
bool _docsLoaded = false;
|
||||||
|
/// Live module-info for installed entries: declared
|
||||||
|
/// permissions and on-disk directory. Pulled lazily so
|
||||||
|
/// not-installed entries don't run an unnecessary RPC. Stays
|
||||||
|
/// null when the fetch fails — the corresponding sections
|
||||||
|
/// just hide rather than show a spinner inside a hover sheet.
|
||||||
|
ModuleDetail? _moduleDetail;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.item.installed) {
|
||||||
|
_loadModuleDetail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadModuleDetail() async {
|
||||||
|
try {
|
||||||
|
final detail =
|
||||||
|
await HubService.instance.moduleInfo(widget.item.name);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _moduleDetail = detail);
|
||||||
|
} catch (_) {
|
||||||
|
// Silent — sections stay hidden, the sheet still works.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _install() async {
|
Future<void> _install() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -2058,6 +2098,64 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
|
// Live-from-hub sections for installed
|
||||||
|
// modules. Folded in here in v0.35.0 when the
|
||||||
|
// standalone Modules page got dropped — its
|
||||||
|
// unique value (declared permissions, on-disk
|
||||||
|
// directory) belongs in the same surface
|
||||||
|
// operators already use for everything else
|
||||||
|
// about a module.
|
||||||
|
if (item.installed && _moduleDetail != null) ...[
|
||||||
|
_SectionHeader(l.storeSectionPermissions),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
if (_moduleDetail!.permissions.isEmpty)
|
||||||
|
Text(
|
||||||
|
l.storeSectionPermissionsNone,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (final p in _moduleDetail!.permissions)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_iconForPermission(p),
|
||||||
|
size: 14,
|
||||||
|
color:
|
||||||
|
theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
SelectableText(
|
||||||
|
p,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 12,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
_SectionHeader(l.storeSectionDirectory),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
SelectableText(
|
||||||
|
_moduleDetail!.directory,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 11,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
],
|
||||||
if (item.screenshotUrls.isNotEmpty) ...[
|
if (item.screenshotUrls.isNotEmpty) ...[
|
||||||
_SectionHeader(l.storeSectionScreenshots),
|
_SectionHeader(l.storeSectionScreenshots),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.34.0
|
version: 0.35.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ void main() {
|
||||||
|
|
||||||
expect(find.text('F∆I Studio'), findsOneWidget);
|
expect(find.text('F∆I Studio'), findsOneWidget);
|
||||||
expect(find.text('Doctor'), findsWidgets);
|
expect(find.text('Doctor'), findsWidgets);
|
||||||
expect(find.text('Modules'), findsWidgets);
|
// Modules tab dropped in v0.35.0 — info now lives inside
|
||||||
|
// the Store detail sheet.
|
||||||
expect(find.text('Store'), findsOneWidget);
|
expect(find.text('Store'), findsOneWidget);
|
||||||
expect(find.text('Flows'), findsOneWidget);
|
expect(find.text('Flows'), findsOneWidget);
|
||||||
expect(find.text('Audit'), findsOneWidget);
|
expect(find.text('Audit'), findsOneWidget);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue