summaryrefslogtreecommitdiff
path: root/Rune/Views/Domains
diff options
context:
space:
mode:
Diffstat (limited to 'Rune/Views/Domains')
-rw-r--r--Rune/Views/Domains/DomainDetailView.swift8
-rw-r--r--Rune/Views/Domains/DomainEditView.swift8
-rw-r--r--Rune/Views/Domains/DomainListView.swift6
-rw-r--r--Rune/Views/Domains/ForwardListView.swift6
-rw-r--r--Rune/Views/Domains/GlueEditView.swift139
-rw-r--r--Rune/Views/Domains/GlueListView.swift139
-rw-r--r--Rune/Views/Domains/RecordListView.swift6
7 files changed, 284 insertions, 28 deletions
diff --git a/Rune/Views/Domains/DomainDetailView.swift b/Rune/Views/Domains/DomainDetailView.swift
index b4ee00d..7fc811a 100644
--- a/Rune/Views/Domains/DomainDetailView.swift
+++ b/Rune/Views/Domains/DomainDetailView.swift
@@ -35,7 +35,11 @@ struct DomainDetailView: View {
NavigationLink("Records") {
RecordListView(domainName: domain.name, viewModel: viewModel, client: client)
}
+ NavigationLink("Glue Records") {
+ GlueListView(domainName: domain.name, viewModel: viewModel, client: client)
+ }
}
+
}
.listStyle(.insetGrouped)
.toolbar {
@@ -79,11 +83,11 @@ struct DomainDetailView: View {
private func nameserverText(_ nameservers: [String]?) -> String {
guard let nameservers else {
- return "Not available"
+ return "Njalla"
}
guard !nameservers.isEmpty else {
- return "Default"
+ return "Njalla"
}
return nameservers.joined(separator: ", ")
diff --git a/Rune/Views/Domains/DomainEditView.swift b/Rune/Views/Domains/DomainEditView.swift
index 0b3b1f8..a5fd99b 100644
--- a/Rune/Views/Domains/DomainEditView.swift
+++ b/Rune/Views/Domains/DomainEditView.swift
@@ -65,14 +65,6 @@ struct DomainEditView: View {
.controlSize(.large)
}
}
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel", role: .cancel) {
- dismiss()
- }
- .disabled(viewModel.isSaving)
- }
- }
.interactiveDismissDisabled(viewModel.isSaving)
.alert("Request Failed", isPresented: localErrorBinding) {
Button("OK", role: .cancel) {}
diff --git a/Rune/Views/Domains/DomainListView.swift b/Rune/Views/Domains/DomainListView.swift
index 68f2ede..ad8134a 100644
--- a/Rune/Views/Domains/DomainListView.swift
+++ b/Rune/Views/Domains/DomainListView.swift
@@ -61,12 +61,6 @@ struct DomainListView: View {
.refreshable {
await viewModel.loadDomains(client: client)
}
- .overlay(alignment: .top) {
- if viewModel.isLoadingDomains && !viewModel.domains.isEmpty {
- ProgressView()
- .padding(.top, 8)
- }
- }
}
}
diff --git a/Rune/Views/Domains/ForwardListView.swift b/Rune/Views/Domains/ForwardListView.swift
index 4e20ab2..6846a79 100644
--- a/Rune/Views/Domains/ForwardListView.swift
+++ b/Rune/Views/Domains/ForwardListView.swift
@@ -88,12 +88,6 @@ struct ForwardListView: View {
await viewModel.loadForwards(for: domainName, client: client)
debugLog("Refresh complete with \(viewModel.forwards.count) forwards for \(domainName)")
}
- .overlay(alignment: .top) {
- if viewModel.isLoadingForwards && !viewModel.forwards.isEmpty {
- ProgressView()
- .padding(.top, 8)
- }
- }
.alert(deleteAlertTitle, isPresented: deleteBinding) {
Button("Delete Forward", role: .destructive) {
guard let forwardPendingDeletion else { return }
diff --git a/Rune/Views/Domains/GlueEditView.swift b/Rune/Views/Domains/GlueEditView.swift
new file mode 100644
index 0000000..b56a5d7
--- /dev/null
+++ b/Rune/Views/Domains/GlueEditView.swift
@@ -0,0 +1,139 @@
+import SwiftUI
+
+struct GlueEditView: View {
+ let domainName: String
+ let existingRecord: GlueRecord?
+ @ObservedObject var viewModel: DomainViewModel
+ let client: NjallaClient
+
+ @Environment(\.dismiss) private var dismiss
+ @State private var name: String
+ @State private var address4: String
+ @State private var address6: String
+ @State private var localErrorMessage: String?
+
+ init(domainName: String, existingRecord: GlueRecord?, viewModel: DomainViewModel, client: NjallaClient) {
+ self.domainName = domainName
+ self.existingRecord = existingRecord
+ self.viewModel = viewModel
+ self.client = client
+ _name = State(initialValue: existingRecord?.name ?? "")
+ _address4 = State(initialValue: existingRecord?.address4 ?? "")
+ _address6 = State(initialValue: existingRecord?.address6 ?? "")
+ }
+
+ var body: some View {
+ Form {
+ Section("Record") {
+ TextField("Name", text: $name)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ .disabled(existingRecord != nil)
+
+ TextField("IPv4", text: $address4)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+
+ TextField("IPv6", text: $address6)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ }
+
+ Section {
+ Button("Save") {
+ Task {
+ await save()
+ }
+ }
+ .disabled(viewModel.isSaving)
+ }
+ }
+ .navigationTitle(existingRecord == nil ? "Add Glue" : "Edit Glue")
+ .navigationBarTitleDisplayMode(.inline)
+ .overlay {
+ if viewModel.isSaving {
+ ProgressView()
+ .controlSize(.large)
+ }
+ }
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel", role: .cancel) {
+ dismiss()
+ }
+ .disabled(viewModel.isSaving)
+ }
+ }
+ .alert("Invalid Glue Data", isPresented: localErrorBinding) {
+ Button("OK", role: .cancel) {}
+ } message: {
+ Text(localErrorMessage ?? "")
+ }
+ .alert("Request Failed", isPresented: mutationErrorBinding) {
+ Button("OK", role: .cancel) {}
+ } message: {
+ Text(viewModel.mutationErrorMessage ?? "")
+ }
+ }
+
+ private func save() async {
+ let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ let trimmedAddress4 = address4.trimmingCharacters(in: .whitespacesAndNewlines)
+ let trimmedAddress6 = address6.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ guard !trimmedName.isEmpty else {
+ localErrorMessage = "Glue record name is required."
+ return
+ }
+
+ guard !trimmedAddress4.isEmpty || !trimmedAddress6.isEmpty else {
+ localErrorMessage = "Provide at least one address (IPv4 or IPv6)."
+ return
+ }
+
+ do {
+ if existingRecord == nil {
+ try await viewModel.addGlue(
+ for: domainName,
+ name: trimmedName,
+ address4: trimmedAddress4.isEmpty ? nil : trimmedAddress4,
+ address6: trimmedAddress6.isEmpty ? nil : trimmedAddress6,
+ client: client
+ )
+ } else {
+ try await viewModel.editGlue(
+ for: domainName,
+ name: trimmedName,
+ address4: trimmedAddress4.isEmpty ? nil : trimmedAddress4,
+ address6: trimmedAddress6.isEmpty ? nil : trimmedAddress6,
+ client: client
+ )
+ }
+ dismiss()
+ } catch {
+ return
+ }
+ }
+
+ private var localErrorBinding: Binding<Bool> {
+ Binding(
+ get: { localErrorMessage != nil },
+ set: { newValue in
+ if !newValue {
+ localErrorMessage = nil
+ }
+ }
+ )
+ }
+
+ private var mutationErrorBinding: Binding<Bool> {
+ Binding(
+ get: { viewModel.mutationErrorMessage != nil },
+ set: { newValue in
+ if !newValue {
+ viewModel.dismissMutationError()
+ }
+ }
+ )
+ }
+}
diff --git a/Rune/Views/Domains/GlueListView.swift b/Rune/Views/Domains/GlueListView.swift
new file mode 100644
index 0000000..b390a84
--- /dev/null
+++ b/Rune/Views/Domains/GlueListView.swift
@@ -0,0 +1,139 @@
+import SwiftUI
+
+struct GlueListView: View {
+ let domainName: String
+ @ObservedObject var viewModel: DomainViewModel
+ let client: NjallaClient
+
+ @State private var showingAddGlue = false
+ @State private var recordPendingDeletion: GlueRecord?
+
+ var body: some View {
+ List {
+ if let errorMessage = viewModel.glueErrorMessage {
+ Section {
+ InlineErrorView(message: errorMessage, retryTitle: "Retry Glue Records") {
+ Task {
+ await viewModel.loadGlue(for: domainName, client: client)
+ }
+ }
+ .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
+ }
+ }
+
+ if viewModel.isLoadingGlue && viewModel.glueRecords.isEmpty {
+ Section {
+ HStack {
+ Spacer()
+ ProgressView("Loading Glue")
+ Spacer()
+ }
+ }
+ } else if viewModel.glueRecords.isEmpty {
+ Section {
+ ContentUnavailableView(
+ "No Glue Records",
+ systemImage: "list.bullet.rectangle",
+ description: Text("No glue records are configured for this domain.")
+ )
+ }
+ } else {
+ ForEach(viewModel.glueRecords) { record in
+ NavigationLink {
+ GlueEditView(domainName: domainName, existingRecord: record, viewModel: viewModel, client: client)
+ } label: {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(record.name)
+ .font(.headline)
+ Text("IPv4: \(record.address4 ?? "n/a")")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ Text("IPv6: \(record.address6 ?? "n/a")")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 4)
+ }
+ .swipeActions {
+ Button("Delete", role: .destructive) {
+ recordPendingDeletion = record
+ }
+ }
+ .disabled(viewModel.isSaving)
+ }
+ }
+ }
+ .listStyle(.insetGrouped)
+ .navigationTitle("Glue Records")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ Button {
+ showingAddGlue = true
+ } label: {
+ Label("Add Glue", systemImage: "plus")
+ }
+ .disabled(viewModel.isSaving)
+ }
+ .sheet(isPresented: $showingAddGlue) {
+ NavigationStack {
+ GlueEditView(domainName: domainName, existingRecord: nil, viewModel: viewModel, client: client)
+ }
+ }
+ .task {
+ await viewModel.loadGlue(for: domainName, client: client)
+ }
+ .refreshable {
+ await viewModel.loadGlue(for: domainName, client: client)
+ }
+ .alert(deleteAlertTitle, isPresented: deleteBinding) {
+ Button("Delete Glue", role: .destructive) {
+ guard let recordPendingDeletion else { return }
+ Task {
+ do {
+ try await viewModel.removeGlue(recordPendingDeletion, client: client)
+ self.recordPendingDeletion = nil
+ } catch {
+ return
+ }
+ }
+ }
+ Button("Cancel", role: .cancel) {
+ recordPendingDeletion = nil
+ }
+ } message: {
+ Text("Delete glue record \(recordPendingDeletion?.name ?? "")?")
+ }
+ .alert("Request Failed", isPresented: mutationErrorBinding) {
+ Button("OK", role: .cancel) {}
+ } message: {
+ Text(viewModel.mutationErrorMessage ?? "")
+ }
+ }
+
+ private var deleteAlertTitle: String {
+ guard let recordPendingDeletion else { return "" }
+ return "Delete glue record \(recordPendingDeletion.name)?"
+ }
+
+ private var deleteBinding: Binding<Bool> {
+ Binding(
+ get: { recordPendingDeletion != nil },
+ set: { newValue in
+ if !newValue {
+ recordPendingDeletion = nil
+ }
+ }
+ )
+ }
+
+ private var mutationErrorBinding: Binding<Bool> {
+ Binding(
+ get: { viewModel.mutationErrorMessage != nil },
+ set: { newValue in
+ if !newValue {
+ viewModel.dismissMutationError()
+ }
+ }
+ )
+ }
+}
diff --git a/Rune/Views/Domains/RecordListView.swift b/Rune/Views/Domains/RecordListView.swift
index 6c40ab0..74ab286 100644
--- a/Rune/Views/Domains/RecordListView.swift
+++ b/Rune/Views/Domains/RecordListView.swift
@@ -73,12 +73,6 @@ struct RecordListView: View {
.refreshable {
await viewModel.loadRecords(for: domainName, client: client)
}
- .overlay(alignment: .top) {
- if viewModel.isLoadingRecords && !viewModel.records.isEmpty {
- ProgressView()
- .padding(.top, 8)
- }
- }
.alert("Request Failed", isPresented: mutationErrorBinding) {
Button("OK", role: .cancel) {}
} message: {