Selected details

This commit is contained in:
Garth Vander Houwen 2024-08-02 19:15:26 -07:00
parent 9cd833b7fe
commit cbcd452b4b
2 changed files with 313 additions and 312 deletions

View file

@ -33,373 +33,371 @@ struct NodeDetail: View {
var columnVisibility = NavigationSplitViewVisibility.all
var body: some View {
NavigationStack {
List {
let connectedNode = getNodeInfo(
id: bleManager.connectedPeripheral?.num ?? -1,
context: context
)
List {
let connectedNode = getNodeInfo(
id: bleManager.connectedPeripheral?.num ?? -1,
context: context
)
Section("Hardware") {
NodeInfoItem(node: node)
Section("Hardware") {
NodeInfoItem(node: node)
}
Section("Node") {
HStack {
Label {
Text("Node Number")
} icon: {
Image(systemName: "number")
.symbolRenderingMode(.hierarchical)
}
Spacer()
Text(String(node.num))
.textSelection(.enabled)
}
Section("Node") {
HStack {
Label {
Text("Node Number")
} icon: {
Image(systemName: "number")
.symbolRenderingMode(.hierarchical)
}
Spacer()
Text(String(node.num))
.textSelection(.enabled)
HStack {
Label {
Text("User Id")
} icon: {
Image(systemName: "person")
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(node.user?.userId ?? "?")
.textSelection(.enabled)
}
if let metadata = node.metadata {
HStack {
Label {
Text("User Id")
Text("firmware.version")
} icon: {
Image(systemName: "person")
Image(systemName: "memorychip")
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(node.user?.userId ?? "?")
Text(metadata.firmwareVersion ?? "unknown".localized)
}
}
if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) {
HStack {
Label {
Text("Role")
} icon: {
Image(systemName: deviceRole.systemName)
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(deviceRole.name)
}
}
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, dm.uptimeSeconds > 0 {
HStack {
Label {
Text("\("uptime".localized)")
} icon: {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.symbolRenderingMode(.hierarchical)
}
Spacer()
let now = Date.now
let later = now + TimeInterval(dm.uptimeSeconds)
let uptime = (now..<later).formatted(.components(style: .narrow))
Text(uptime)
.textSelection(.enabled)
}
}
if let metadata = node.metadata {
HStack {
Label {
Text("firmware.version")
} icon: {
Image(systemName: "memorychip")
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(metadata.firmwareVersion ?? "unknown".localized)
if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 {
HStack {
Label {
Text("First heard")
} icon: {
Image(systemName: "clock")
.symbolRenderingMode(.multicolor)
}
}
if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) {
HStack {
Label {
Text("Role")
} icon: {
Image(systemName: deviceRole.systemName)
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(deviceRole.name)
}
}
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, dm.uptimeSeconds > 0 {
HStack {
Label {
Text("\("uptime".localized)")
} icon: {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.symbolRenderingMode(.hierarchical)
}
Spacer()
let now = Date.now
let later = now + TimeInterval(dm.uptimeSeconds)
let uptime = (now..<later).formatted(.components(style: .narrow))
Text(uptime)
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: firstHeard) {
Text(text)
.textSelection(.enabled)
} else {
Text(firstHeard.formatted())
.textSelection(.enabled)
}
}
if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 {
HStack {
Label {
Text("First heard")
} icon: {
Image(systemName: "clock")
.symbolRenderingMode(.multicolor)
}
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: firstHeard) {
Text(text)
.textSelection(.enabled)
} else {
Text(firstHeard.formatted())
.textSelection(.enabled)
}
}.onTapGesture {
dateFormatRelative.toggle()
}
}
if let lastHeard = node.lastHeard, lastHeard.timeIntervalSince1970 > 0 {
HStack {
Label {
Text("Last heard")
} icon: {
Image(systemName: "clock.arrow.circlepath")
.symbolRenderingMode(.multicolor)
}
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) {
Text(text)
.textSelection(.enabled)
} else {
Text(lastHeard.formatted())
.textSelection(.enabled)
}
}.onTapGesture {
dateFormatRelative.toggle()
}
}.onTapGesture {
dateFormatRelative.toggle()
}
}
if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasEnvironmentMetrics {
Section("Environment") {
if !node.hasEnvironmentMetrics {
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
if let lastHeard = node.lastHeard, lastHeard.timeIntervalSince1970 > 0 {
HStack {
Label {
Text("Last heard")
} icon: {
Image(systemName: "clock.arrow.circlepath")
.symbolRenderingMode(.multicolor)
}
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) {
Text(text)
.textSelection(.enabled)
} else {
VStack {
if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 {
IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient)
.padding(.vertical)
}
LazyVGrid(columns: gridItemLayout) {
WeatherConditionsCompactWidget(temperature: String(node.latestEnvironmentMetrics?.temperature.shortFormattedTemperature() ?? "99°"), symbolName: "cloud.sun", description: "TEMP")
if node.latestEnvironmentMetrics?.relativeHumidity ?? 0.0 > 0.0 {
HumidityCompactWidget(humidity: Int(node.latestEnvironmentMetrics?.relativeHumidity ?? 0.0), dewPoint: String(format: "%.0f", calculateDewPoint(temp: node.latestEnvironmentMetrics?.temperature ?? 0.0, relativeHumidity: node.latestEnvironmentMetrics?.relativeHumidity ?? 0.0)) + "°")
}
if node.latestEnvironmentMetrics?.barometricPressure ?? 0.0 > 0.0 {
PressureCompactWidget(pressure: String(format: "%.2f", node.latestEnvironmentMetrics?.barometricPressure ?? 0.0), unit: "hPA", low: node.latestEnvironmentMetrics?.barometricPressure ?? 0.0 <= 1009.144)
}
if node.latestEnvironmentMetrics?.windSpeed ?? 0.0 > 0.0 {
WindCompactWidget(speed: String(node.latestEnvironmentMetrics?.windSpeed ?? 0.0), gust: String(node.latestEnvironmentMetrics?.windGust ?? 0.0), direction: "")
}
}
.padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical)
Text(lastHeard.formatted())
.textSelection(.enabled)
}
}.onTapGesture {
dateFormatRelative.toggle()
}
}
}
if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasEnvironmentMetrics {
Section("Environment") {
if !node.hasEnvironmentMetrics {
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
} else {
VStack {
if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 {
IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient)
.padding(.vertical)
}
LazyVGrid(columns: gridItemLayout) {
WeatherConditionsCompactWidget(temperature: String(node.latestEnvironmentMetrics?.temperature.shortFormattedTemperature() ?? "99°"), symbolName: "cloud.sun", description: "TEMP")
if node.latestEnvironmentMetrics?.relativeHumidity ?? 0.0 > 0.0 {
HumidityCompactWidget(humidity: Int(node.latestEnvironmentMetrics?.relativeHumidity ?? 0.0), dewPoint: String(format: "%.0f", calculateDewPoint(temp: node.latestEnvironmentMetrics?.temperature ?? 0.0, relativeHumidity: node.latestEnvironmentMetrics?.relativeHumidity ?? 0.0)) + "°")
}
if node.latestEnvironmentMetrics?.barometricPressure ?? 0.0 > 0.0 {
PressureCompactWidget(pressure: String(format: "%.2f", node.latestEnvironmentMetrics?.barometricPressure ?? 0.0), unit: "hPA", low: node.latestEnvironmentMetrics?.barometricPressure ?? 0.0 <= 1009.144)
}
if node.latestEnvironmentMetrics?.windSpeed ?? 0.0 > 0.0 {
WindCompactWidget(speed: String(node.latestEnvironmentMetrics?.windSpeed ?? 0.0), gust: String(node.latestEnvironmentMetrics?.windGust ?? 0.0), direction: "")
}
}
.padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical)
}
}
}
Section("Logs") {
ForEach(NodeDetails.allCases) { detail in
// List( selection: $selectedDetails) { detail in
switch detail {
case .deviceMetricsLog:
NavigationLink {
DeviceMetricsLog(node: node)
} label: {
Label {
Text("Device Metrics Log")
} icon: {
Image(systemName: "flipphone")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasDeviceMetrics)
case .nodeMap:
NavigationLink {
if #available (iOS 17, macOS 14, *) {
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
} else {
NodeMapMapkit(node: node)
}
} label: {
Label {
Text("Node Map")
} icon: {
Image(systemName: "map")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasPositions)
case .positionLog:
NavigationLink {
PositionLog(node: node)
} label: {
Label {
Text("Position Log")
} icon: {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasPositions)
case .environmentMetricsLog:
NavigationLink {
EnvironmentMetricsLog(node: node)
} label: {
Label {
Text("Environment Metrics Log")
} icon: {
Image(systemName: "cloud.sun.rain")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasEnvironmentMetrics)
case .traceRouteLog:
if #available(iOS 17.0, macOS 14.0, *) {
NavigationLink {
TraceRouteLog(node: node)
} label: {
Label {
Text("Trace Route Log")
} icon: {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.multicolor)
}
}
.disabled(node.traceRoutes?.count ?? 0 == 0)
}
case .detectionSensorLog:
NavigationLink {
DetectionSensorLog(node: node)
} label: {
Label {
Text("Detection Sensor Log")
} icon: {
Image(systemName: "sensor")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasDetectionSensorMetrics)
case .paxCounterLog:
if node.hasPax {
NavigationLink {
PaxCounterLog(node: node)
} label: {
Label {
Text("paxcounter.log")
} icon: {
Image(systemName: "figure.walk.motion")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasPax)
}
Section("Logs") {
ForEach(NodeDetails.allCases) { detail in
// List( selection: $selectedDetails) { detail in
switch detail {
case .deviceMetricsLog:
NavigationLink {
DeviceMetricsLog(node: node)
} label: {
Label {
Text("Device Metrics Log")
} icon: {
Image(systemName: "flipphone")
.symbolRenderingMode(.multicolor)
}
}
}
.disabled(!node.hasDeviceMetrics)
case .nodeMap:
NavigationLink {
if #available (iOS 17, macOS 14, *) {
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
} else {
NodeMapMapkit(node: node)
}
} label: {
Label {
Text("Node Map")
} icon: {
Image(systemName: "map")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasPositions)
case .positionLog:
NavigationLink {
PositionLog(node: node)
} label: {
Label {
Text("Position Log")
} icon: {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasPositions)
case .environmentMetricsLog:
NavigationLink {
EnvironmentMetricsLog(node: node)
} label: {
Label {
Text("Environment Metrics Log")
} icon: {
Image(systemName: "cloud.sun.rain")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasEnvironmentMetrics)
case .traceRouteLog:
if #available(iOS 17.0, macOS 14.0, *) {
NavigationLink {
TraceRouteLog(node: node)
} label: {
Label {
Text("Trace Route Log")
} icon: {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.multicolor)
}
}
.disabled(node.traceRoutes?.count ?? 0 == 0)
}
case .detectionSensorLog:
NavigationLink {
DetectionSensorLog(node: node)
} label: {
Label {
Text("Detection Sensor Log")
} icon: {
Image(systemName: "sensor")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasDetectionSensorMetrics)
case .paxCounterLog:
if node.hasPax {
NavigationLink {
PaxCounterLog(node: node)
} label: {
Label {
Text("paxcounter.log")
} icon: {
Image(systemName: "figure.walk.motion")
.symbolRenderingMode(.multicolor)
}
}
.disabled(!node.hasPax)
}
}
}
}
Section("Actions") {
FavoriteNodeButton(
bleManager: bleManager,
context: context,
node: node
)
if let user = node.user {
NodeAlertsButton(
context: context,
node: node,
user: user
)
}
Section("Actions") {
FavoriteNodeButton(
if let connectedPeripheral = bleManager.connectedPeripheral,
node.num != connectedPeripheral.num {
ExchangePositionsButton(
bleManager: bleManager,
context: context,
node: node
)
if let user = node.user {
NodeAlertsButton(
context: context,
node: node,
user: user
)
}
TraceRouteButton(
bleManager: bleManager,
node: node
)
if let connectedPeripheral = bleManager.connectedPeripheral,
node.num != connectedPeripheral.num {
ExchangePositionsButton(
bleManager: bleManager,
node: node
)
TraceRouteButton(
bleManager: bleManager,
node: node
)
if let connectedNode {
if node.isStoreForwardRouter {
ClientHistoryButton(
bleManager: bleManager,
connectedNode: connectedNode,
node: node
)
}
DeleteNodeButton(
if let connectedNode {
if node.isStoreForwardRouter {
ClientHistoryButton(
bleManager: bleManager,
context: context,
connectedNode: connectedNode,
node: node
)
}
DeleteNodeButton(
bleManager: bleManager,
context: context,
connectedNode: connectedNode,
node: node
)
}
}
}
if let metadata = node.metadata,
let connectedNode,
self.bleManager.connectedPeripheral != nil {
Section("Administration") {
if connectedNode.myInfo?.hasAdmin ?? false {
Button {
let adminMessageId = bleManager.requestDeviceMetadata(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex,
context: context
)
if adminMessageId > 0 {
Logger.mesh.info("Sent node metadata request from node details")
}
} label: {
Label {
Text("Refresh device metadata")
} icon: {
Image(systemName: "arrow.clockwise")
}
}
}
if metadata.canShutdown {
Button {
showingShutdownConfirm = true
} label: {
Label("Power Off", systemImage: "power")
}.confirmationDialog(
"are.you.sure",
isPresented: $showingShutdownConfirm
) {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
) {
Logger.mesh.warning("Shutdown Failed")
}
}
}
}
if let metadata = node.metadata,
let connectedNode,
self.bleManager.connectedPeripheral != nil {
Section("Administration") {
if connectedNode.myInfo?.hasAdmin ?? false {
Button {
showingRebootConfirm = true
} label: {
Label(
"reboot",
systemImage: "arrow.triangle.2.circlepath"
let adminMessageId = bleManager.requestDeviceMetadata(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex,
context: context
)
if adminMessageId > 0 {
Logger.mesh.info("Sent node metadata request from node details")
}
} label: {
Label {
Text("Refresh device metadata")
} icon: {
Image(systemName: "arrow.clockwise")
}
}
}
if metadata.canShutdown {
Button {
showingShutdownConfirm = true
} label: {
Label("Power Off", systemImage: "power")
}.confirmationDialog(
"are.you.sure",
isPresented: $showingRebootConfirm
isPresented: $showingShutdownConfirm
) {
Button("reboot.node", role: .destructive) {
if !bleManager.sendReboot(
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
) {
Logger.mesh.warning("Reboot Failed")
Logger.mesh.warning("Shutdown Failed")
}
}
}
}
Button {
showingRebootConfirm = true
} label: {
Label(
"reboot",
systemImage: "arrow.triangle.2.circlepath"
)
}.confirmationDialog(
"are.you.sure",
isPresented: $showingRebootConfirm
) {
Button("reboot.node", role: .destructive) {
if !bleManager.sendReboot(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
) {
Logger.mesh.warning("Reboot Failed")
}
}
}
}
}
.listStyle(.insetGrouped)
}
.listStyle(.insetGrouped)
}
}

View file

@ -20,6 +20,7 @@ struct NodeList: View {
@State private var columnVisibility = NavigationSplitViewVisibility.all
@State private var selectedNode: NodeInfoEntity?
@State private var selectedDetails: NodeDetails?
@State private var searchText = ""
@State private var viaLora = true
@State private var viaMqtt = true
@ -251,14 +252,16 @@ struct NodeList: View {
await searchNodeList()
}
}
.onChange(of: router.navigationState) { state in
.onChange(of: router.navigationState) { _ in
// Handle deep link routing
if case .nodes(let selected) = router.navigationState {
self.selectedNode = selected?.selectedNodeNum.flatMap {
getNodeInfo(id: $0, context: context)
}
self.selectedDetails = selected?.details
} else {
self.selectedNode = nil
self.selectedDetails = nil
}
}
.onAppear {