feat: workspace switcher, per-project filters and run stamping (stage 1)
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Multi-project stage 1 against the shared hub (platform design docs/architecture/projects.md, § Studio): - ChainWorkspaceSwitcher in the Audit + Approvals AppBars: lists the registry (colour dot per project, shield for protected, honesty tooltip), 'All projects' stays reachable — a filter, not a jail. Selection is persisted and shared via the Workspace notifier. - Audit page: list query AND live stream re-scoped hub-side on switch. - Approvals page: pending + history scoped; the sidebar badge counts the active workspace's pending approvals. - Flow runs are stamped with the active workspace; a flow file carrying its own project: keeps it (file wins, CLI semantics). - Data layer: listProjects/ProjectRef; project fields on AuditEvent, PendingApproval(+Record), SavedFlow; project params through HubService. l10n DE+EN. Widget tests for the switcher contract. Visual verification (light+dark screenshots) still pending — the shared desktop was in active use; code paths are covered by flutter test (26 green). Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
7a38cd58aa
commit
2592a23cc0
14 changed files with 554 additions and 9 deletions
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/hub.dart';
|
||||
import '../data/workspace.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
|
@ -63,23 +64,30 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
|||
void initState() {
|
||||
super.initState();
|
||||
_tab = TabController(length: 2, vsync: this);
|
||||
Workspace.instance.addListener(_refresh);
|
||||
Workspace.instance.ensureLoaded();
|
||||
_refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Workspace.instance.removeListener(_refresh);
|
||||
_tab.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _refresh() {
|
||||
if (!mounted) return;
|
||||
final project = Workspace.instance.activeSlug;
|
||||
setState(() {
|
||||
_pendingFuture = HubService.instance.listApprovalsRecords(
|
||||
statuses: const ['pending'],
|
||||
project: project,
|
||||
);
|
||||
_historyFuture = HubService.instance.listApprovalsRecords(
|
||||
statuses: const ['approved', 'rejected', 'expired'],
|
||||
limit: 200,
|
||||
project: project,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -256,6 +264,8 @@ class _ApprovalsPageState extends State<ApprovalsPage>
|
|||
],
|
||||
),
|
||||
actions: [
|
||||
const ChainWorkspaceSwitcher(),
|
||||
const SizedBox(width: ChainSpace.md),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline, size: 18),
|
||||
tooltip: AppLocalizations.of(context)!.helpTooltip,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../data/workspace.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
|
@ -38,6 +39,8 @@ class _AuditPageState extends State<AuditPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Workspace.instance.addListener(_onWorkspaceChanged);
|
||||
Workspace.instance.ensureLoaded();
|
||||
_refresh();
|
||||
_poller = Timer.periodic(const Duration(seconds: 2), (_) => _refresh());
|
||||
_subscribeLive();
|
||||
|
|
@ -45,15 +48,26 @@ class _AuditPageState extends State<AuditPage> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
Workspace.instance.removeListener(_onWorkspaceChanged);
|
||||
_poller?.cancel();
|
||||
_nudgeDebounce?.cancel();
|
||||
_eventSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// The workspace filter is applied hub-side, so both the list
|
||||
/// query and the live stream have to be re-established.
|
||||
void _onWorkspaceChanged() {
|
||||
if (!mounted) return;
|
||||
_refresh();
|
||||
_subscribeLive();
|
||||
}
|
||||
|
||||
void _subscribeLive() {
|
||||
_eventSub?.cancel();
|
||||
_eventSub = HubService.instance.streamEvents(backfill: 0).listen(
|
||||
_eventSub = HubService.instance
|
||||
.streamEvents(backfill: 0, project: Workspace.instance.activeSlug)
|
||||
.listen(
|
||||
(_) => _nudge(),
|
||||
// On a persistent error the 2 s poll keeps the page fresh, so we
|
||||
// do nothing. On a clean close (hub restart / stream end) try to
|
||||
|
|
@ -108,7 +122,10 @@ class _AuditPageState extends State<AuditPage> {
|
|||
|
||||
Future<void> _refresh() async {
|
||||
try {
|
||||
final events = await HubService.instance.recentEvents(limit: 100);
|
||||
final events = await HubService.instance.recentEvents(
|
||||
limit: 100,
|
||||
project: Workspace.instance.activeSlug,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_events = events;
|
||||
|
|
@ -136,6 +153,8 @@ class _AuditPageState extends State<AuditPage> {
|
|||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.auditTitle),
|
||||
actions: [
|
||||
const ChainWorkspaceSwitcher(),
|
||||
const SizedBox(width: ChainSpace.md),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: ChainSpace.lg),
|
||||
child: _FilterChips(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue