220 lines
6.6 KiB
Swift
220 lines
6.6 KiB
Swift
//
|
|
// ServerDetailView.swift
|
|
// iKeyMon
|
|
//
|
|
// Created by tracer on 30.03.25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ServerActionFeedback: Identifiable {
|
|
let id = UUID()
|
|
let title: String
|
|
let message: String
|
|
}
|
|
|
|
struct ServerDetailView: View {
|
|
@Binding var server: Server
|
|
var isFetching: Bool
|
|
var canRestart: Bool = false
|
|
var isRestarting: Bool = false
|
|
var onRestart: (() async -> ServerActionFeedback)? = nil
|
|
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true
|
|
@AppStorage("refreshInterval") private var refreshInterval: Int = 60
|
|
|
|
private var showPlaceholder: Bool {
|
|
server.info == nil
|
|
}
|
|
|
|
@State private var progress: Double = 0
|
|
@State private var showRestartSheet = false
|
|
@State private var restartFeedback: ServerActionFeedback?
|
|
private let indicatorTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
if showIntervalIndicator {
|
|
ProgressView(value: progress)
|
|
.progressViewStyle(LinearProgressViewStyle())
|
|
.padding(.horizontal)
|
|
.frame(height: 2)
|
|
}
|
|
|
|
ZStack(alignment: .topTrailing) {
|
|
VStack(spacing: 0) {
|
|
Spacer().frame(height: 6)
|
|
TabView {
|
|
GeneralView(
|
|
server: resolvedBinding,
|
|
canRestart: canRestart,
|
|
isRestarting: isRestarting
|
|
) {
|
|
showRestartSheet = true
|
|
}
|
|
.tabItem {
|
|
Text("General").unredacted()
|
|
}
|
|
ResourcesView(server: resolvedBinding)
|
|
.tabItem {
|
|
Text("Resources").unredacted()
|
|
}
|
|
ServicesView(server: resolvedBinding)
|
|
.tabItem {
|
|
Text("Services").unredacted()
|
|
}
|
|
}
|
|
.redacted(reason: showPlaceholder ? .placeholder : [])
|
|
.shimmering(active: showPlaceholder)
|
|
}
|
|
|
|
if showPlaceholder || isFetching {
|
|
LoadingBadge()
|
|
.padding()
|
|
}
|
|
}
|
|
.padding(0)
|
|
}
|
|
.overlay(alignment: .bottomTrailing) {
|
|
if let feedback = restartFeedback {
|
|
RestartFeedbackBanner(
|
|
feedback: feedback,
|
|
onDismiss: {
|
|
restartFeedback = nil
|
|
}
|
|
)
|
|
.padding()
|
|
}
|
|
}
|
|
.onReceive(indicatorTimer) { _ in
|
|
guard showIntervalIndicator else { return }
|
|
withAnimation(.linear(duration: 1)) {
|
|
progress += 1.0 / Double(refreshInterval)
|
|
if progress >= 1 { progress = 0 }
|
|
}
|
|
}
|
|
.onChange(of: refreshInterval) { _, _ in
|
|
progress = 0
|
|
}
|
|
.onChange(of: showIntervalIndicator) { _, isVisible in
|
|
if !isVisible {
|
|
progress = 0
|
|
}
|
|
}
|
|
.sheet(isPresented: $showRestartSheet) {
|
|
RestartConfirmationSheet(
|
|
hostname: server.hostname,
|
|
isRestarting: isRestarting,
|
|
onCancel: {
|
|
showRestartSheet = false
|
|
},
|
|
onConfirm: {
|
|
guard let onRestart else { return }
|
|
showRestartSheet = false
|
|
Task {
|
|
let feedback = await onRestart()
|
|
await MainActor.run {
|
|
restartFeedback = feedback
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private var resolvedBinding: Binding<Server> {
|
|
if showPlaceholder {
|
|
return .constant(placeholderServer())
|
|
}
|
|
return $server
|
|
}
|
|
|
|
private func placeholderServer() -> Server {
|
|
Server(id: server.id, hostname: server.hostname, info: .placeholder, pingable: server.pingable)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ServerDetailView(
|
|
server: .constant(Server(id: UUID(), hostname: "preview.example.com", info: ServerInfo.placeholder)),
|
|
isFetching: false,
|
|
canRestart: true
|
|
)
|
|
}
|
|
|
|
private struct LoadingBadge: View {
|
|
var body: some View {
|
|
HStack(spacing: 6) {
|
|
ProgressView()
|
|
.scaleEffect(0.5)
|
|
Text("Fetching latest data…")
|
|
.font(.caption)
|
|
}
|
|
.padding(8)
|
|
.background(.ultraThinMaterial, in: Capsule())
|
|
}
|
|
}
|
|
|
|
private struct RestartConfirmationSheet: View {
|
|
let hostname: String
|
|
let isRestarting: Bool
|
|
let onCancel: () -> Void
|
|
let onConfirm: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text("Reboot this server?")
|
|
.font(.title3.weight(.semibold))
|
|
|
|
Text("This will send a reboot command to \(hostname).")
|
|
.foregroundColor(.secondary)
|
|
|
|
HStack {
|
|
Spacer()
|
|
|
|
Button("Cancel") {
|
|
onCancel()
|
|
}
|
|
.keyboardShortcut(.cancelAction)
|
|
.disabled(isRestarting)
|
|
|
|
Button("Reboot", role: .destructive) {
|
|
onConfirm()
|
|
}
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(isRestarting)
|
|
}
|
|
}
|
|
.padding(24)
|
|
.frame(width: 420)
|
|
}
|
|
}
|
|
|
|
private struct RestartFeedbackBanner: View {
|
|
let feedback: ServerActionFeedback
|
|
let onDismiss: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text(feedback.title)
|
|
.font(.headline)
|
|
|
|
Text(feedback.message)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
HStack {
|
|
Spacer()
|
|
|
|
Button("OK") {
|
|
onDismiss()
|
|
}
|
|
.keyboardShortcut(.defaultAction)
|
|
}
|
|
}
|
|
.frame(maxWidth: 360, alignment: .leading)
|
|
.padding(24)
|
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.shadow(radius: 12)
|
|
}
|
|
}
|