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
|
|
@ -808,6 +808,25 @@ class HubService {
|
|||
return (name: r.name, version: r.version);
|
||||
}
|
||||
|
||||
/// The shared hub's project registry (`general` first) — the
|
||||
/// lightweight labels grouping flows, runs, approvals and audit
|
||||
/// events. Sealed areas never appear here: they are their own
|
||||
/// hub instance Studio connects to directly (stage 3).
|
||||
Future<List<ProjectRef>> listProjects() async {
|
||||
final projects = await _client.listProjects();
|
||||
return projects
|
||||
.map(
|
||||
(p) => ProjectRef(
|
||||
slug: p.slug,
|
||||
name: p.name,
|
||||
color: p.color,
|
||||
description: p.description,
|
||||
isolation: p.isolation,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Saved flows known to the hub.
|
||||
Future<List<SavedFlow>> listFlows() async {
|
||||
final flows = await _client.listFlows();
|
||||
|
|
@ -818,6 +837,7 @@ class HubService {
|
|||
path: f.path,
|
||||
sizeBytes: f.sizeBytes.toInt(),
|
||||
requiredCapabilities: List<String>.from(f.requiredCapabilities),
|
||||
project: f.project,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
|
|
@ -849,12 +869,14 @@ class HubService {
|
|||
Map<String, String> textInputs = const {},
|
||||
Map<String, Uint8List> fileInputs = const {},
|
||||
Map<String, String> fileMimeTypes = const {},
|
||||
String project = '',
|
||||
}) async {
|
||||
final r = await _client.runSavedFlow(
|
||||
name: name,
|
||||
textInputs: textInputs,
|
||||
fileInputs: fileInputs,
|
||||
fileMimeTypes: fileMimeTypes,
|
||||
project: project,
|
||||
);
|
||||
return {
|
||||
for (final entry in r.outputs.entries)
|
||||
|
|
@ -874,9 +896,10 @@ class HubService {
|
|||
Stream<AuditEvent> streamEvents({
|
||||
int backfill = 0,
|
||||
List<String> types = const [],
|
||||
String project = '',
|
||||
}) {
|
||||
return _client
|
||||
.streamEvents(backfill: backfill, types: types)
|
||||
.streamEvents(backfill: backfill, types: types, project: project)
|
||||
.map(
|
||||
(e) => AuditEvent(
|
||||
eventId: e.eventId,
|
||||
|
|
@ -891,6 +914,7 @@ class HubService {
|
|||
durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(),
|
||||
error: e.error.isEmpty ? null : e.error,
|
||||
detail: e.detail.isEmpty ? null : e.detail,
|
||||
project: e.project,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -898,8 +922,13 @@ class HubService {
|
|||
Future<List<AuditEvent>> recentEvents({
|
||||
int limit = 50,
|
||||
List<String> types = const [],
|
||||
String project = '',
|
||||
}) async {
|
||||
final events = await _client.eventLog(limit: limit, types: types);
|
||||
final events = await _client.eventLog(
|
||||
limit: limit,
|
||||
types: types,
|
||||
project: project,
|
||||
);
|
||||
return events
|
||||
.map(
|
||||
(e) => AuditEvent(
|
||||
|
|
@ -915,13 +944,17 @@ class HubService {
|
|||
durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(),
|
||||
error: e.error.isEmpty ? null : e.error,
|
||||
detail: e.detail.isEmpty ? null : e.detail,
|
||||
project: e.project,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<PendingApproval>> pendingApprovals() async {
|
||||
final entries = await _client.listApprovals(statuses: ['pending']);
|
||||
Future<List<PendingApproval>> pendingApprovals({String project = ''}) async {
|
||||
final entries = await _client.listApprovals(
|
||||
statuses: ['pending'],
|
||||
project: project,
|
||||
);
|
||||
return entries
|
||||
.map(
|
||||
(e) => PendingApproval(
|
||||
|
|
@ -934,6 +967,7 @@ class HubService {
|
|||
expiresAt: e.expiresAt.isEmpty
|
||||
? null
|
||||
: DateTime.tryParse(e.expiresAt),
|
||||
project: e.project,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
@ -946,10 +980,12 @@ class HubService {
|
|||
Future<List<ApprovalRecord>> listApprovalsRecords({
|
||||
List<String> statuses = const [],
|
||||
int limit = 200,
|
||||
String project = '',
|
||||
}) async {
|
||||
final entries = await _client.listApprovals(
|
||||
statuses: statuses,
|
||||
limit: limit,
|
||||
project: project,
|
||||
);
|
||||
return entries
|
||||
.map(
|
||||
|
|
@ -969,6 +1005,7 @@ class HubService {
|
|||
: DateTime.tryParse(e.decidedAt),
|
||||
decidedBy: e.decidedBy,
|
||||
reason: e.reason,
|
||||
project: e.project,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
@ -1231,6 +1268,43 @@ class ModuleSummary {
|
|||
});
|
||||
}
|
||||
|
||||
/// One registered project (shared-hub registry row): the stable
|
||||
/// slug plus renamable presentation metadata. `isolation` is
|
||||
/// `open` (pure label) or `protected` (logically separated — no
|
||||
/// hard process barrier; every UI keeps that honest wording).
|
||||
class ProjectRef {
|
||||
final String slug;
|
||||
final String name;
|
||||
|
||||
/// Accent colour hex (e.g. `#8b7cf6`), empty when unset. Used
|
||||
/// as a marking dot — never as the app theme.
|
||||
final String color;
|
||||
final String description;
|
||||
final String isolation;
|
||||
|
||||
const ProjectRef({
|
||||
required this.slug,
|
||||
required this.name,
|
||||
this.color = '',
|
||||
this.description = '',
|
||||
this.isolation = 'open',
|
||||
});
|
||||
|
||||
bool get isProtected => isolation == 'protected';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ProjectRef &&
|
||||
other.slug == slug &&
|
||||
other.name == name &&
|
||||
other.color == color &&
|
||||
other.description == description &&
|
||||
other.isolation == isolation;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(slug, name, color, description, isolation);
|
||||
}
|
||||
|
||||
class SavedFlow {
|
||||
final String name;
|
||||
final String path;
|
||||
|
|
@ -1242,11 +1316,17 @@ class SavedFlow {
|
|||
/// resolves to an installed module).
|
||||
final List<String> requiredCapabilities;
|
||||
|
||||
/// The flow file's own top-level `project:` slug; empty when
|
||||
/// the YAML has none (a run then lands in the caller's
|
||||
/// project, then `general`).
|
||||
final String project;
|
||||
|
||||
const SavedFlow({
|
||||
required this.name,
|
||||
required this.path,
|
||||
required this.sizeBytes,
|
||||
required this.requiredCapabilities,
|
||||
this.project = '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1335,6 +1415,10 @@ class AuditEvent {
|
|||
/// Surfaced verbatim in the audit drill-down dialog.
|
||||
final String? detail;
|
||||
|
||||
/// Project slug the run was stamped with (bound into the V4
|
||||
/// audit hash). Empty on rows pre-dating the project migration.
|
||||
final String project;
|
||||
|
||||
const AuditEvent({
|
||||
required this.eventId,
|
||||
required this.timestamp,
|
||||
|
|
@ -1348,6 +1432,7 @@ class AuditEvent {
|
|||
this.durationMs,
|
||||
this.error,
|
||||
this.detail,
|
||||
this.project = '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1360,6 +1445,9 @@ class PendingApproval {
|
|||
final DateTime createdAt;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
/// Project slug the requesting run was stamped with.
|
||||
final String project;
|
||||
|
||||
const PendingApproval({
|
||||
required this.id,
|
||||
required this.flowName,
|
||||
|
|
@ -1368,6 +1456,7 @@ class PendingApproval {
|
|||
this.showPreview,
|
||||
required this.createdAt,
|
||||
this.expiresAt,
|
||||
this.project = '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1439,6 +1528,9 @@ class ApprovalRecord {
|
|||
/// Reject reason — empty for approve / pending / expired.
|
||||
final String reason;
|
||||
|
||||
/// Project slug the requesting run was stamped with.
|
||||
final String project;
|
||||
|
||||
const ApprovalRecord({
|
||||
required this.id,
|
||||
required this.flowName,
|
||||
|
|
@ -1451,6 +1543,7 @@ class ApprovalRecord {
|
|||
required this.decidedAt,
|
||||
required this.decidedBy,
|
||||
required this.reason,
|
||||
this.project = '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue