diff --git a/APIClient/APIClient.swift b/APIClient/APIClient.swift index dc2e785..6c5139d 100644 --- a/APIClient/APIClient.swift +++ b/APIClient/APIClient.swift @@ -54,6 +54,8 @@ final class APIClient: ObservableObject { } } + @AppStorage("currentOrganisationID") var _currentOrganisationID: String? + @Published var registrationStatus: RegistrationStatus? @Published var userToken: UserTokenDTO? { @@ -387,6 +389,9 @@ extension APIClient { request.httpMethod = httpMethod request.setValue(contentType, forHTTPHeaderField: "Content-Type") request.setValue(userToken?.bearerTokenAuthString, forHTTPHeaderField: "Authorization") + if let currentOrgID = _currentOrganisationID { + request.setValue(currentOrgID, forHTTPHeaderField: "td-organization-id") + } if let httpBody = httpBody { request.httpBody = httpBody diff --git a/APIClient/DTOs/OrganizationInfo.swift b/APIClient/DTOs/OrganizationInfo.swift new file mode 100644 index 0000000..0483db4 --- /dev/null +++ b/APIClient/DTOs/OrganizationInfo.swift @@ -0,0 +1,80 @@ +// +// OrganizationInfo.swift +// Telemetry Viewer +// +// Created by Lukas on 29.05.24. +// + +import Foundation +import SwiftUI + +struct OrganizationInfo: Codable, Equatable { + var id: UUID + var name: String + var stripeCustomerID: String? + var stripeMaxSignals: Double? + var maxSignalsMultiplier: Double? + var resolvedMaxSignals: Int64 + var isInRestrictedMode: Bool + var countryCode: String? + var referralCode: String + var usagePercentage: Double? + var isSuperOrg: Bool + var apps: [AppInfo] + var basePermissions: AppAccessLevel + var roleOrganizationPermissions: AppAccessLevel? + + var appIDs: [UUID] { + apps.map { app in + app.id + } + } + + } + +public enum AppAccessLevel: String, Codable, Comparable { + case none + case read + case write + case administrate + + public static func < (lhs: AppAccessLevel, rhs: AppAccessLevel) -> Bool { + switch lhs { + case .none: + switch rhs { + case .none: + false + case .read: + true + case .write: + true + case .administrate: + true + } + case .read: + switch rhs { + case .none: + false + case .read: + false + case .write: + true + case .administrate: + true + } + case .write: + switch rhs { + case .none: + false + case .read: + false + case .write: + false + case .administrate: + true + } + case .administrate: + false + } + } +} diff --git a/Services/AppService.swift b/Services/AppService.swift index 896be1a..955128e 100644 --- a/Services/AppService.swift +++ b/Services/AppService.swift @@ -75,42 +75,4 @@ class AppService: ObservableObject { } } } - - func create(appNamed name: String, callback: ((Result) -> Void)? = nil) { - let url = api.urlForPath(apiVersion: .v3, "apps") - - api.post(["name": name], to: url) { [unowned self] (result: Result) in - - if let app = try? result.get() { - appDictionary[app.id] = app - orgService.organization?.appIDs.append(app.id) - } - - callback?(result) - } - } - - func update(appID: UUID, newName: String, callback: ((Result) -> Void)? = nil) { - let url = api.urlForPath(apiVersion: .v3, "apps", appID.uuidString) - - api.patch(["name": newName], to: url) { [unowned self] (result: Result) in - - if let app = try? result.get() { - appDictionary[app.id] = app - } - - callback?(result) - } - } - - func delete(appID: UUID, callback: ((Result<[String: String], TransferError>) -> Void)? = nil) { - let url = api.urlForPath(apiVersion: .v2, "apps", appID.uuidString) - - api.delete(url) { [unowned self] (result: Result<[String: String], TransferError>) in - - appDictionary[appID] = nil - orgService.organization?.appIDs = (orgService.organization?.appIDs.filter { $0 != appID })! - callback?(result) - } - } } diff --git a/Services/OrgService.swift b/Services/OrgService.swift index b904ca3..32c7a87 100644 --- a/Services/OrgService.swift +++ b/Services/OrgService.swift @@ -22,13 +22,7 @@ class OrgService: ObservableObject { self.errorService = errors } - // Wasn't getting called before -> Never leaving loading state. - // For now called when retrieveOrganisations also called - // Maybe move getCode into the retrieve Func? func getOrganisation() { - let locallyCachedOrganization = retrieveFromDisk() - self.organization = locallyCachedOrganization - self.loadingState = .loading Task { @@ -57,8 +51,6 @@ class OrgService: ObservableObject { switch result { case let .success(org): - self.saveToDisk(org: org) - continuation.resume(returning: org) case let .failure(error): @@ -68,32 +60,10 @@ class OrgService: ObservableObject { } } } -} - -/// this is interesting, do we want this for more than the org? -private extension OrgService { - var organizationCacheFilePath: URL { - let fileManager = FileManager.default - let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask) - let cachesDirectoryUrl = urls[0] - let fileUrl = cachesDirectoryUrl.appendingPathComponent("telemetrydeck.organization.json") - let filePath = fileUrl.path - - if !fileManager.fileExists(atPath: filePath) { - let contents = Data() - fileManager.createFile(atPath: filePath, contents: contents) - } - return fileUrl - } - - func saveToDisk(org: DTOv2.Organization) { - guard let data = try? JSONEncoder.telemetryEncoder.encode(org) else { return } - try? data.write(to: self.organizationCacheFilePath, options: .atomic) - } - - func retrieveFromDisk() -> DTOv2.Organization? { - guard let data = try? Data(contentsOf: organizationCacheFilePath) else { return nil } - return try? JSONDecoder.telemetryDecoder.decode(DTOv2.Organization.self, from: data) + func allOrganizations() async throws -> [OrganizationInfo]{ + let url = api.urlForPath(apiVersion: .v3, "organizations") + return try await api.get(url: url) } } + diff --git a/Shared/Empty Status Views/NoAppSelectedView.swift b/Shared/Empty Status Views/NoAppSelectedView.swift index 14ded84..a493c3e 100644 --- a/Shared/Empty Status Views/NoAppSelectedView.swift +++ b/Shared/Empty Status Views/NoAppSelectedView.swift @@ -22,11 +22,6 @@ struct NoAppSelectedView: View { Text("To start, create your first App. You can use that App's unique identifier to send signals from your code.") .foregroundColor(.grayColor) VStack { - Button("Create First App") { - appService.create(appNamed: "New App") - } - .buttonStyle(SmallPrimaryButtonStyle()) - Button("Documentation: Sending Signals") { #if os(macOS) NSWorkspace.shared.open(URL(string: "https://telemetrydeck.com/pages/quickstart.html")!) diff --git a/Shared/Navigational Structure/LeftSidebarView.swift b/Shared/Navigational Structure/LeftSidebarView.swift index 76cec7c..24c1e4e 100644 --- a/Shared/Navigational Structure/LeftSidebarView.swift +++ b/Shared/Navigational Structure/LeftSidebarView.swift @@ -36,6 +36,23 @@ struct LeftSidebarView: View { case editApp(app: UUID) } + func getApps(organization: DTOv2.Organization?) { + Task { + for appID in organization?.appIDs ?? [] { + if let app = try? await appService.retrieveApp(withID: appID) { + DispatchQueue.main.async { + app.insightGroupIDs.forEach { groupID in + if !(groupService.groupsDictionary.keys.contains(groupID)) { + groupService.retrieveGroup(with: groupID) + } + } + appService.appDictionary[app.id] = app + } + } + } + } + } + var body: some View { List { Section { @@ -43,19 +60,11 @@ struct LeftSidebarView: View { ForEach(organization.appIDs, id: \.self) { appID in section(for: appID) } + .onChange(of: orgService.organization) { + getApps(organization: orgService.organization) + } .task { - for appID in organization.appIDs { - if let app = try? await appService.retrieveApp(withID: appID) { - DispatchQueue.main.async { - app.insightGroupIDs.forEach { groupID in - if !(groupService.groupsDictionary.keys.contains(groupID)) { - groupService.retrieveGroup(with: groupID) - } - } - appService.appDictionary[app.id] = app - } - } - } + getApps(organization: orgService.organization) } } @@ -64,7 +73,7 @@ struct LeftSidebarView: View { } Section { - LoadingStateIndicator(loadingState: orgService.loadingState, title: orgService.organization?.name) + OrganisationSwitcher() #if os(iOS) Button { diff --git a/Telemetry Viewer.xcodeproj/project.pbxproj b/Telemetry Viewer.xcodeproj/project.pbxproj index 31fa610..4c5a3dc 100644 --- a/Telemetry Viewer.xcodeproj/project.pbxproj +++ b/Telemetry Viewer.xcodeproj/project.pbxproj @@ -163,6 +163,13 @@ 80427FF62BFE2AF4007E89CC /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80427FDF2BFE2AF4007E89CC /* UserInfo.swift */; }; 80427FF72BFE2AF4007E89CC /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80427FDF2BFE2AF4007E89CC /* UserInfo.swift */; }; 80427FF82BFE2AF4007E89CC /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80427FDF2BFE2AF4007E89CC /* UserInfo.swift */; }; + 804385CF2C073763004E3285 /* OrganizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385CE2C073763004E3285 /* OrganizationInfo.swift */; }; + 804385D02C073763004E3285 /* OrganizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385CE2C073763004E3285 /* OrganizationInfo.swift */; }; + 804385D12C073763004E3285 /* OrganizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385CE2C073763004E3285 /* OrganizationInfo.swift */; }; + 804385D22C073763004E3285 /* OrganizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385CE2C073763004E3285 /* OrganizationInfo.swift */; }; + 804385D32C073763004E3285 /* OrganizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385CE2C073763004E3285 /* OrganizationInfo.swift */; }; + 804385D42C073763004E3285 /* OrganizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385CE2C073763004E3285 /* OrganizationInfo.swift */; }; + 804385D62C0739D0004E3285 /* OrganisationSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804385D52C0739D0004E3285 /* OrganisationSwitcher.swift */; }; 8083DD692C05C9C300596926 /* ClusterPieChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8083DD682C05C9C300596926 /* ClusterPieChart.swift */; }; 8083DD6B2C05C9FE00596926 /* PieChartTopN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8083DD6A2C05C9FE00596926 /* PieChartTopN.swift */; }; 8083DD6D2C05EA0100596926 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8083DD6C2C05EA0100596926 /* Extensions.swift */; }; @@ -516,6 +523,8 @@ 80427FDD2BFE2AF4007E89CC /* InsightGroupInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsightGroupInfo.swift; sourceTree = ""; }; 80427FDE2BFE2AF4007E89CC /* InsightInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsightInfo.swift; sourceTree = ""; }; 80427FDF2BFE2AF4007E89CC /* UserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; + 804385CE2C073763004E3285 /* OrganizationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizationInfo.swift; sourceTree = ""; }; + 804385D52C0739D0004E3285 /* OrganisationSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganisationSwitcher.swift; sourceTree = ""; }; 8083DD682C05C9C300596926 /* ClusterPieChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClusterPieChart.swift; sourceTree = ""; }; 8083DD6A2C05C9FE00596926 /* PieChartTopN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieChartTopN.swift; sourceTree = ""; }; 8083DD6C2C05EA0100596926 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; @@ -855,6 +864,7 @@ 80427FDD2BFE2AF4007E89CC /* InsightGroupInfo.swift */, 80427FDE2BFE2AF4007E89CC /* InsightInfo.swift */, 80427FDF2BFE2AF4007E89CC /* UserInfo.swift */, + 804385CE2C073763004E3285 /* OrganizationInfo.swift */, ); path = DTOs; sourceTree = ""; @@ -1142,6 +1152,7 @@ DC9A92AF255C1CD100E92C89 /* AutoCompletingTextField.swift */, 2B21FCD926FDC9F900A8A55B /* InsightsGrid.swift */, DCDC6EFE253EDA4C0012D9A7 /* FilterEditView.swift */, + 804385D52C0739D0004E3285 /* OrganisationSwitcher.swift */, 2B21FCDC26FDCA6A00A8A55B /* GroupView.swift */, 2B21FCD426FDC33F00A8A55B /* InsightGroupsView.swift */, DCE239FC24D3687C00053370 /* Telemetry_ViewerApp_iOS.swift */, @@ -1551,6 +1562,7 @@ C51CB78B27566279005A3FB9 /* InsightService.swift in Sources */, C5F4D37028ACF64700EBB667 /* ChartDataSet.swift in Sources */, 80427FEC2BFE2AF4007E89CC /* InsightGroupInfo.swift in Sources */, + 804385D42C073763004E3285 /* OrganizationInfo.swift in Sources */, C51CB78927566275005A3FB9 /* ErrorService.swift in Sources */, C51CB7922756646E005A3FB9 /* TelemetrySignalTypes.swift in Sources */, C5F4D37128ACF64700EBB667 /* ChartDataPoint.swift in Sources */, @@ -1613,6 +1625,7 @@ C5F4D33528ACF48000EBB667 /* LineChartView.swift in Sources */, C51CB78527566258005A3FB9 /* ConditionalViewModifier.swift in Sources */, C5F4D33D28ACF48000EBB667 /* RawChartView.swift in Sources */, + 804385D22C073763004E3285 /* OrganizationInfo.swift in Sources */, C51CB77F27566243005A3FB9 /* Color.swift in Sources */, C5F4D33128ACF48000EBB667 /* ChartBottomView.swift in Sources */, C51CB7912756646D005A3FB9 /* TelemetrySignalTypes.swift in Sources */, @@ -1675,6 +1688,7 @@ C5F4D33428ACF48000EBB667 /* LineChartView.swift in Sources */, C5CE3D57271AFCE2005232EC /* Buttonstyles.swift in Sources */, C5F4D33C28ACF48000EBB667 /* RawChartView.swift in Sources */, + 804385D12C073763004E3285 /* OrganizationInfo.swift in Sources */, 2B6431A72739A5BB009A33C4 /* AsyncOperation.swift in Sources */, C5F4D33028ACF48000EBB667 /* ChartBottomView.swift in Sources */, C581F4E0271B22FD0031E99C /* Color+Hex.swift in Sources */, @@ -1699,6 +1713,7 @@ 2B64319C2739A5BB009A33C4 /* Data+JSONPrettyPrint.swift in Sources */, C5F4D36E28ACF64600EBB667 /* ChartDataSet.swift in Sources */, 80427FEB2BFE2AF4007E89CC /* InsightGroupInfo.swift in Sources */, + 804385D32C073763004E3285 /* OrganizationInfo.swift in Sources */, 2B6431AC2739A5BB009A33C4 /* Caching.swift in Sources */, C51CB78F27566296005A3FB9 /* ConditionalViewModifier.swift in Sources */, C5F4D36F28ACF64600EBB667 /* ChartDataPoint.swift in Sources */, @@ -1721,6 +1736,7 @@ C5F4D35628ACF48000EBB667 /* ChartDataSet.swift in Sources */, C5F4D33E28ACF48000EBB667 /* RoundedCorners.swift in Sources */, 2B21FCD526FDC33F00A8A55B /* InsightGroupsView.swift in Sources */, + 804385D62C0739D0004E3285 /* OrganisationSwitcher.swift in Sources */, DCB02B722502781A00304964 /* LoginView.swift in Sources */, 2B379D2126FBC3A300714BE6 /* IconFinderService.swift in Sources */, C5CD589D2810368400671359 /* ThreeCirclesInATrenchcode.swift in Sources */, @@ -1743,6 +1759,7 @@ 2B781B8326F4A5E30062DBDC /* StatusMessageBanner.swift in Sources */, DCE23A0F24D3687D00053370 /* Telemetry_ViewerApp_iOS.swift in Sources */, DCF7CD40254A08B900BFA23B /* LexiconView.swift in Sources */, + 804385CF2C073763004E3285 /* OrganizationInfo.swift in Sources */, 80AD3E9E2BFF305100BBD7EB /* ClusterBarChart.swift in Sources */, 2B3CC0DC264D4AFE0038B528 /* LexiconService.swift in Sources */, 80AD3EA52BFF33FF00BBD7EB /* LineChartTimeSeries.swift in Sources */, @@ -1928,6 +1945,7 @@ 2B46280F27286D2500515530 /* TestingModeToggle.swift in Sources */, 2B1D469926CC4C5D008814A9 /* ErrorService.swift in Sources */, 2B61B94F264C08D4003F62C4 /* SignalsService.swift in Sources */, + 804385D02C073763004E3285 /* OrganizationInfo.swift in Sources */, C5F4D33F28ACF48000EBB667 /* RoundedCorners.swift in Sources */, DC627EF025A35B6A00C1DF33 /* EmptyAppView.swift in Sources */, 6351A78A277C9EDA003AF559 /* InsightDisplayMode+Extensions.swift in Sources */, diff --git a/iOS/OrganisationSwitcher.swift b/iOS/OrganisationSwitcher.swift new file mode 100644 index 0000000..cc8a0bb --- /dev/null +++ b/iOS/OrganisationSwitcher.swift @@ -0,0 +1,59 @@ +// +// OrganisationSwitcher.swift +// Telemetry Viewer +// +// Created by Lukas on 29.05.24. +// + +import SwiftUI +import DataTransferObjects + +struct OrganisationSwitcher: View { + @EnvironmentObject var api: APIClient + @EnvironmentObject var orgService: OrgService + + @State var organizations: [OrganizationInfo] = [] + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 0){ + Menu { + ForEach(organizations, id: \.id) { org in + Button(action: { + api._currentOrganisationID = org.id.uuidString + orgService.getOrganisation() + }, label: { + Text(org.name) + }) + } + } label: { + Text(getCurrentOrgName()) + .tint(.primary) + } + .task { + organizations = (try? await orgService.allOrganizations()) ?? [] + } + Text("Tap to switch organization") + .font(.callout) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "person.3") + .foregroundStyle(Color.telemetryOrange) + + } + } + + func getCurrentOrgName() -> String{ + let org = organizations.first { org in + org.id.uuidString == api._currentOrganisationID + } + return org?.name ?? organizations.first?.name ?? "No Data" + } +} + +#Preview { + OrganisationSwitcher() +}