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)): + + + + + + + + +

Mirko Milovanovic

💻 🎨
+ + + + + +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) {