diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 0000000..ace634a
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,25 @@
+{
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": false,
+ "contributors": [
+ {
+ "login": "kobimx",
+ "name": "Mirko Milovanovic",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1266640?v=4",
+ "profile": "https://github.com/kobimx",
+ "contributions": [
+ "code",
+ "design"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7,
+ "projectName": "netdata-ios",
+ "projectOwner": "arjunkomath",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "skipCi": true
+}
diff --git a/NetdataClip/MainView/ServerDetailDemoView.swift b/NetdataClip/MainView/ServerDetailDemoView.swift
index 0dfe938..331142b 100644
--- a/NetdataClip/MainView/ServerDetailDemoView.swift
+++ b/NetdataClip/MainView/ServerDetailDemoView.swift
@@ -86,7 +86,7 @@ struct ServerDetailDemoView: View {
.readableGuidePadding()
}
.onAppear {
- self.viewModel.fetch(baseUrl: serverUrl)
+ self.viewModel.fetch(baseUrl: serverUrl, basicAuthBase64: "")
// hide scroll indicators
UITableView.appearance().showsVerticalScrollIndicator = false
diff --git a/README.md b/README.md
index bcd8ff6..97d8e20 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,8 @@
# Netdata client for iOS, iPadOS & macOS
+
+[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
+
+![Xcode build](https://github.com/arjunkomath/netdata-ios/workflows/Xcode%20build/badge.svg)
### A beautiful and minimal client for Netdata that allows you to monitor cloud infrastructure in real-time.
@@ -6,8 +10,6 @@
*Warning! I'm learning Swift and SwiftUI, so the code is probably terrible.*
-![Xcode build](https://github.com/arjunkomath/netdata-ios/workflows/Xcode%20build/badge.svg)
-
![screenshot-1](https://github.com/arjunkomath/netdata-ios/blob/main/screenshots/mockup.png?raw=true)
## Features
@@ -24,3 +26,22 @@ Open-source iOS, iPadOS and macOS client
## Netdata
[Netdata](https://github.com/netdata/netdata) is distributed, real-time performance and health monitoring for systems and applications. It is a highly-optimized monitoring agent you install on all your systems and containers.
+
+## Contributors ✨
+
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
diff --git a/netdata.xcodeproj/project.pbxproj b/netdata.xcodeproj/project.pbxproj
index 84e34bf..4d2a6f5 100644
--- a/netdata.xcodeproj/project.pbxproj
+++ b/netdata.xcodeproj/project.pbxproj
@@ -1066,7 +1066,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = NetdataClip/NetdataClip.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 15;
+ CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_ASSET_PATHS = "\"NetdataClip/Preview Content\"";
DEVELOPMENT_TEAM = H78GYE72WQ;
ENABLE_PREVIEWS = YES;
@@ -1091,7 +1091,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = NetdataClip/NetdataClip.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 15;
+ CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_ASSET_PATHS = "\"NetdataClip/Preview Content\"";
DEVELOPMENT_TEAM = H78GYE72WQ;
ENABLE_PREVIEWS = YES;
@@ -1320,7 +1320,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = netdata/netdata.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 15;
+ CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_ASSET_PATHS = "\"netdata/Preview Content\"";
DEVELOPMENT_TEAM = H78GYE72WQ;
ENABLE_PREVIEWS = YES;
@@ -1350,7 +1350,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = netdata/netdata.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 15;
+ CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_ASSET_PATHS = "\"netdata/Preview Content\"";
DEVELOPMENT_TEAM = H78GYE72WQ;
ENABLE_PREVIEWS = YES;
diff --git a/netdata/Core/Network/Agent.swift b/netdata/Core/Network/Agent.swift
index 9f62d37..8f21aef 100644
--- a/netdata/Core/Network/Agent.swift
+++ b/netdata/Core/Network/Agent.swift
@@ -8,6 +8,12 @@
import Foundation
import Combine
+enum APIError: Error {
+ case userIsOffline
+ case authenticationFailed
+ case somethingWentWrong
+}
+
struct Agent {
struct Response {
let value: T
@@ -18,9 +24,31 @@ struct Agent {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> Response in
+ guard let httpResponse = result.response as? HTTPURLResponse,
+ httpResponse.statusCode > 0 else {
+ throw URLError(.unknown)
+ }
+
+ // Handle basic authentication error
+ if httpResponse.statusCode == 401 {
+ throw URLError(.userAuthenticationRequired)
+ }
+
let value = try decoder.decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
+ .mapError { error -> APIError in
+ debugPrint(error)
+
+ switch error {
+ case URLError.notConnectedToInternet:
+ return .userIsOffline
+ case URLError.userAuthenticationRequired:
+ return .authenticationFailed
+ default:
+ return .somethingWentWrong
+ }
+ }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
diff --git a/netdata/Core/Network/NetDataAPI.swift b/netdata/Core/Network/NetDataAPI.swift
index 14a486e..295cd16 100644
--- a/netdata/Core/Network/NetDataAPI.swift
+++ b/netdata/Core/Network/NetDataAPI.swift
@@ -1,6 +1,5 @@
//
// Network.swift
-// fiftybest
//
// Created by thomas on 7/1/20.
// Copyright © 2020 thomas. All rights reserved.
@@ -21,31 +20,37 @@ enum NetDataAPI {
}
extension NetDataAPI {
- static func getInfo(baseUrl: String) -> AnyPublisher {
- let base = URL(string: baseUrl)!
+ static func getInfo(baseUrl: String, basicAuthBase64: String = "") -> AnyPublisher {
+ let requestUrl = URL(string: baseUrl)!.appendingPathComponent(NetDataEndpoint.info.rawValue)
- return run(URLRequest(url: base.appendingPathComponent(NetDataEndpoint.info.rawValue)))
+ return run(requestUrl: requestUrl, basicAuthBase64: basicAuthBase64)
}
- static func getCharts(baseUrl: String) -> AnyPublisher {
- let base = URL(string: baseUrl)!
+ static func getCharts(baseUrl: String, basicAuthBase64: String = "") -> AnyPublisher {
+ let requestUrl = URL(string: baseUrl)!.appendingPathComponent(NetDataEndpoint.charts.rawValue)
- return run(URLRequest(url: base.appendingPathComponent(NetDataEndpoint.charts.rawValue)))
+ return run(requestUrl: requestUrl, basicAuthBase64: basicAuthBase64)
}
- static func getChartData(baseUrl: String, chart: String) -> AnyPublisher {
- let base = URL(string: baseUrl)!
+ static func getChartData(baseUrl: String, basicAuthBase64: String = "", chart: String) -> AnyPublisher {
+ let requestUrl = URL(string: baseUrl)!.appendingPathComponent(NetDataEndpoint.data.rawValue + chart)
- return run(URLRequest(url: base.appendingPathComponent(NetDataEndpoint.data.rawValue + chart)))
+ return run(requestUrl: requestUrl, basicAuthBase64: basicAuthBase64)
}
- static func getAlarms(baseUrl: String) -> AnyPublisher {
- let base = URL(string: baseUrl)!
+ static func getAlarms(baseUrl: String, basicAuthBase64: String = "") -> AnyPublisher {
+ let requestUrl = URL(string: baseUrl)!.appendingPathComponent(NetDataEndpoint.alarms.rawValue)
- return run(URLRequest(url: base.appendingPathComponent(NetDataEndpoint.alarms.rawValue)))
+ return run(requestUrl: requestUrl, basicAuthBase64: basicAuthBase64)
}
- static func run(_ request: URLRequest) -> AnyPublisher {
+ static func run(requestUrl: URL, basicAuthBase64: String) -> AnyPublisher {
+ var request = URLRequest(url: requestUrl)
+
+ if !basicAuthBase64.isEmpty {
+ request.setValue("Basic \(basicAuthBase64)", forHTTPHeaderField: "Authorization")
+ }
+
return agent.run(request)
.map(\.value)
.eraseToAnyPublisher()
diff --git a/netdata/Models/NDServer.swift b/netdata/Models/NDServer.swift
index 4bef413..24db255 100644
--- a/netdata/Models/NDServer.swift
+++ b/netdata/Models/NDServer.swift
@@ -18,6 +18,9 @@ public struct NDServer: CloudModel, Equatable, Identifiable {
public let serverInfoJson: String
public let isFavourite: Int
+ // authentication
+ public let basicAuthBase64: String
+
public var record: CKRecord?
public let serverInfo: ServerInfo?
@@ -26,19 +29,20 @@ public struct NDServer: CloudModel, Equatable, Identifiable {
}
enum RecordKeys: String {
- case id, name, description, url, serverInfoJson, isFavourite
+ case id, name, description, url, serverInfoJson, isFavourite, basicAuthBase64
}
public static func == (lhs: NDServer, rhs: NDServer) -> Bool {
lhs.id == rhs.id
}
- public init(name: String, description: String, url: String, serverInfo: ServerInfo?, isFavourite: Int?) {
+ public init(name: String, description: String, url: String, serverInfo: ServerInfo?, basicAuthBase64: String?, isFavourite: Int?) {
self.id = UUID().uuidString
self.name = name
self.description = description
self.url = url
self.serverInfo = serverInfo
+ self.basicAuthBase64 = basicAuthBase64 == nil ? "" : basicAuthBase64!
self.isFavourite = isFavourite == nil ? 0 : isFavourite!
if (serverInfo != nil) {
@@ -56,6 +60,7 @@ public struct NDServer: CloudModel, Equatable, Identifiable {
self.description = record[RecordKeys.description.rawValue] as? String ?? ""
self.url = record[RecordKeys.url.rawValue] as? String ?? ""
self.serverInfoJson = record[RecordKeys.serverInfoJson.rawValue] as? String ?? ""
+ self.basicAuthBase64 = record[RecordKeys.basicAuthBase64.rawValue] as? String ?? ""
self.isFavourite = record[RecordKeys.isFavourite.rawValue] as? Int ?? 0
self.record = record
@@ -70,6 +75,7 @@ public struct NDServer: CloudModel, Equatable, Identifiable {
record[RecordKeys.description.rawValue] = description
record[RecordKeys.url.rawValue] = url
record[RecordKeys.serverInfoJson.rawValue] = serverInfoJson
+ record[RecordKeys.basicAuthBase64.rawValue] = basicAuthBase64
record[RecordKeys.isFavourite.rawValue] = isFavourite
return record
}
diff --git a/netdata/Modules/ServerDetail/Components/ChartsListView.swift b/netdata/Modules/ServerDetail/Components/ChartsListView.swift
index 6456b00..f9dbfea 100644
--- a/netdata/Modules/ServerDetail/Components/ChartsListView.swift
+++ b/netdata/Modules/ServerDetail/Components/ChartsListView.swift
@@ -11,6 +11,7 @@ struct ChartsListView: View {
@Environment(\.presentationMode) private var presentationMode
var serverCharts: ServerCharts
var serverUrl: String
+ var basicAuthBase64: String
@State private var searchText = ""
@@ -23,7 +24,8 @@ struct ChartsListView: View {
ForEach(serverCharts.charts.keys.sorted().filter({ searchText.isEmpty ? true : $0.contains(searchText) }), id: \.self) { key in
if serverCharts.charts[key] != nil && serverCharts.charts[key]!.enabled == true {
NavigationLink(destination: CustomChartDetailView(serverChart: serverCharts.charts[key]!,
- serverUrl: serverUrl)) {
+ serverUrl: serverUrl,
+ basicAuthBase64: basicAuthBase64)) {
ChartListRow(chart: serverCharts.charts[key]!)
}
}
@@ -52,6 +54,7 @@ struct ChartsListView_Previews: PreviewProvider {
ChartsListView(serverCharts: ServerCharts(version: "1.0",
release_channel: "beta",
charts: [:]),
- serverUrl: "")
+ serverUrl: "",
+ basicAuthBase64: "")
}
}
diff --git a/netdata/Modules/ServerDetail/Components/CustomChartDetailView.swift b/netdata/Modules/ServerDetail/Components/CustomChartDetailView.swift
index 692b19e..ebdf4f4 100644
--- a/netdata/Modules/ServerDetail/Components/CustomChartDetailView.swift
+++ b/netdata/Modules/ServerDetail/Components/CustomChartDetailView.swift
@@ -10,6 +10,7 @@ import SwiftUI
struct CustomChartDetailView: View {
var serverChart: ServerChart
var serverUrl: String
+ var basicAuthBase64: String
@StateObject var viewModel = ServerDetailViewModel()
@@ -29,7 +30,7 @@ struct CustomChartDetailView: View {
}
.listStyle(InsetGroupedListStyle())
.onAppear {
- viewModel.fetchCustomChartData(baseUrl: serverUrl, chart: serverChart.name)
+ viewModel.fetchCustomChartData(baseUrl: serverUrl, basicAuthBase64: basicAuthBase64, chart: serverChart.name)
}
.onDisappear {
viewModel.destroyCustomChartData()
diff --git a/netdata/Modules/ServerDetail/ServerDetailView.swift b/netdata/Modules/ServerDetail/ServerDetailView.swift
index 08d8ce8..bd4af3f 100644
--- a/netdata/Modules/ServerDetail/ServerDetailView.swift
+++ b/netdata/Modules/ServerDetail/ServerDetailView.swift
@@ -8,15 +8,18 @@
import SwiftUI
import Combine
+enum ActiveSheet {
+ case alarms, charts, loading
+}
+
struct ServerDetailView: View {
var server: NDServer;
var serverAlarms: ServerAlarms;
- var alarmStatusColor: Color;
@StateObject var viewModel = ServerDetailViewModel()
- @State private var showAlarmsSheet = false
- @State private var showChartsSheet = false
+ @State private var showSheet = false
+ @State private var activeSheet: ActiveSheet = .loading
var body: some View {
List {
@@ -102,7 +105,7 @@ struct ServerDetailView: View {
.readableGuidePadding()
}
.onAppear {
- self.viewModel.fetch(baseUrl: server.url)
+ self.viewModel.fetch(baseUrl: server.url, basicAuthBase64: server.basicAuthBase64)
// hide scroll indicators
UITableView.appearance().showsVerticalScrollIndicator = false
}
@@ -111,42 +114,39 @@ struct ServerDetailView: View {
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(Text(server.name))
- .navigationBarItems(trailing:
- HStack(spacing: 16) {
- Button(action: {
- self.viewModel.destroy()
- self.showChartsSheet = true
- }) {
- Image(systemName: "chart.pie")
- .imageScale(.small)
- .foregroundColor(.accentColor)
- }
- .disabled(self.viewModel.serverChartsToolbarButton)
- .sheet(isPresented: $showChartsSheet, onDismiss: {
- self.viewModel.destroyModel()
- self.viewModel.fetch(baseUrl: server.url)
- }, content: {
- ChartsListView(serverCharts: viewModel.serverCharts, serverUrl: server.url)
- })
-
- Button(action: {
- self.viewModel.destroy()
- self.showAlarmsSheet = true
- }) {
- Image(systemName: "alarm")
- .imageScale(.small)
- .foregroundColor(self.alarmStatusColor)
- }
- .accentColor(self.alarmStatusColor)
- .sheet(isPresented: $showAlarmsSheet, onDismiss: {
- self.viewModel.destroyModel()
- self.viewModel.fetch(baseUrl: server.url)
- }, content: {
- AlarmsListView(serverAlarms: self.serverAlarms)
- })
- }
- .buttonStyle(BorderedBarButtonStyle())
- )
+ .toolbar {
+ ToolbarItemGroup(placement: .navigationBarTrailing) {
+ Button(action: {
+ self.activeSheet = .charts
+ self.viewModel.destroy()
+ self.showSheet.toggle()
+ }) {
+ Image(systemName: "chart.pie")
+ }
+ .disabled(self.viewModel.serverChartsToolbarButton)
+
+ Button(action: {
+ self.activeSheet = .alarms
+ self.viewModel.destroy()
+ self.showSheet.toggle()
+ }) {
+ Image(systemName: "alarm")
+ }
+ }
+ }
+ .sheet(isPresented: $showSheet, onDismiss: {
+ self.viewModel.destroyModel()
+ self.viewModel.fetch(baseUrl: server.url, basicAuthBase64: server.basicAuthBase64)
+ self.activeSheet = .loading
+ }, content: {
+ if self.activeSheet == .loading {
+ ProgressView()
+ } else if self.activeSheet == .charts {
+ ChartsListView(serverCharts: viewModel.serverCharts, serverUrl: server.url, basicAuthBase64: server.basicAuthBase64)
+ } else if self.activeSheet == .alarms {
+ AlarmsListView(serverAlarms: self.serverAlarms)
+ }
+ })
}
func makeSectionHeader(text: String) -> some View {
diff --git a/netdata/Modules/ServerDetail/ServerDetailViewModel.swift b/netdata/Modules/ServerDetail/ServerDetailViewModel.swift
index ca16be3..121d05f 100644
--- a/netdata/Modules/ServerDetail/ServerDetailViewModel.swift
+++ b/netdata/Modules/ServerDetail/ServerDetailViewModel.swift
@@ -34,13 +34,15 @@ final class ServerDetailViewModel: ObservableObject {
@Published var customChartData: ServerData = ServerData(labels: [], data: [])
private var baseUrl = ""
+ private var basicAuthBase64 = ""
private var timer = Timer()
private var customChartTimer = Timer()
private var cancellable = Set()
- func fetch(baseUrl: String) {
+ func fetch(baseUrl: String, basicAuthBase64: String) {
self.loading = true
self.baseUrl = baseUrl
+ self.basicAuthBase64 = basicAuthBase64
self.fetchCharts()
@@ -65,7 +67,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchCpu() {
NetDataAPI
- .getChartData(baseUrl: self.baseUrl, chart: "system.cpu")
+ .getChartData(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64, chart: "system.cpu")
.sink(receiveCompletion: { _ in
}) { data in
self.cpuUsage = data
@@ -79,7 +81,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchLoad() {
NetDataAPI
- .getChartData(baseUrl: self.baseUrl, chart: "system.load")
+ .getChartData(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64, chart: "system.load")
.sink(receiveCompletion: { _ in
}) { data in
self.load = data
@@ -89,7 +91,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchRam() {
NetDataAPI
- .getChartData(baseUrl: self.baseUrl, chart: "system.ram")
+ .getChartData(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64, chart: "system.ram")
.sink(receiveCompletion: { _ in
}) { data in
self.ramUsage = data
@@ -103,7 +105,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchDiskIo() {
NetDataAPI
- .getChartData(baseUrl: self.baseUrl, chart: "system.io")
+ .getChartData(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64, chart: "system.io")
.sink(receiveCompletion: { _ in
}) { data in
self.diskIO = data
@@ -113,7 +115,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchNetwork() {
NetDataAPI
- .getChartData(baseUrl: self.baseUrl, chart: "system.net")
+ .getChartData(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64, chart: "system.net")
.sink(receiveCompletion: { _ in
}) { data in
self.network = data
@@ -123,7 +125,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchDiskSpace() {
NetDataAPI
- .getChartData(baseUrl: self.baseUrl, chart: "disk_space._")
+ .getChartData(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64, chart: "disk_space._")
.sink(receiveCompletion: { _ in
}) { data in
self.diskSpaceUsage = data
@@ -137,7 +139,7 @@ final class ServerDetailViewModel: ObservableObject {
func fetchCharts() {
NetDataAPI
- .getCharts(baseUrl: self.baseUrl)
+ .getCharts(baseUrl: self.baseUrl, basicAuthBase64: self.basicAuthBase64)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
@@ -151,10 +153,10 @@ final class ServerDetailViewModel: ObservableObject {
.store(in: &self.cancellable)
}
- func fetchCustomChartData(baseUrl: String, chart: String) {
+ func fetchCustomChartData(baseUrl: String, basicAuthBase64: String, chart: String) {
self.customChartTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
NetDataAPI
- .getChartData(baseUrl: baseUrl, chart: chart)
+ .getChartData(baseUrl: baseUrl, basicAuthBase64: basicAuthBase64, chart: chart)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
@@ -163,7 +165,6 @@ final class ServerDetailViewModel: ObservableObject {
debugPrint("fetchCustomChartData", error)
}
}) { data in
-
self.customChartData = data
}
.store(in: &self.cancellable)
diff --git a/netdata/Modules/ServerList/Components/AddServerForm.swift b/netdata/Modules/ServerList/Components/AddServerForm.swift
index 6681953..5ca3221 100644
--- a/netdata/Modules/ServerList/Components/AddServerForm.swift
+++ b/netdata/Modules/ServerList/Components/AddServerForm.swift
@@ -11,13 +11,11 @@ import Combine
struct AddServerForm: View {
@Environment(\.presentationMode) private var presentationMode
@StateObject var viewModel = ServerListViewModel()
-
- @State private var invalidUrlAlert = false
-
+
var body: some View {
NavigationView {
Form {
- Section(header: Text("Install Netdata Agent on Server"),
+ Section(header: makeSectionHeader(text: "Install Netdata Agent on Server"),
footer: Text("The Netdata Agent is 100% open source and powered by more than 300 contributors. All components are available under the GPL v3 license on GitHub.")) {
makeRow(image: "gear",
text: "View Installation guide",
@@ -25,12 +23,11 @@ struct AddServerForm: View {
color: .accentColor)
}
- Section(header: Text("Enter Server details"),
+ Section(header: makeSectionHeader(text: "Enter Server details"),
footer: Text("HTTPS is required for connections over the internet\nHTTP is allowed for LAN connections with IP or mDNS domains")) {
if viewModel.validationError {
ErrorMessage(message: viewModel.validationErrorMessage)
}
-
TextField("Name", text: $viewModel.name)
TextField("Description", text: $viewModel.description)
@@ -38,6 +35,26 @@ struct AddServerForm: View {
.autocapitalization(UITextAutocapitalizationType.none)
.disableAutocorrection(true)
}
+
+ Section(header: makeSectionHeader(text: "Authentication"),
+ footer: Text("Base64 encoded authorisation header will be stored in iCloud")) {
+ HStack {
+ Toggle(isOn: $viewModel.enableBasicAuth) {
+ Text("Basic Authentication")
+ }
+ .toggleStyle(SwitchToggleStyle(tint: .accentColor))
+ }
+
+ if viewModel.basicAuthvalidationError {
+ ErrorMessage(message: viewModel.basicAuthvalidationErrorMessage)
+ }
+
+ if viewModel.enableBasicAuth {
+ TextField("Username", text: $viewModel.basicAuthUsername)
+ .autocapitalization(UITextAutocapitalizationType.none)
+ SecureField("Password", text: $viewModel.basicAuthPassword)
+ }
+ }
}
.navigationBarTitle("Setup Server")
.navigationBarItems(leading: dismissButton, trailing: saveButton)
@@ -66,22 +83,6 @@ struct AddServerForm: View {
}
}
- private func checkForMissingField() {
- if (viewModel.name.isEmpty || viewModel.description.isEmpty || viewModel.url.isEmpty) {
- viewModel.validationError = true
- viewModel.validationErrorMessage = "Please fill all the fields"
- return
- }
-
- if (!viewModel.validateUrl(urlString: viewModel.url)) {
- self.invalidUrlAlert = true
- return
- }
-
- viewModel.validationError = false
- viewModel.validationErrorMessage = ""
- }
-
private var dismissButton: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
@@ -95,8 +96,7 @@ struct AddServerForm: View {
private var saveButton: some View {
Button(action: {
- self.checkForMissingField()
- if viewModel.validationError {
+ if viewModel.validateForm() == false {
FeedbackGenerator.shared.triggerNotification(type: .error)
return
}
@@ -115,10 +115,15 @@ struct AddServerForm: View {
}
}
.buttonStyle(BorderedBarButtonStyle())
- .alert(isPresented: $invalidUrlAlert) {
+ .alert(isPresented: $viewModel.invalidUrlAlert) {
Alert(title: Text("Oops!"), message: Text("You've entered an invalid URL"), dismissButton: .default(Text("OK")))
}
}
+
+ func makeSectionHeader(text: String) -> some View {
+ Text(text)
+ .sectionHeaderStyle()
+ }
}
struct AddServerForm_Previews: PreviewProvider {
diff --git a/netdata/Modules/ServerList/Components/EditServerForm.swift b/netdata/Modules/ServerList/Components/EditServerForm.swift
index 86d3e89..c457ea5 100644
--- a/netdata/Modules/ServerList/Components/EditServerForm.swift
+++ b/netdata/Modules/ServerList/Components/EditServerForm.swift
@@ -12,16 +12,14 @@ struct EditServerForm: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject var userSettings = UserSettings()
@StateObject var viewModel = ServerListViewModel()
-
- @State private var invalidUrlAlert = false
-
+
let editingServer: NDServer?
var body: some View {
NavigationView {
Form {
- Section(header: Text("Update Server details"),
- footer: Text("HTTPS is required to connect")) {
+ Section(header: makeSectionHeader(text: "Update Server details"),
+ footer: Text("HTTPS is required for connections over the internet\nHTTP is allowed for LAN connections with IP or mDNS domains")) {
if viewModel.validationError {
ErrorMessage(message: viewModel.validationErrorMessage)
}
@@ -32,6 +30,26 @@ struct EditServerForm: View {
.autocapitalization(UITextAutocapitalizationType.none)
.disableAutocorrection(true)
}
+
+ Section(header: makeSectionHeader(text: "Authentication"),
+ footer: Text("Base64 encoded authorisation header will be stored in iCloud")) {
+ HStack {
+ Toggle(isOn: $viewModel.enableBasicAuth) {
+ Text("Basic Authentication")
+ }
+ .toggleStyle(SwitchToggleStyle(tint: .accentColor))
+ }
+
+ if viewModel.basicAuthvalidationError {
+ ErrorMessage(message: viewModel.basicAuthvalidationErrorMessage)
+ }
+
+ if viewModel.enableBasicAuth {
+ TextField("Username", text: $viewModel.basicAuthUsername)
+ .autocapitalization(UITextAutocapitalizationType.none)
+ SecureField("Password", text: $viewModel.basicAuthPassword)
+ }
+ }
}
.navigationBarTitle("Edit Server")
.navigationBarItems(leading: dismissButton, trailing: saveButton)
@@ -40,27 +58,12 @@ struct EditServerForm: View {
viewModel.name = server.name
viewModel.description = server.description
viewModel.url = server.url
+ viewModel.isFavourite = server.isFavourite
}
}
}
}
- private func checkForMissingField() {
- if (viewModel.name.isEmpty || viewModel.description.isEmpty || viewModel.url.isEmpty) {
- viewModel.validationError = true
- viewModel.validationErrorMessage = "Please fill all the fields"
- return
- }
-
- if (!viewModel.validateUrl(urlString: viewModel.url)) {
- self.invalidUrlAlert = true
- return
- }
-
- viewModel.validationError = false
- viewModel.validationErrorMessage = ""
- }
-
private var dismissButton: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
@@ -74,13 +77,12 @@ struct EditServerForm: View {
private var saveButton: some View {
Button(action: {
- self.checkForMissingField()
- if viewModel.validationError {
+ if viewModel.validateForm() == false {
FeedbackGenerator.shared.triggerNotification(type: .error)
return
}
- viewModel.updateServer(editingServer: editingServer!) { server in
+ viewModel.updateServer(editingServer: editingServer!) { _ in
self.presentationMode.wrappedValue.dismiss()
}
}) {
@@ -94,10 +96,15 @@ struct EditServerForm: View {
}
}
.buttonStyle(BorderedBarButtonStyle())
- .alert(isPresented: $invalidUrlAlert) {
+ .alert(isPresented: $viewModel.invalidUrlAlert) {
Alert(title: Text("Oops!"), message: Text("You've entered an invalid URL"), dismissButton: .default(Text("OK")))
}
}
+
+ func makeSectionHeader(text: String) -> some View {
+ Text(text)
+ .sectionHeaderStyle()
+ }
}
struct EditServerForm_Previews: PreviewProvider {
diff --git a/netdata/Modules/ServerList/Components/ServerListRow.swift b/netdata/Modules/ServerList/Components/ServerListRow.swift
index 91fd6e9..3da8b94 100644
--- a/netdata/Modules/ServerList/Components/ServerListRow.swift
+++ b/netdata/Modules/ServerList/Components/ServerListRow.swift
@@ -19,8 +19,7 @@ struct ServerListRow: View {
var body: some View {
NavigationLink(destination: ServerDetailView(server: server,
- serverAlarms: self.serverAlarms,
- alarmStatusColor: self.getAlarmStatusColor())) {
+ serverAlarms: self.serverAlarms)) {
HStack {
Circle()
.fill(self.getAlarmStatusColor())
@@ -29,6 +28,11 @@ struct ServerListRow: View {
VStack(alignment: .leading, spacing: 5) {
HStack {
+ if !server.basicAuthBase64.isEmpty {
+ Image(systemName: "lock.shield")
+ .foregroundColor(.accentColor)
+ }
+
Text(server.name)
.font(.headline)
}
@@ -78,6 +82,7 @@ struct ServerListRow: View {
description: server.description,
url: server.url,
serverInfo: server.serverInfo,
+ basicAuthBase64: server.basicAuthBase64,
isFavourite: 0)
if let record = server.record {
@@ -98,6 +103,7 @@ struct ServerListRow: View {
description: server.description,
url: server.url,
serverInfo: server.serverInfo,
+ basicAuthBase64: server.basicAuthBase64,
isFavourite: 1)
if let record = server.record {
@@ -146,6 +152,14 @@ struct ServerListRow_Previews: PreviewProvider {
description: "gc us server",
url: "techulus.com",
serverInfo: nil,
+ basicAuthBase64: nil,
+ isFavourite: 0))
+
+ ServerListRow(server: NDServer(name: "Techulus",
+ description: "gc us server",
+ url: "techulus.com",
+ serverInfo: nil,
+ basicAuthBase64: "base64==",
isFavourite: 0))
}
}
diff --git a/netdata/Modules/ServerList/ServerListView.swift b/netdata/Modules/ServerList/ServerListView.swift
index ef18e1c..16d0be9 100644
--- a/netdata/Modules/ServerList/ServerListView.swift
+++ b/netdata/Modules/ServerList/ServerListView.swift
@@ -62,16 +62,22 @@ struct ServerListView: View {
.sheet(isPresented: $showWelcomeSheet, content: {
WelcomeScreen()
})
- .navigationBarItems(leading: settingsButton, trailing: HStack(spacing: 16) {
- refreshButton
+ .toolbar {
+ ToolbarItemGroup(placement: .navigationBarLeading) {
+ settingsButton
+
+ refreshButton
+ }
- if self.serverService.isCloudEnabled && self.serverService.mostRecentError == nil {
- addButton
- .sheet(isPresented: $showAddServerSheet, content: {
- AddServerForm()
- })
+ ToolbarItem(placement: .primaryAction) {
+ if self.serverService.isCloudEnabled && self.serverService.mostRecentError == nil {
+ addButton
+ .sheet(isPresented: $showAddServerSheet, content: {
+ AddServerForm()
+ })
+ }
}
- })
+ }
.navigationTitle("My Servers")
.listStyle(InsetGroupedListStyle())
.onAppear(perform: {
@@ -132,10 +138,8 @@ struct ServerListView: View {
Button(action: {
self.addServer()
}) {
- Image(systemName: "plus")
- .imageScale(.medium)
+ Image(systemName: "externaldrive.badge.plus")
}
- .buttonStyle(BorderedBarButtonStyle())
}
private var refreshButton: some View {
@@ -143,13 +147,11 @@ struct ServerListView: View {
self.serverService.refresh()
}) {
if serverService.isSynching {
- ProgressView().frame(maxWidth: 14.5, maxHeight: 16, alignment: .center) // attempt at fixing the progress circle view
- } else { // from popping and being bigger that the button frame; not pixel perfect
- Image(systemName: "arrow.clockwise") // which it drives me mad
- .imageScale(.small)
+ ProgressView().frame(width: 32, height: 16, alignment: .trailing)
+ } else {
+ Image(systemName: "arrow.clockwise")
}
}
- .buttonStyle(BorderedBarButtonStyle())
}
private var settingsButton: some View {
@@ -157,9 +159,7 @@ struct ServerListView: View {
self.showSettingsSheet = true
}) {
Image(systemName: "gear")
- .imageScale(.small)
}
- .buttonStyle(BorderedBarButtonStyle())
.sheet(isPresented: $showSettingsSheet, content: {
SettingsView()
.environmentObject(serverService)
diff --git a/netdata/Modules/ServerList/ServerListViewModel.swift b/netdata/Modules/ServerList/ServerListViewModel.swift
index 3c3034b..a4f4ce3 100644
--- a/netdata/Modules/ServerList/ServerListViewModel.swift
+++ b/netdata/Modules/ServerList/ServerListViewModel.swift
@@ -16,14 +16,22 @@ final class ServerListViewModel: ObservableObject {
@Published var name = ""
@Published var description = ""
@Published var url = ""
+ @Published var isFavourite = 0
+
+ @Published var enableBasicAuth = false
+ @Published var basicAuthUsername = ""
+ @Published var basicAuthPassword = ""
+ @Published var basicAuthvalidationError = false
+ @Published var basicAuthvalidationErrorMessage = ""
@Published var validatingUrl = false
+ @Published var invalidUrlAlert = false
@Published var validationError = false
@Published var validationErrorMessage = ""
func fetchAlarms(server: NDServer, completion: @escaping (ServerAlarms) -> ()) {
NetDataAPI
- .getAlarms(baseUrl: server.url)
+ .getAlarms(baseUrl: server.url, basicAuthBase64: server.basicAuthBase64)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
@@ -41,22 +49,33 @@ final class ServerListViewModel: ObservableObject {
func addServer(completion: @escaping (NDServer) -> ()) {
validatingUrl = true
+ var basicAuthBase64: String = ""
+ if (enableBasicAuth == true && !basicAuthUsername.isEmpty && !basicAuthPassword.isEmpty) {
+ let loginString = String(format: "%@:%@", basicAuthUsername, basicAuthPassword)
+ let loginData = loginString.data(using: String.Encoding.utf8)!
+ basicAuthBase64 = loginData.base64EncodedString()
+ }
+
NetDataAPI
- .getInfo(baseUrl: url)
+ .getInfo(baseUrl: url, basicAuthBase64: basicAuthBase64)
.sink(receiveCompletion: { completion in
print(completion)
switch completion {
case .finished:
break
case .failure(let error):
- DispatchQueue.main.async {
- self.validatingUrl = false
- self.validationError = true
+ FeedbackGenerator.shared.triggerNotification(type: .error)
+ self.validatingUrl = false
+ self.validationError = true
+
+ guard let apiError = error as? APIError, apiError != .somethingWentWrong else {
self.validationErrorMessage = "Invalid server URL! Please ensure Netdata has been installed on the server."
+ return
}
- FeedbackGenerator.shared.triggerNotification(type: .error)
- debugPrint(error)
+ if apiError == APIError.authenticationFailed {
+ self.validationErrorMessage = "Authentication Failed"
+ }
}
},
receiveValue: { info in
@@ -64,9 +83,11 @@ final class ServerListViewModel: ObservableObject {
description: self.description,
url: self.url,
serverInfo: info,
- isFavourite: 0)
-
- ServerService.shared.add(server: server)
+ basicAuthBase64: basicAuthBase64,
+ isFavourite: self.isFavourite)
+ DispatchQueue.main.async {
+ ServerService.shared.add(server: server)
+ }
completion(server)
})
@@ -76,22 +97,33 @@ final class ServerListViewModel: ObservableObject {
func updateServer(editingServer: NDServer, completion: @escaping (NDServer) -> ()) {
validatingUrl = true
+ var basicAuthBase64: String = ""
+ if (enableBasicAuth == true && !basicAuthUsername.isEmpty && !basicAuthPassword.isEmpty) {
+ let loginString = String(format: "%@:%@", basicAuthUsername, basicAuthPassword)
+ let loginData = loginString.data(using: String.Encoding.utf8)!
+ basicAuthBase64 = loginData.base64EncodedString()
+ }
+
NetDataAPI
- .getInfo(baseUrl: url)
+ .getInfo(baseUrl: url, basicAuthBase64: basicAuthBase64)
.sink(receiveCompletion: { completion in
print(completion)
switch completion {
case .finished:
break
case .failure(let error):
- DispatchQueue.main.async {
- self.validatingUrl = false
- self.validationError = true
- self.validationErrorMessage = "Invalid server URL"
+ FeedbackGenerator.shared.triggerNotification(type: .error)
+ self.validatingUrl = false
+ self.validationError = true
+
+ guard let apiError = error as? APIError, apiError != .somethingWentWrong else {
+ self.validationErrorMessage = "Invalid server URL! Please ensure Netdata has been installed on the server."
+ return
}
- FeedbackGenerator.shared.triggerNotification(type: .error)
- debugPrint(error)
+ if apiError == APIError.authenticationFailed {
+ self.validationErrorMessage = "Authentication Failed"
+ }
}
},
receiveValue: { info in
@@ -99,12 +131,15 @@ final class ServerListViewModel: ObservableObject {
description: self.description,
url: self.url,
serverInfo: info,
- isFavourite: 0)
+ basicAuthBase64: basicAuthBase64,
+ isFavourite: self.isFavourite)
if let record = editingServer.record {
server.record = record
- ServerService.shared.edit(server: server)
+ DispatchQueue.main.async {
+ ServerService.shared.edit(server: server)
+ }
completion(server)
}
@@ -112,6 +147,31 @@ final class ServerListViewModel: ObservableObject {
.store(in: &self.cancellable)
}
+ func validateForm() -> Bool {
+ if (name.isEmpty || description.isEmpty || url.isEmpty) {
+ validationError = true
+ validationErrorMessage = "Please fill all the fields"
+ return false
+ }
+ self.validationError = false
+
+ if (enableBasicAuth == true && basicAuthUsername.isEmpty && basicAuthPassword.isEmpty) {
+ basicAuthvalidationError = true
+ basicAuthvalidationErrorMessage = "Please fill all the fields"
+ return false
+ }
+ self.basicAuthvalidationError = false
+
+ if (!validateUrl(urlString: self.url)) {
+ invalidUrlAlert = true
+ return false
+ }
+
+ self.validationError = false
+ self.validationErrorMessage = ""
+ return true
+ }
+
func validateUrl(urlString: String?) -> Bool {
if let urlString = urlString {
if let url = NSURL(string: urlString) {