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>
199 lines
6.3 KiB
Dart
199 lines
6.3 KiB
Dart
// StudioFlowRunDriver — adapter that bridges the editor
|
|
// package's FlowRunDriver interface to Studio's HubService.
|
|
//
|
|
// Lives in Studio (not in the editor package) so the package
|
|
// stays host-agnostic. The driver maps:
|
|
//
|
|
// - editor.runFlow(...) -> HubService.runSavedFlow
|
|
// - editor.events() -> HubService.streamEvents filtered
|
|
// to step.* events and reshaped
|
|
// into the editor's FlowRunEvent
|
|
// value classes.
|
|
|
|
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart' as editor;
|
|
|
|
import 'hub.dart';
|
|
import 'workspace.dart';
|
|
|
|
class StudioFlowRunDriver implements editor.FlowRunDriver {
|
|
StudioFlowRunDriver();
|
|
|
|
@override
|
|
Future<Map<String, editor.FlowOutputValue>> runFlow({
|
|
required String flowName,
|
|
required Map<String, String> textInputs,
|
|
required Map<String, Uint8List> fileInputs,
|
|
required Map<String, String> fileMimes,
|
|
}) async {
|
|
// Stamp the run with the active workspace — but the file wins:
|
|
// a flow carrying its own `project:` keeps it (same semantics
|
|
// as the CLI), so the override is only sent when the flow file
|
|
// has none. Resolution order hub-side is request > file > general.
|
|
var project = Workspace.instance.activeSlug;
|
|
if (project.isNotEmpty) {
|
|
try {
|
|
final flows = await HubService.instance.listFlows();
|
|
for (final f in flows) {
|
|
if (f.name == flowName && f.project.isNotEmpty) {
|
|
project = '';
|
|
break;
|
|
}
|
|
}
|
|
} catch (_) {
|
|
// Lookup failed — keep the workspace label. An unreachable
|
|
// hub fails the run itself a moment later anyway.
|
|
}
|
|
}
|
|
final outputs = await HubService.instance.runSavedFlow(
|
|
name: flowName,
|
|
textInputs: textInputs,
|
|
fileInputs: fileInputs,
|
|
fileMimeTypes: fileMimes,
|
|
project: project,
|
|
);
|
|
final mapped = <String, editor.FlowOutputValue>{};
|
|
for (final entry in outputs.entries) {
|
|
mapped[entry.key] = _convertOutput(entry.value);
|
|
}
|
|
return mapped;
|
|
}
|
|
|
|
@override
|
|
Future<editor.ModuleSpec?> moduleInfo(String capability) async {
|
|
try {
|
|
final detail = await HubService.instance.moduleInfo(capability);
|
|
return editor.ModuleSpec(
|
|
capability: capability,
|
|
inputs: detail.inputs
|
|
.map(
|
|
(f) => editor.ModuleField(
|
|
name: f.name,
|
|
type: f.type,
|
|
description: f.description,
|
|
),
|
|
)
|
|
.toList(),
|
|
outputs: detail.outputs
|
|
.map(
|
|
(f) => editor.ModuleField(
|
|
name: f.name,
|
|
type: f.type,
|
|
description: f.description,
|
|
),
|
|
)
|
|
.toList(),
|
|
);
|
|
} catch (_) {
|
|
// Module not installed locally, or transient hub error.
|
|
// Returning null lets the editor fall back to YAML-
|
|
// derived ports instead of failing the whole canvas.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Stream<editor.FlowRunEvent> events() {
|
|
return HubService.instance
|
|
.streamEvents(
|
|
backfill: 0,
|
|
types: const [
|
|
'step.started',
|
|
'step.completed',
|
|
'step.failed',
|
|
'step.awaiting_approval',
|
|
'step.approved',
|
|
'step.rejected',
|
|
],
|
|
)
|
|
.map(_convertEvent)
|
|
.where((e) => e != null)
|
|
.cast<editor.FlowRunEvent>();
|
|
}
|
|
|
|
editor.FlowRunEvent? _convertEvent(AuditEvent e) {
|
|
final flowName = e.flowName;
|
|
final stepId = e.stepId;
|
|
if (flowName == null || flowName.isEmpty) return null;
|
|
if (stepId == null || stepId.isEmpty) return null;
|
|
switch (e.type) {
|
|
case 'step.started':
|
|
case 'step.approved':
|
|
return editor.StepStarted(flowName: flowName, stepId: stepId);
|
|
case 'step.completed':
|
|
return editor.StepCompleted(
|
|
flowName: flowName,
|
|
stepId: stepId,
|
|
durationMs: e.durationMs ?? 0,
|
|
);
|
|
case 'step.failed':
|
|
case 'step.rejected':
|
|
return editor.StepFailed(
|
|
flowName: flowName,
|
|
stepId: stepId,
|
|
error: e.error ?? '',
|
|
);
|
|
case 'step.awaiting_approval':
|
|
return editor.StepAwaitingApproval(flowName: flowName, stepId: stepId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Future<String?> pendingApprovalIdForStep({
|
|
required String flowName,
|
|
required String stepId,
|
|
}) async {
|
|
try {
|
|
final pending = await HubService.instance.pendingApprovals();
|
|
// Match the newest approval for this (flow, step). The hub
|
|
// creates one approval row per step invocation; if multiple
|
|
// are pending we surface the most recent (operator can still
|
|
// navigate to the standalone Approvals page for the others).
|
|
final match = pending
|
|
.where((p) => p.flowName == flowName && p.stepId == stepId)
|
|
.toList()
|
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
return match.isEmpty ? null : match.first.id;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> approveApproval({
|
|
required String approvalId,
|
|
required String reviewer,
|
|
}) =>
|
|
HubService.instance.approve(approvalId, reviewer);
|
|
|
|
@override
|
|
Future<void> rejectApproval({
|
|
required String approvalId,
|
|
required String reviewer,
|
|
required String reason,
|
|
}) =>
|
|
HubService.instance.reject(approvalId, reviewer, reason);
|
|
|
|
/// Studio's FlowOutput hierarchy is gRPC-shaped (one class
|
|
/// per payload variant); the editor's is host-agnostic
|
|
/// (text / json / bytes only). File outputs degrade to
|
|
/// JSON containing the URI string — the editor's run-tab
|
|
/// renderer is read-only so the operator can still see
|
|
/// where the file went; Studio's richer ChainFlowOutput
|
|
/// widget keeps file-open affordances available elsewhere.
|
|
editor.FlowOutputValue _convertOutput(FlowOutput out) {
|
|
return switch (out) {
|
|
FlowOutputText() => editor.FlowOutputText(out.text),
|
|
FlowOutputJson() => editor.FlowOutputJson(out.pretty),
|
|
FlowOutputBytes() => editor.FlowOutputBytes(out.bytes, out.mimeType),
|
|
FlowOutputFile() => editor.FlowOutputJson({
|
|
'file_uri': out.uri,
|
|
'mime_type': out.mimeType,
|
|
}),
|
|
FlowOutputUnknown() => const editor.FlowOutputText(''),
|
|
};
|
|
}
|
|
}
|