feat(approvals,runs): explain approvals in place + one-click hub update (0.81.0)
Approvals: the pending card now shows its full origin — flow, step, run id (previously dropped at the Dart mapping layer), project, and requested-at — under an ORIGIN heading, led by a one-line intro strip that says what the inbox is and what Approve/Reject do. Approve/Reject buttons carry tooltips; the history dialog gains project + run id. Fixes the approvals doc drift (title/details/reviewer -> prompt/show/timeout_seconds). Guard: approvals_origin_test renders the card via the hermetic fake hub and pins every origin fact. Runs: the "hub too old" state now leads with an in-place update button (same `chain update apply` path as the Diagnose page), the Diagnose deeplink demoted to secondary, with a CLI-absent fallback. Guard: two new RunsLoadErrorView widget tests. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
14f824b8ef
commit
28f6fe1a9a
16 changed files with 613 additions and 115 deletions
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/// Studio's own build version. Bump on every UI release so the
|
||||
/// running app self-identifies.
|
||||
const String kStudioVersion = '0.80.0';
|
||||
const String kStudioVersion = '0.81.0';
|
||||
|
||||
const String kProductName = 'Ch∆In Studio';
|
||||
const String kVendorName = 'Flemming.AI (F∆I)';
|
||||
|
|
|
|||
|
|
@ -79,34 +79,42 @@ class HubService {
|
|||
/// null when it cannot be determined (then we keep the default).
|
||||
static ({String channel, HubEndpoint endpoint})? _discoverActiveChannel() {
|
||||
try {
|
||||
final home = Platform.environment['HOME'] ??
|
||||
Platform.environment['USERPROFILE'];
|
||||
final home =
|
||||
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
|
||||
if (home == null || home.isEmpty) return null;
|
||||
final sep = Platform.pathSeparator;
|
||||
final base = '$home$sep.chain';
|
||||
final ccFile = File('$base${sep}current-channel');
|
||||
final channel =
|
||||
ccFile.existsSync() ? ccFile.readAsStringSync().trim() : 'local';
|
||||
final channel = ccFile.existsSync()
|
||||
? ccFile.readAsStringSync().trim()
|
||||
: 'local';
|
||||
if (channel.isEmpty) return null;
|
||||
// Prefer the actual bound endpoint the daemon wrote.
|
||||
final epFile = File('$base${sep}run$sep$channel.endpoint');
|
||||
if (epFile.existsSync()) {
|
||||
final raw =
|
||||
epFile.readAsStringSync().trim().replaceFirst(RegExp(r'^\w+://'), '');
|
||||
final raw = epFile.readAsStringSync().trim().replaceFirst(
|
||||
RegExp(r'^\w+://'),
|
||||
'',
|
||||
);
|
||||
final i = raw.lastIndexOf(':');
|
||||
if (i > 0) {
|
||||
final host = raw.substring(0, i);
|
||||
final port = int.tryParse(raw.substring(i + 1));
|
||||
if (port != null) {
|
||||
return (channel: channel, endpoint: HubEndpoint(host: host, port: port));
|
||||
return (
|
||||
channel: channel,
|
||||
endpoint: HubEndpoint(host: host, port: port),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to the channel's well-known default port.
|
||||
return (
|
||||
channel: channel,
|
||||
endpoint:
|
||||
HubEndpoint(host: '127.0.0.1', port: _channelPorts[channel] ?? 50051),
|
||||
endpoint: HubEndpoint(
|
||||
host: '127.0.0.1',
|
||||
port: _channelPorts[channel] ?? 50051,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
|
@ -332,13 +340,12 @@ class HubService {
|
|||
required String url,
|
||||
String bearerEnv = '',
|
||||
String pinnedPubkeyPem = '',
|
||||
}) =>
|
||||
_client.addStore(
|
||||
name: name,
|
||||
url: url,
|
||||
bearerEnv: bearerEnv,
|
||||
pinnedPubkeyPem: pinnedPubkeyPem,
|
||||
);
|
||||
}) => _client.addStore(
|
||||
name: name,
|
||||
url: url,
|
||||
bearerEnv: bearerEnv,
|
||||
pinnedPubkeyPem: pinnedPubkeyPem,
|
||||
);
|
||||
|
||||
/// Drop a configured store by name (the bundled seed cannot be removed).
|
||||
Future<RemoveStoreResponse> removeStore(String name) =>
|
||||
|
|
@ -1156,6 +1163,7 @@ class HubService {
|
|||
decidedBy: e.decidedBy,
|
||||
reason: e.reason,
|
||||
project: e.project,
|
||||
flowExecution: e.flowExecution.isEmpty ? null : e.flowExecution,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
@ -1184,8 +1192,7 @@ class HubService {
|
|||
|
||||
/// Cancel a running/pending detached invocation. Returns true when
|
||||
/// it was signalled, false if already finished or unknown.
|
||||
Future<bool> cancelDetachedRun(String id) =>
|
||||
_client.cancelInvocation(id);
|
||||
Future<bool> cancelDetachedRun(String id) => _client.cancelInvocation(id);
|
||||
|
||||
/// Federation satellites currently connected to this hub
|
||||
/// (primary side). Empty when none are connected.
|
||||
|
|
@ -1241,9 +1248,7 @@ class HubService {
|
|||
/// error — the hint simply stays away then.
|
||||
Future<UpdateStatus?> checkHubUpdate() async {
|
||||
try {
|
||||
final r = await _client.checkUpdate().timeout(
|
||||
const Duration(seconds: 3),
|
||||
);
|
||||
final r = await _client.checkUpdate().timeout(const Duration(seconds: 3));
|
||||
if (!r.updateAvailable) return null;
|
||||
return UpdateStatus(
|
||||
channel: r.channel,
|
||||
|
|
@ -1264,9 +1269,9 @@ class HubService {
|
|||
Future<String> hubVersion() async {
|
||||
try {
|
||||
final r = await _client.checkUpdate().timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () => CheckUpdateResponse(),
|
||||
);
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () => CheckUpdateResponse(),
|
||||
);
|
||||
return r.localVersion;
|
||||
} catch (_) {
|
||||
return '';
|
||||
|
|
@ -1326,8 +1331,7 @@ class HubService {
|
|||
eventChainTamperedAt: chain.tamperedAt.isEmpty ? null : chain.tamperedAt,
|
||||
services: services
|
||||
.map(
|
||||
(s) =>
|
||||
ServiceEntry(
|
||||
(s) => ServiceEntry(
|
||||
name: s.name,
|
||||
endpoint: s.endpoint,
|
||||
tags: s.tags,
|
||||
|
|
@ -1775,6 +1779,11 @@ class ApprovalRecord {
|
|||
/// Project slug the requesting run was stamped with.
|
||||
final String project;
|
||||
|
||||
/// The flow-execution (run) id this approval belongs to. Ties the
|
||||
/// pause back to the concrete run and its event timeline. Null on
|
||||
/// pre-0.21 hubs that did not carry the field over the wire.
|
||||
final String? flowExecution;
|
||||
|
||||
const ApprovalRecord({
|
||||
required this.id,
|
||||
required this.flowName,
|
||||
|
|
@ -1788,6 +1797,7 @@ class ApprovalRecord {
|
|||
required this.decidedBy,
|
||||
required this.reason,
|
||||
this.project = '',
|
||||
this.flowExecution,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue