Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import '../../about/external/data/Kafka.dart';
/// One scan's worth of cluster state.
class KafkaScan {
final List<KafkaTopic> topics;
final List<KafkaConsumerGroup> groups;
final KafkaCluster cluster;
const KafkaScan({
required this.topics,
required this.groups,
required this.cluster,
});
/// Nothing came back at all — usually an unreachable bootstrap server rather
/// than a genuinely empty cluster.
bool get isEmpty =>
topics.isEmpty && groups.isEmpty && cluster.brokers.isEmpty;
}
abstract class ConnectKafka {
/// Whether the Kafka CLI tools were found, and where.
void onKafkaDetected(KafkaStatus status);
/// Detection itself failed (the server couldn't be probed).
void onDetectFailed(String message);
/// Topics, groups and cluster health, freshly read.
void onScanned(KafkaScan scan);
/// The scan failed.
void onScanFailed(String message);
/// Output of the broker probe, to show in a sheet.
void onProbeOutput(String title, String output);
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/cupertino.dart';
import '../../about/internal/application/ServerProfile.dart';
import 'KafkaState.dart';
/// Live Kafka monitoring over SSH: topics with message offsets, consumer
/// groups with lag, and cluster/broker health. Everything comes from the
/// standard Kafka CLI tools run on the server.
class Kafka extends StatefulWidget {
final ServerProfile server;
const Kafka({super.key, required this.server});
@override
State<Kafka> createState() => KafkaState();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
import '../../about/internal/application/ServerProfile.dart';
import '../parent/ParentViewModel.dart';
import 'ConnectKafka.dart';
class ViewKafka extends ParentViewModel {
ConnectKafka connection;
ViewKafka(super.context, this.connection);
/// Looks for the Kafka CLI tools on PATH and in the usual install dirs.
void detectKafka(ServerProfile server) async {
if (!await canReach(server, () => detectKafka(server))) return;
try {
final status = await getDataManager().kafkaDetect(server);
connection.onKafkaDetected(status);
} catch (error) {
connection.onDetectFailed(error.toString());
}
}
/// Reads topics, consumer groups and cluster health in one pass. They're
/// three separate CLI invocations, so this is where they're sequenced.
void scan(
ServerProfile server, {
required String binDir,
required String suffix,
required String bootstrap,
required bool includeInternal,
}) async {
try {
final topics = await getDataManager().kafkaTopics(
server,
binDir: binDir,
suffix: suffix,
bootstrap: bootstrap,
includeInternal: includeInternal,
);
final groups = await getDataManager().kafkaGroups(
server,
binDir: binDir,
suffix: suffix,
bootstrap: bootstrap,
);
final cluster = await getDataManager().kafkaCluster(
server,
binDir: binDir,
suffix: suffix,
bootstrap: bootstrap,
);
connection.onScanned(
KafkaScan(topics: topics, groups: groups, cluster: cluster),
);
} catch (error) {
connection.onScanFailed(error.toString());
}
}
/// Runs `kafka-broker-api-versions` so the user can see exactly what the
/// broker says when a scan comes back empty.
void probeBroker(
ServerProfile server, {
required String binDir,
required String suffix,
required String bootstrap,
}) async {
showLoading("Probing $bootstrap …");
try {
final output = await getDataManager().kafkaTest(
server,
binDir: binDir,
suffix: suffix,
bootstrap: bootstrap,
);
closeLoading();
connection.onProbeOutput(
'kafka-broker-api-versions · $bootstrap',
output.isEmpty ? '(no output — broker unreachable?)' : output,
);
} catch (error) {
handleError(
error,
() => probeBroker(
server,
binDir: binDir,
suffix: suffix,
bootstrap: bootstrap,
),
() {},
"Retry",
);
}
}
}