783 lines
25 KiB
Dart
783 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:oto_console/oto_console.dart';
|
|
|
|
import 'core_connection_client.dart';
|
|
|
|
const otoDefaultCapabilityPack = OtoCapabilityPack(
|
|
capabilities: [
|
|
OtoCapability(
|
|
name: 'Runner Registry',
|
|
description: 'Track connected build runners and bootstrap status.',
|
|
),
|
|
OtoCapability(
|
|
name: 'Remote Run',
|
|
description: 'Dispatch typed command build and deploy pipelines.',
|
|
),
|
|
OtoCapability(
|
|
name: 'Execution Status',
|
|
description: 'Follow active runs, cancellation, and terminal state.',
|
|
),
|
|
OtoCapability(
|
|
name: 'Step Logs',
|
|
description: 'Inspect live command output and execution traces.',
|
|
),
|
|
OtoCapability(
|
|
name: 'Artifacts',
|
|
description: 'Review build output events and published artifacts.',
|
|
),
|
|
],
|
|
);
|
|
|
|
class OtoClientApp extends StatefulWidget {
|
|
final OtoConsoleConfig config;
|
|
final OtoCoreConnectionSnapshot initialConnection;
|
|
final OtoCoreConnectionClient? coreClient;
|
|
final OtoCoreReadClient? readClient;
|
|
final OtoCoreWriteClient? writeClient;
|
|
final List<String> runnerIDs;
|
|
final List<String> jobIDs;
|
|
final List<String> executionIDs;
|
|
final OtoCapabilityPack capabilities;
|
|
|
|
const OtoClientApp({
|
|
super.key,
|
|
this.config = const OtoConsoleConfig(
|
|
serverHttpUrl: String.fromEnvironment(
|
|
'OTO_SERVER_HTTP_URL',
|
|
defaultValue: 'http://localhost:8080',
|
|
),
|
|
serverWireUrl: String.fromEnvironment(
|
|
'OTO_SERVER_WIRE_URL',
|
|
defaultValue: 'ws://localhost:18080/runner',
|
|
),
|
|
),
|
|
this.initialConnection = const OtoCoreConnectionSnapshot.checking(),
|
|
this.coreClient,
|
|
this.readClient,
|
|
this.writeClient,
|
|
this.runnerIDs = const [],
|
|
this.jobIDs = const [],
|
|
this.executionIDs = const [],
|
|
this.capabilities = otoDefaultCapabilityPack,
|
|
});
|
|
|
|
@override
|
|
State<OtoClientApp> createState() => _OtoClientAppState();
|
|
}
|
|
|
|
class _OtoClientAppState extends State<OtoClientApp> {
|
|
late final OtoCoreConnectionClient _defaultCoreClient =
|
|
OtoHttpCoreConnectionClient();
|
|
late final OtoCoreReadClient _defaultReadClient = OtoHttpCoreReadClient();
|
|
late final OtoCoreWriteClient _defaultWriteClient = OtoHttpCoreWriteClient();
|
|
late OtoCoreConnectionSnapshot _connection = widget.initialConnection;
|
|
OtoSurfaceSnapshot<List<OtoRunnerViewModel>> _runnersSnapshot =
|
|
const OtoSurfaceSnapshot.loading();
|
|
OtoSurfaceSnapshot<List<OtoJobViewModel>> _jobsSnapshot =
|
|
const OtoSurfaceSnapshot.loading();
|
|
OtoSurfaceSnapshot<List<OtoExecutionViewModel>> _executionsSnapshot =
|
|
const OtoSurfaceSnapshot.loading();
|
|
OtoSurfaceSnapshot<List<OtoArtifactViewModel>> _artifactsSnapshot =
|
|
const OtoSurfaceSnapshot.loading();
|
|
|
|
String? _expandedExecutionID;
|
|
OtoSurfaceSnapshot<List<OtoLogEntryViewModel>>? _logsSnapshot;
|
|
OtoSurfaceSnapshot<List<OtoArtifactViewModel>>? _expandedArtifactsSnapshot;
|
|
|
|
OtoConsoleSection _activeSection = OtoConsoleSection.overview;
|
|
bool _showJobCreateForm = false;
|
|
int _refreshGeneration = 0;
|
|
|
|
final Set<String> _createdJobIDs = <String>{};
|
|
OtoActionViewState _jobCreateState = const OtoActionViewState.idle();
|
|
bool _isSubmittingJob = false;
|
|
final Map<String, OtoActionViewState> _executionActionStates =
|
|
<String, OtoActionViewState>{};
|
|
final Map<String, OtoRunnerActionState> _runnerActionStates =
|
|
<String, OtoRunnerActionState>{};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_refreshCoreStatus();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant OtoClientApp oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.config.serverHttpUrl != widget.config.serverHttpUrl ||
|
|
oldWidget.coreClient != widget.coreClient ||
|
|
oldWidget.readClient != widget.readClient ||
|
|
oldWidget.writeClient != widget.writeClient ||
|
|
!_listEquals(oldWidget.runnerIDs, widget.runnerIDs) ||
|
|
!_listEquals(oldWidget.jobIDs, widget.jobIDs) ||
|
|
!_listEquals(oldWidget.executionIDs, widget.executionIDs)) {
|
|
_refreshCoreStatus();
|
|
}
|
|
}
|
|
|
|
Future<void> _submitJobCreate(OtoJobCreateDraft draft) async {
|
|
if (_isSubmittingJob) return;
|
|
|
|
setState(() {
|
|
_isSubmittingJob = true;
|
|
_jobCreateState = const OtoActionViewState.submitting();
|
|
});
|
|
|
|
final writeClient = widget.writeClient ?? _defaultWriteClient;
|
|
final result = await writeClient.createJob(widget.config, draft);
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
_isSubmittingJob = false;
|
|
if (result.isSuccess) {
|
|
_createdJobIDs.add(result.data!.id);
|
|
_jobCreateState = OtoActionViewState.succeeded(message: result.message);
|
|
_refreshCoreStatus();
|
|
} else {
|
|
_jobCreateState = OtoActionViewState.failed(message: result.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _submitRunnerSelfUpdate(OtoRunnerSelfUpdateDraft draft) async {
|
|
setState(() {
|
|
_runnerActionStates[draft.runnerID] =
|
|
const OtoActionViewState.submitting();
|
|
});
|
|
|
|
final writeClient = widget.writeClient ?? _defaultWriteClient;
|
|
final result = await writeClient.selfUpdateRunner(widget.config, draft);
|
|
|
|
if (!mounted) return;
|
|
|
|
final accepted =
|
|
result.state == OtoCoreWriteState.succeeded ||
|
|
result.state == OtoCoreWriteState.deferred;
|
|
|
|
setState(() {
|
|
if (result.state == OtoCoreWriteState.deferred) {
|
|
_runnerActionStates[draft.runnerID] = OtoActionViewState.deferred(
|
|
message: result.message,
|
|
);
|
|
} else if (accepted) {
|
|
_runnerActionStates[draft.runnerID] = OtoActionViewState.succeeded(
|
|
message: result.message,
|
|
);
|
|
} else {
|
|
_runnerActionStates[draft.runnerID] = OtoActionViewState.failed(
|
|
message: result.message,
|
|
);
|
|
}
|
|
});
|
|
|
|
if (accepted) {
|
|
_refreshCoreStatus();
|
|
}
|
|
}
|
|
|
|
Future<void> _submitCancelExecution(OtoExecutionCancelDraft draft) async {
|
|
setState(() {
|
|
_executionActionStates[draft.executionID] =
|
|
const OtoActionViewState.submitting();
|
|
});
|
|
|
|
final writeClient = widget.writeClient ?? _defaultWriteClient;
|
|
final result = await writeClient.cancelExecution(widget.config, draft);
|
|
|
|
if (!mounted) return;
|
|
|
|
_handleExecutionActionResult(draft.executionID, result);
|
|
}
|
|
|
|
Future<void> _submitReportExecution(OtoExecutionReportDraft draft) async {
|
|
setState(() {
|
|
_executionActionStates[draft.executionID] =
|
|
const OtoActionViewState.submitting();
|
|
});
|
|
|
|
final writeClient = widget.writeClient ?? _defaultWriteClient;
|
|
final result = await writeClient.reportExecution(widget.config, draft);
|
|
|
|
if (!mounted) return;
|
|
|
|
_handleExecutionActionResult(draft.executionID, result);
|
|
}
|
|
|
|
Future<void> _submitAppendExecutionLog(OtoExecutionLogDraft draft) async {
|
|
setState(() {
|
|
_executionActionStates[draft.executionID] =
|
|
const OtoActionViewState.submitting();
|
|
});
|
|
|
|
final writeClient = widget.writeClient ?? _defaultWriteClient;
|
|
final result = await writeClient.appendExecutionLog(widget.config, draft);
|
|
|
|
if (!mounted) return;
|
|
|
|
_handleExecutionActionResult(draft.executionID, result);
|
|
}
|
|
|
|
Future<void> _submitAppendExecutionArtifact(
|
|
OtoExecutionArtifactDraft draft,
|
|
) async {
|
|
setState(() {
|
|
_executionActionStates[draft.executionID] =
|
|
const OtoActionViewState.submitting();
|
|
});
|
|
|
|
final writeClient = widget.writeClient ?? _defaultWriteClient;
|
|
final result = await writeClient.appendExecutionArtifact(
|
|
widget.config,
|
|
draft,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
_handleExecutionActionResult(draft.executionID, result);
|
|
}
|
|
|
|
void _handleExecutionActionResult(
|
|
String executionID,
|
|
OtoCoreWriteResult<void> result,
|
|
) {
|
|
if (!mounted) return;
|
|
|
|
// Execution actions return `OtoCoreWriteResult<void>.succeeded(null, ...)` or `.deferred(...)`.
|
|
// `result.isSuccess` requires `data != null`, so check state directly.
|
|
final accepted =
|
|
result.state == OtoCoreWriteState.succeeded ||
|
|
result.state == OtoCoreWriteState.deferred;
|
|
|
|
if (accepted) {
|
|
setState(() {
|
|
_executionActionStates[executionID] = OtoActionViewState.succeeded(
|
|
message: result.message,
|
|
);
|
|
});
|
|
_refreshCoreStatus();
|
|
if (_expandedExecutionID == executionID) {
|
|
_loadExecutionDetail(executionID);
|
|
}
|
|
} else {
|
|
setState(() {
|
|
_executionActionStates[executionID] = OtoActionViewState.failed(
|
|
message: result.message,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
bool _listEquals(List<String>? a, List<String>? b) {
|
|
if (a == b) return true;
|
|
if (a == null || b == null) return false;
|
|
if (a.length != b.length) return false;
|
|
for (int i = 0; i < a.length; i++) {
|
|
if (a[i] != b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Future<void> _refreshCoreStatus() async {
|
|
final generation = ++_refreshGeneration;
|
|
setState(() {
|
|
_connection = const OtoCoreConnectionSnapshot.checking();
|
|
_runnersSnapshot = const OtoSurfaceSnapshot.loading();
|
|
_jobsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
_executionsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
_artifactsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
});
|
|
|
|
final coreClient = widget.coreClient ?? _defaultCoreClient;
|
|
final readClient = widget.readClient ?? _defaultReadClient;
|
|
|
|
final Future<OtoCoreConnectionSnapshot> connectionFuture = coreClient.check(
|
|
widget.config,
|
|
);
|
|
|
|
final Future<OtoSurfaceSnapshot<List<OtoRunnerViewModel>>> runnersFuture =
|
|
_fetchRunners(readClient);
|
|
|
|
final Future<OtoSurfaceSnapshot<List<OtoJobViewModel>>> jobsFuture =
|
|
_fetchJobs(readClient);
|
|
|
|
final Future<OtoSurfaceSnapshot<List<OtoExecutionViewModel>>>
|
|
executionsFuture = _fetchExecutions(readClient);
|
|
|
|
final Future<OtoSurfaceSnapshot<List<OtoArtifactViewModel>>>
|
|
artifactsFuture = _fetchArtifacts(readClient);
|
|
|
|
final results = await Future.wait([
|
|
connectionFuture,
|
|
runnersFuture,
|
|
jobsFuture,
|
|
executionsFuture,
|
|
artifactsFuture,
|
|
]);
|
|
final connection = results[0] as OtoCoreConnectionSnapshot;
|
|
final runnersSnapshot =
|
|
results[1] as OtoSurfaceSnapshot<List<OtoRunnerViewModel>>;
|
|
final jobsSnapshot =
|
|
results[2] as OtoSurfaceSnapshot<List<OtoJobViewModel>>;
|
|
final executionsSnapshot =
|
|
results[3] as OtoSurfaceSnapshot<List<OtoExecutionViewModel>>;
|
|
final artifactsSnapshot =
|
|
results[4] as OtoSurfaceSnapshot<List<OtoArtifactViewModel>>;
|
|
|
|
if (!mounted || generation != _refreshGeneration) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_connection = connection;
|
|
_runnersSnapshot = runnersSnapshot;
|
|
_jobsSnapshot = jobsSnapshot;
|
|
_executionsSnapshot = executionsSnapshot;
|
|
_artifactsSnapshot = artifactsSnapshot;
|
|
});
|
|
|
|
if (_expandedExecutionID != null) {
|
|
_loadExecutionDetail(_expandedExecutionID);
|
|
}
|
|
}
|
|
|
|
Future<OtoSurfaceSnapshot<List<OtoRunnerViewModel>>> _fetchRunners(
|
|
OtoCoreReadClient readClient,
|
|
) async {
|
|
if (widget.runnerIDs.isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
}
|
|
|
|
try {
|
|
final results = await Future.wait(
|
|
widget.runnerIDs.map((id) async {
|
|
final recordResult = await readClient.fetchRunner(widget.config, id);
|
|
final statusResult = await readClient.fetchRunnerStatus(
|
|
widget.config,
|
|
id,
|
|
);
|
|
return _RunnerFetchResult(id, recordResult, statusResult);
|
|
}),
|
|
);
|
|
|
|
final viewModels = <OtoRunnerViewModel>[];
|
|
String? errorMessage;
|
|
bool isLoading = false;
|
|
bool isEmpty = true;
|
|
|
|
for (final result in results) {
|
|
if (result.recordResult.state == OtoCoreReadState.error) {
|
|
errorMessage = result.recordResult.message;
|
|
break;
|
|
}
|
|
if (result.statusResult.state == OtoCoreReadState.error) {
|
|
errorMessage = result.statusResult.message;
|
|
break;
|
|
}
|
|
if (result.recordResult.state == OtoCoreReadState.loading ||
|
|
result.statusResult.state == OtoCoreReadState.loading) {
|
|
isLoading = true;
|
|
break;
|
|
}
|
|
if (result.recordResult.state == OtoCoreReadState.data) {
|
|
isEmpty = false;
|
|
final record = result.recordResult.data!;
|
|
final status = result.statusResult.state == OtoCoreReadState.data
|
|
? result.statusResult.data
|
|
: null;
|
|
viewModels.add(
|
|
OtoRunnerViewModel(
|
|
runnerID: record.runnerID,
|
|
alias: record.alias,
|
|
status: status?.status ?? record.status,
|
|
currentJobID: status?.currentJobID ?? '',
|
|
currentExecutionID: status?.currentExecutionID ?? '',
|
|
message: status?.message ?? record.failureReason,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (errorMessage != null) {
|
|
return OtoSurfaceSnapshot.error(errorMessage);
|
|
} else if (isLoading) {
|
|
return const OtoSurfaceSnapshot.loading();
|
|
} else if (isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
} else {
|
|
return OtoSurfaceSnapshot.data(viewModels);
|
|
}
|
|
} on Exception catch (e) {
|
|
return OtoSurfaceSnapshot.error(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<OtoSurfaceSnapshot<List<OtoJobViewModel>>> _fetchJobs(
|
|
OtoCoreReadClient readClient,
|
|
) async {
|
|
if (widget.jobIDs.isEmpty && _createdJobIDs.isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
}
|
|
|
|
final allJobIDs = <String>{...widget.jobIDs, ..._createdJobIDs};
|
|
|
|
try {
|
|
final results = await Future.wait(
|
|
allJobIDs.map((id) => readClient.fetchJob(widget.config, id)),
|
|
);
|
|
|
|
final viewModels = <OtoJobViewModel>[];
|
|
String? errorMessage;
|
|
bool isLoading = false;
|
|
bool isEmpty = true;
|
|
|
|
for (final result in results) {
|
|
if (result.state == OtoCoreReadState.error) {
|
|
errorMessage = result.message;
|
|
break;
|
|
}
|
|
if (result.state == OtoCoreReadState.loading) {
|
|
isLoading = true;
|
|
break;
|
|
}
|
|
if (result.state == OtoCoreReadState.data) {
|
|
isEmpty = false;
|
|
final record = result.data!;
|
|
viewModels.add(
|
|
OtoJobViewModel(
|
|
jobID: record.id,
|
|
name: record.name,
|
|
state: record.state,
|
|
executionID: record.executionID,
|
|
createdAt: record.createdAt?.toIso8601String() ?? '',
|
|
updatedAt: record.updatedAt?.toIso8601String() ?? '',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (errorMessage != null) {
|
|
return OtoSurfaceSnapshot.error(errorMessage);
|
|
} else if (isLoading) {
|
|
return const OtoSurfaceSnapshot.loading();
|
|
} else if (isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
} else {
|
|
return OtoSurfaceSnapshot.data(viewModels);
|
|
}
|
|
} on Exception catch (e) {
|
|
return OtoSurfaceSnapshot.error(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<OtoSurfaceSnapshot<List<OtoExecutionViewModel>>> _fetchExecutions(
|
|
OtoCoreReadClient readClient,
|
|
) async {
|
|
if (widget.executionIDs.isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
}
|
|
|
|
try {
|
|
// Build executionID -> runnerID map from runner status.
|
|
Map<String, String> runnerIDByExecution = <String, String>{};
|
|
if (widget.runnerIDs.isNotEmpty) {
|
|
final runnerStatusResults = await Future.wait(
|
|
widget.runnerIDs.map(
|
|
(rid) => readClient.fetchRunnerStatus(widget.config, rid),
|
|
),
|
|
);
|
|
for (final statusResult in runnerStatusResults) {
|
|
if (statusResult.state == OtoCoreReadState.data &&
|
|
statusResult.data != null) {
|
|
final status = statusResult.data!;
|
|
if (status.currentExecutionID.isNotEmpty) {
|
|
runnerIDByExecution[status.currentExecutionID] = status.runnerID;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final results = await Future.wait(
|
|
widget.executionIDs.map(
|
|
(id) => readClient.fetchExecution(widget.config, id),
|
|
),
|
|
);
|
|
|
|
final viewModels = <OtoExecutionViewModel>[];
|
|
String? errorMessage;
|
|
bool isLoading = false;
|
|
bool isEmpty = true;
|
|
|
|
for (final result in results) {
|
|
if (result.state == OtoCoreReadState.error) {
|
|
errorMessage = result.message;
|
|
break;
|
|
}
|
|
if (result.state == OtoCoreReadState.loading) {
|
|
isLoading = true;
|
|
break;
|
|
}
|
|
if (result.state == OtoCoreReadState.data) {
|
|
isEmpty = false;
|
|
final record = result.data!;
|
|
viewModels.add(
|
|
OtoExecutionViewModel(
|
|
executionID: record.id,
|
|
jobID: record.jobID,
|
|
state: record.state,
|
|
runnerID: runnerIDByExecution[record.id] ?? '',
|
|
createdAt: record.createdAt?.toIso8601String() ?? '',
|
|
updatedAt: record.updatedAt?.toIso8601String() ?? '',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (errorMessage != null) {
|
|
return OtoSurfaceSnapshot.error(errorMessage);
|
|
} else if (isLoading) {
|
|
return const OtoSurfaceSnapshot.loading();
|
|
} else if (isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
} else {
|
|
return OtoSurfaceSnapshot.data(viewModels);
|
|
}
|
|
} on Exception catch (e) {
|
|
return OtoSurfaceSnapshot.error(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<OtoSurfaceSnapshot<List<OtoArtifactViewModel>>> _fetchArtifacts(
|
|
OtoCoreReadClient readClient,
|
|
) async {
|
|
if (widget.executionIDs.isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
}
|
|
|
|
try {
|
|
final results = await Future.wait(
|
|
widget.executionIDs.map(
|
|
(id) => readClient.fetchArtifacts(widget.config, id),
|
|
),
|
|
);
|
|
|
|
final viewModels = <OtoArtifactViewModel>[];
|
|
String? errorMessage;
|
|
bool isLoading = false;
|
|
bool isEmpty = true;
|
|
|
|
for (final result in results) {
|
|
if (result.state == OtoCoreReadState.error) {
|
|
errorMessage = result.message;
|
|
break;
|
|
}
|
|
if (result.state == OtoCoreReadState.loading) {
|
|
isLoading = true;
|
|
break;
|
|
}
|
|
if (result.state == OtoCoreReadState.data ||
|
|
result.state == OtoCoreReadState.empty) {
|
|
if (result.state == OtoCoreReadState.data) {
|
|
isEmpty = false;
|
|
final records = result.data ?? [];
|
|
for (final record in records) {
|
|
viewModels.add(
|
|
OtoArtifactViewModel(name: record.name, path: record.path),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errorMessage != null) {
|
|
return OtoSurfaceSnapshot.error(errorMessage);
|
|
} else if (isLoading) {
|
|
return const OtoSurfaceSnapshot.loading();
|
|
} else if (isEmpty && viewModels.isEmpty) {
|
|
return const OtoSurfaceSnapshot.empty();
|
|
} else {
|
|
return OtoSurfaceSnapshot.data(viewModels);
|
|
}
|
|
} on Exception catch (e) {
|
|
return OtoSurfaceSnapshot.error(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<void> _loadExecutionDetail(String? execID) async {
|
|
if (execID == null) {
|
|
setState(() {
|
|
_expandedExecutionID = null;
|
|
_logsSnapshot = null;
|
|
_expandedArtifactsSnapshot = null;
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_expandedExecutionID = execID;
|
|
_logsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
_expandedArtifactsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
});
|
|
|
|
final readClient = widget.readClient ?? _defaultReadClient;
|
|
|
|
try {
|
|
final Future<OtoCoreReadResult<List<OtoExecutionLogEntry>>> logsFuture =
|
|
readClient.fetchLogs(widget.config, execID);
|
|
|
|
final Future<OtoCoreReadResult<List<OtoArtifactRecord>>> artifactsFuture =
|
|
readClient.fetchArtifacts(widget.config, execID);
|
|
|
|
final results = await Future.wait([logsFuture, artifactsFuture]);
|
|
final logsResult =
|
|
results[0] as OtoCoreReadResult<List<OtoExecutionLogEntry>>;
|
|
final artifactsResult =
|
|
results[1] as OtoCoreReadResult<List<OtoArtifactRecord>>;
|
|
|
|
if (!mounted || _expandedExecutionID != execID) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
if (logsResult.state == OtoCoreReadState.error) {
|
|
_logsSnapshot = OtoSurfaceSnapshot.error(logsResult.message);
|
|
} else if (logsResult.state == OtoCoreReadState.loading) {
|
|
_logsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
} else if (logsResult.state == OtoCoreReadState.empty ||
|
|
logsResult.data == null) {
|
|
_logsSnapshot = const OtoSurfaceSnapshot.empty();
|
|
} else {
|
|
_logsSnapshot = OtoSurfaceSnapshot.data(
|
|
logsResult.data!
|
|
.map(
|
|
(e) => OtoLogEntryViewModel(
|
|
timestamp: e.timestamp?.toIso8601String() ?? '',
|
|
line: e.line,
|
|
),
|
|
)
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
if (artifactsResult.state == OtoCoreReadState.error) {
|
|
_expandedArtifactsSnapshot = OtoSurfaceSnapshot.error(
|
|
artifactsResult.message,
|
|
);
|
|
} else if (artifactsResult.state == OtoCoreReadState.loading) {
|
|
_expandedArtifactsSnapshot = const OtoSurfaceSnapshot.loading();
|
|
} else if (artifactsResult.state == OtoCoreReadState.empty ||
|
|
artifactsResult.data == null) {
|
|
_expandedArtifactsSnapshot = const OtoSurfaceSnapshot.empty();
|
|
} else {
|
|
_expandedArtifactsSnapshot = OtoSurfaceSnapshot.data(
|
|
artifactsResult.data!
|
|
.map((e) => OtoArtifactViewModel(name: e.name, path: e.path))
|
|
.toList(),
|
|
);
|
|
}
|
|
});
|
|
} on Exception catch (e) {
|
|
if (mounted && _expandedExecutionID == execID) {
|
|
setState(() {
|
|
_logsSnapshot = OtoSurfaceSnapshot.error(e.toString());
|
|
_expandedArtifactsSnapshot = OtoSurfaceSnapshot.error(e.toString());
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const themeAdapter = OtoConsoleThemeAdapter();
|
|
return MaterialApp(
|
|
title: 'OTO Client',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: ThemeData.dark(useMaterial3: true).copyWith(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: themeAdapter.primaryColor,
|
|
brightness: Brightness.dark,
|
|
primary: themeAdapter.primaryColor,
|
|
secondary: themeAdapter.accentColor,
|
|
),
|
|
scaffoldBackgroundColor: themeAdapter.backgroundColor,
|
|
),
|
|
home: OtoConsoleShell(
|
|
config: widget.config,
|
|
capabilities: widget.capabilities,
|
|
themeAdapter: themeAdapter,
|
|
initialSection: _activeSection,
|
|
onNavigate: (section) {
|
|
setState(() {
|
|
_activeSection = section;
|
|
_showJobCreateForm = false;
|
|
});
|
|
},
|
|
onNewItem: () {
|
|
setState(() {
|
|
_activeSection = OtoConsoleSection.pipelines;
|
|
_showJobCreateForm = true;
|
|
});
|
|
},
|
|
overview: OtoConsoleOverview(
|
|
config: widget.config,
|
|
connection: _connection,
|
|
onRefresh: _refreshCoreStatus,
|
|
themeAdapter: themeAdapter,
|
|
),
|
|
runners: OtoRunnersSurface(
|
|
snapshot: _runnersSnapshot,
|
|
actionStates: _runnerActionStates,
|
|
onSelfUpdateRunner: _submitRunnerSelfUpdate,
|
|
themeAdapter: themeAdapter,
|
|
),
|
|
pipelines: OtoJobsSurface(
|
|
snapshot: _jobsSnapshot,
|
|
themeAdapter: themeAdapter,
|
|
jobCreateState: _jobCreateState,
|
|
showCreateForm: _showJobCreateForm,
|
|
createOnly: _showJobCreateForm,
|
|
onCreateJob: _submitJobCreate,
|
|
onExecutionTap: (execID) {
|
|
_loadExecutionDetail(execID);
|
|
setState(() {
|
|
_activeSection = OtoConsoleSection.executions;
|
|
_showJobCreateForm = false;
|
|
});
|
|
},
|
|
),
|
|
executions: OtoExecutionsSurface(
|
|
snapshot: _executionsSnapshot,
|
|
expandedExecutionID: _expandedExecutionID,
|
|
actionStates: _executionActionStates,
|
|
logsSnapshot: _logsSnapshot,
|
|
artifactsSnapshot: _expandedArtifactsSnapshot,
|
|
onExpandExecution: _loadExecutionDetail,
|
|
onViewArtifactsSection: () {
|
|
setState(() {
|
|
_activeSection = OtoConsoleSection.artifacts;
|
|
_showJobCreateForm = false;
|
|
});
|
|
},
|
|
onCancelExecution: _submitCancelExecution,
|
|
onReportExecution: _submitReportExecution,
|
|
onAppendLog: _submitAppendExecutionLog,
|
|
onAppendArtifact: _submitAppendExecutionArtifact,
|
|
themeAdapter: themeAdapter,
|
|
),
|
|
artifacts: OtoArtifactsSurface(
|
|
snapshot: _artifactsSnapshot,
|
|
themeAdapter: themeAdapter,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RunnerFetchResult {
|
|
final String id;
|
|
final OtoCoreReadResult<OtoRunnerRecord> recordResult;
|
|
final OtoCoreReadResult<OtoRunnerStatus> statusResult;
|
|
|
|
_RunnerFetchResult(this.id, this.recordResult, this.statusResult);
|
|
}
|