mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
feat(apple): config to hide resource list (#10824)
Adds a configuration variable `hideResourceList` accessible by provisioning profile only to hide or show the Resource list. This is helpful when end-users need not be concerned with the resources available to their account. Also updates the associated ProfileManifests, docs, and a little bit of housekeeping around `configuration`, making it public for direct access. <img width="292" height="228" alt="Screenshot 2025-11-09 at 9 12 47 PM" src="https://github.com/user-attachments/assets/a4ce5586-bf92-4ebc-bc0d-51215e1efd61" /> Related: https://github.com/ProfileManifests/ProfileManifests/pull/839 Fixes: #10808 --------- Signed-off-by: Jamil <jamilbk@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,7 @@ public class Configuration: ObservableObject {
|
||||
|
||||
@Published private(set) var publishedInternetResourceEnabled = false
|
||||
@Published private(set) var publishedHideAdminPortalMenuItem = false
|
||||
@Published private(set) var publishedHideResourceList = false
|
||||
|
||||
var isAuthURLForced: Bool { defaults.objectIsForced(forKey: Keys.authURL) }
|
||||
var isApiURLForced: Bool { defaults.objectIsForced(forKey: Keys.apiURL) }
|
||||
@@ -51,6 +52,11 @@ public class Configuration: ObservableObject {
|
||||
set { defaults.set(newValue, forKey: Keys.hideAdminPortalMenuItem) }
|
||||
}
|
||||
|
||||
var hideResourceList: Bool {
|
||||
get { defaults.bool(forKey: Keys.hideResourceList) }
|
||||
set { defaults.set(newValue, forKey: Keys.hideResourceList) }
|
||||
}
|
||||
|
||||
var connectOnStart: Bool {
|
||||
get { defaults.bool(forKey: Keys.connectOnStart) }
|
||||
set { defaults.set(newValue, forKey: Keys.connectOnStart) }
|
||||
@@ -102,6 +108,7 @@ public class Configuration: ObservableObject {
|
||||
static let accountSlug = "accountSlug"
|
||||
static let internetResourceEnabled = "internetResourceEnabled"
|
||||
static let hideAdminPortalMenuItem = "hideAdminPortalMenuItem"
|
||||
static let hideResourceList = "hideResourceList"
|
||||
static let connectOnStart = "connectOnStart"
|
||||
static let startOnLogin = "startOnLogin"
|
||||
static let disableUpdateCheck = "disableUpdateCheck"
|
||||
@@ -119,6 +126,7 @@ public class Configuration: ObservableObject {
|
||||
|
||||
self.publishedInternetResourceEnabled = internetResourceEnabled
|
||||
self.publishedHideAdminPortalMenuItem = hideAdminPortalMenuItem
|
||||
self.publishedHideResourceList = hideResourceList
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@@ -173,6 +181,7 @@ public class Configuration: ObservableObject {
|
||||
// Update published properties
|
||||
self.publishedInternetResourceEnabled = internetResourceEnabled
|
||||
self.publishedHideAdminPortalMenuItem = hideAdminPortalMenuItem
|
||||
self.publishedHideResourceList = hideResourceList
|
||||
|
||||
// Announce we changed
|
||||
objectWillChange.send()
|
||||
|
||||
@@ -43,7 +43,7 @@ public final class Store: ObservableObject {
|
||||
|
||||
private var resourcesTimer: Timer?
|
||||
private var resourceUpdateTask: Task<Void, Never>?
|
||||
private var configuration: Configuration
|
||||
public let configuration: Configuration
|
||||
private var vpnConfigurationManager: VPNConfigurationManager?
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import SwiftUI
|
||||
var cancellables: Set<AnyCancellable> = []
|
||||
var updateChecker: UpdateChecker
|
||||
var updateMenuDisplayed: Bool = false
|
||||
var hideResourceList: Bool
|
||||
var signedOutIcon: NSImage?
|
||||
var signedInConnectedIcon: NSImage?
|
||||
var signedOutIconNotification: NSImage?
|
||||
@@ -164,10 +165,7 @@ import SwiftUI
|
||||
return menuItem
|
||||
}()
|
||||
|
||||
private let configuration: Configuration
|
||||
|
||||
public init(store: Store, configuration: Configuration? = nil) {
|
||||
self.configuration = configuration ?? Configuration.shared
|
||||
public init(store: Store) {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
self.store = store
|
||||
self.updateChecker = UpdateChecker()
|
||||
@@ -182,6 +180,7 @@ import SwiftUI
|
||||
self.connectingAnimationImages[.first] = NSImage(named: "MenuBarIconConnecting1")
|
||||
self.connectingAnimationImages[.second] = NSImage(named: "MenuBarIconConnecting2")
|
||||
self.connectingAnimationImages[.last] = NSImage(named: "MenuBarIconConnecting3")
|
||||
self.hideResourceList = self.store.configuration.publishedHideResourceList
|
||||
|
||||
super.init()
|
||||
|
||||
@@ -215,24 +214,32 @@ import SwiftUI
|
||||
self.handleStatusChanged()
|
||||
}).store(in: &cancellables)
|
||||
|
||||
configuration.$publishedInternetResourceEnabled
|
||||
store.configuration.$publishedInternetResourceEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] newEnabled in
|
||||
guard let self = self else { return }
|
||||
|
||||
if configuration.internetResourceEnabled != newEnabled {
|
||||
if store.configuration.internetResourceEnabled != newEnabled {
|
||||
handleResourceListChanged()
|
||||
}
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
configuration.$publishedHideAdminPortalMenuItem
|
||||
store.configuration.$publishedHideAdminPortalMenuItem
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] newValue in
|
||||
self?.updateConfigurableMenuItems(hideAdminPortalMenuItem: newValue)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
store.configuration.$publishedHideResourceList
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] newValue in
|
||||
self?.hideResourceList = newValue
|
||||
self?.handleResourceListChanged()
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
updateChecker.$updateAvailable
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
@@ -273,23 +280,28 @@ import SwiftUI
|
||||
}
|
||||
|
||||
func populateResourceMenus(_ newResources: [Resource]) {
|
||||
// If we have no favorites, then everything is a favorite
|
||||
let hasAnyFavorites = newResources.contains { store.favorites.contains($0.id) }
|
||||
let newFavorites =
|
||||
if hasAnyFavorites {
|
||||
newResources.filter { store.favorites.contains($0.id) || $0.isInternetResource() }
|
||||
} else {
|
||||
newResources
|
||||
}
|
||||
let newOthers: [Resource] =
|
||||
if hasAnyFavorites {
|
||||
newResources.filter { !store.favorites.contains($0.id) && !$0.isInternetResource() }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
if self.hideResourceList {
|
||||
populateFavoriteResourcesMenu([])
|
||||
populateOtherResourcesMenu([])
|
||||
} else {
|
||||
// If we have no favorites, then everything is a favorite
|
||||
let hasAnyFavorites = newResources.contains { store.favorites.contains($0.id) }
|
||||
let newFavorites =
|
||||
if hasAnyFavorites {
|
||||
newResources.filter { store.favorites.contains($0.id) || $0.isInternetResource() }
|
||||
} else {
|
||||
newResources
|
||||
}
|
||||
let newOthers: [Resource] =
|
||||
if hasAnyFavorites {
|
||||
newResources.filter { !store.favorites.contains($0.id) && !$0.isInternetResource() }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
populateFavoriteResourcesMenu(newFavorites)
|
||||
populateOtherResourcesMenu(newOthers)
|
||||
populateFavoriteResourcesMenu(newFavorites)
|
||||
populateOtherResourcesMenu(newOthers)
|
||||
}
|
||||
}
|
||||
|
||||
func populateFavoriteResourcesMenu(_ newFavorites: [Resource]) {
|
||||
@@ -310,7 +322,7 @@ import SwiftUI
|
||||
}
|
||||
}
|
||||
lastShownFavorites = newFavorites
|
||||
wasInternetResourceEnabled = configuration.internetResourceEnabled
|
||||
wasInternetResourceEnabled = store.configuration.internetResourceEnabled
|
||||
}
|
||||
|
||||
func populateOtherResourcesMenu(_ newOthers: [Resource]) {
|
||||
@@ -338,7 +350,7 @@ import SwiftUI
|
||||
}
|
||||
}
|
||||
lastShownOthers = newOthers
|
||||
wasInternetResourceEnabled = configuration.internetResourceEnabled
|
||||
wasInternetResourceEnabled = store.configuration.internetResourceEnabled
|
||||
}
|
||||
|
||||
func updateStatusItemIcon() {
|
||||
@@ -386,47 +398,55 @@ import SwiftUI
|
||||
}
|
||||
}
|
||||
|
||||
// Update resources "header" menu items. An administrator can choose to set a configuration to
|
||||
// hide the Resource List at which point we avoid displaying it.
|
||||
func updateResourcesMenuItems() {
|
||||
// Update resources "header" menu items
|
||||
switch store.vpnStatus {
|
||||
case .connecting:
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.target = nil
|
||||
resourcesUnavailableReasonMenuItem.title = "Connecting…"
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case .connected:
|
||||
resourcesTitleMenuItem.isHidden = false
|
||||
resourcesUnavailableMenuItem.isHidden = true
|
||||
resourcesUnavailableReasonMenuItem.isHidden = true
|
||||
resourcesTitleMenuItem.title = resourceMenuTitle(store.resourceList)
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case .reasserting:
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.target = nil
|
||||
resourcesUnavailableReasonMenuItem.title = "No network connectivity"
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case .disconnecting:
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.target = nil
|
||||
resourcesUnavailableReasonMenuItem.title = "Disconnecting…"
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case nil, .disconnected, .invalid:
|
||||
// We should never be in a state where the tunnel is
|
||||
// down but the user is signed in, but we have
|
||||
// code to handle it just for the sake of completion.
|
||||
if self.hideResourceList {
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = true
|
||||
resourcesUnavailableReasonMenuItem.isHidden = true
|
||||
resourcesUnavailableReasonMenuItem.title = "Disconnected"
|
||||
resourcesSeparatorMenuItem.isHidden = true
|
||||
@unknown default:
|
||||
break
|
||||
} else {
|
||||
switch store.vpnStatus {
|
||||
case .connecting:
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.target = nil
|
||||
resourcesUnavailableReasonMenuItem.title = "Connecting…"
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case .connected:
|
||||
resourcesTitleMenuItem.isHidden = false
|
||||
resourcesUnavailableMenuItem.isHidden = true
|
||||
resourcesUnavailableReasonMenuItem.isHidden = true
|
||||
resourcesTitleMenuItem.title = resourceMenuTitle(store.resourceList)
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case .reasserting:
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.target = nil
|
||||
resourcesUnavailableReasonMenuItem.title = "No network connectivity"
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case .disconnecting:
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.isHidden = false
|
||||
resourcesUnavailableReasonMenuItem.target = nil
|
||||
resourcesUnavailableReasonMenuItem.title = "Disconnecting…"
|
||||
resourcesSeparatorMenuItem.isHidden = false
|
||||
case nil, .disconnected, .invalid:
|
||||
// We should never be in a state where the tunnel is
|
||||
// down but the user is signed in, but we have
|
||||
// code to handle it just for the sake of completion.
|
||||
resourcesTitleMenuItem.isHidden = true
|
||||
resourcesUnavailableMenuItem.isHidden = true
|
||||
resourcesUnavailableReasonMenuItem.isHidden = true
|
||||
resourcesUnavailableReasonMenuItem.title = "Disconnected"
|
||||
resourcesSeparatorMenuItem.isHidden = true
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +523,7 @@ import SwiftUI
|
||||
return false
|
||||
}
|
||||
|
||||
return wasInternetResourceEnabled != configuration.internetResourceEnabled
|
||||
return wasInternetResourceEnabled != store.configuration.internetResourceEnabled
|
||||
}
|
||||
|
||||
func refreshUpdateItem() {
|
||||
@@ -540,7 +560,7 @@ import SwiftUI
|
||||
|
||||
func internetResourceTitle(resource: Resource) -> String {
|
||||
let status =
|
||||
configuration.internetResourceEnabled ? StatusSymbol.enabled : StatusSymbol.disabled
|
||||
store.configuration.internetResourceEnabled ? StatusSymbol.enabled : StatusSymbol.disabled
|
||||
|
||||
return status + " " + resource.name
|
||||
}
|
||||
@@ -564,7 +584,7 @@ import SwiftUI
|
||||
}
|
||||
|
||||
func internetResourceToggleTitle() -> String {
|
||||
let isEnabled = configuration.internetResourceEnabled
|
||||
let isEnabled = store.configuration.internetResourceEnabled
|
||||
|
||||
return isEnabled ? "Disable this resource" : "Enable this resource"
|
||||
}
|
||||
@@ -767,13 +787,13 @@ import SwiftUI
|
||||
}
|
||||
|
||||
@objc func adminPortalButtonTapped() {
|
||||
guard let baseURL = URL(string: configuration.authURL)
|
||||
guard let baseURL = URL(string: store.configuration.authURL)
|
||||
else {
|
||||
Log.warning("Admin portal URL invalid: \(configuration.authURL)")
|
||||
Log.warning("Admin portal URL invalid: \(store.configuration.authURL)")
|
||||
return
|
||||
}
|
||||
|
||||
let accountSlug = configuration.accountSlug
|
||||
let accountSlug = store.configuration.accountSlug
|
||||
let authURL = baseURL.appendingPathComponent(accountSlug)
|
||||
|
||||
Task { await NSWorkspace.shared.openAsync(authURL) }
|
||||
@@ -791,7 +811,7 @@ import SwiftUI
|
||||
|
||||
@objc func supportButtonTapped() {
|
||||
let url =
|
||||
URL(string: configuration.supportURL) ?? URL(string: Configuration.defaultSupportURL)!
|
||||
URL(string: store.configuration.supportURL) ?? URL(string: Configuration.defaultSupportURL)!
|
||||
|
||||
Task { await NSWorkspace.shared.openAsync(url) }
|
||||
}
|
||||
@@ -812,7 +832,7 @@ import SwiftUI
|
||||
}
|
||||
|
||||
@objc func internetResourceToggle(_ sender: NSMenuItem) {
|
||||
configuration.internetResourceEnabled = !configuration.internetResourceEnabled
|
||||
store.configuration.internetResourceEnabled = !store.configuration.internetResourceEnabled
|
||||
|
||||
sender.title = internetResourceToggleTitle()
|
||||
}
|
||||
|
||||
@@ -17,28 +17,32 @@ import SwiftUI
|
||||
var body: some View {
|
||||
switch store.vpnStatus {
|
||||
case .connected:
|
||||
switch store.resourceList {
|
||||
case .loaded(let resources):
|
||||
if resources.isEmpty {
|
||||
Text("No Resources. Contact your admin to be granted access.")
|
||||
} else {
|
||||
List {
|
||||
if !store.favorites.isEmpty() {
|
||||
Section("Favorites") {
|
||||
ResourceSection(resources: favoriteResources())
|
||||
}
|
||||
if store.configuration.publishedHideResourceList {
|
||||
Text("Signed in as \(store.actorName)")
|
||||
} else {
|
||||
switch store.resourceList {
|
||||
case .loaded(let resources):
|
||||
if resources.isEmpty {
|
||||
Text("No Resources. Contact your admin to be granted access.")
|
||||
} else {
|
||||
List {
|
||||
if !store.favorites.isEmpty() {
|
||||
Section("Favorites") {
|
||||
ResourceSection(resources: favoriteResources())
|
||||
}
|
||||
|
||||
Section("Other Resources") {
|
||||
ResourceSection(resources: nonFavoriteResources())
|
||||
Section("Other Resources") {
|
||||
ResourceSection(resources: nonFavoriteResources())
|
||||
}
|
||||
} else {
|
||||
ResourceSection(resources: resources)
|
||||
}
|
||||
} else {
|
||||
ResourceSection(resources: resources)
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
case .loading:
|
||||
Text("Loading Resources...")
|
||||
}
|
||||
case .loading:
|
||||
Text("Loading Resources...")
|
||||
}
|
||||
case nil:
|
||||
Text("Loading VPN configurations from system settings…")
|
||||
|
||||
@@ -128,9 +128,9 @@ public struct SettingsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
public init(store: Store, configuration: Configuration? = nil) {
|
||||
public init(store: Store) {
|
||||
self.store = store
|
||||
self.configuration = configuration ?? Configuration.shared
|
||||
self.configuration = store.configuration
|
||||
_viewModel = StateObject(wrappedValue: SettingsViewModel())
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
<true/>
|
||||
|
||||
<!-- Whether to show or hide the admin portal link in the main menu. -->
|
||||
<key>hideResourceList</key>
|
||||
<true/>
|
||||
|
||||
<!-- Whether to show or hide the resource list in the main menu. -->
|
||||
<key>logFilter</key>
|
||||
<string>info</string>
|
||||
|
||||
|
||||
@@ -175,6 +175,16 @@ A profile can consist of payloads with different version numbers. For example, c
|
||||
<key>pfm_type</key>
|
||||
<string>boolean</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>pfm_description</key>
|
||||
<string>Hide the Resource List in the Firezone menu in the macOS menu bar. If unset, defaults to false.</string>
|
||||
<key>pfm_name</key>
|
||||
<string>hideResourceList</string>
|
||||
<key>pfm_title</key>
|
||||
<string>Hide resource list</string>
|
||||
<key>pfm_type</key>
|
||||
<string>boolean</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>pfm_description</key>
|
||||
<string>Try to connect to Firezone using the saved token and configuration when the client application starts. If the authentication token is expired, the client will start in a disconnected state. Setting this field will override the user's setting. If unset, defaults to false.</string>
|
||||
|
||||
@@ -80,6 +80,7 @@ managed configuration available and to which platforms they apply.
|
||||
| `disableUpdateCheck` | `Boolean` | `false` | Whether to disable the periodic update checker. The update checker is enabled by default for standalone macOS Clients. | macOS, iOS, Android | 1.5.0 |
|
||||
| `checkForUpdates` | `Boolean` | `false` | Enable or disable the periodic update checker. The update checker is enabled by default for Windows Clients. | Windows | 1.5.0 |
|
||||
| `hideAdminPortalMenuItem` | `Boolean` | `false` | Whether to show or hide the admin portal link in the main menu. | macOS, Windows | 1.5.0 |
|
||||
| `hideResourceList` | `Boolean` | `false` | Whether to show or hide the resource list in the main menu. | macOS, iOS | 1.5.10 |
|
||||
| `supportURL` | `String` | `https://www.firezone.dev/support` | The destination URL used for the support link in the main menu. | macOS, iOS, Windows | 1.5.0 |
|
||||
|
||||
### Applying managed configuration
|
||||
|
||||
@@ -32,6 +32,10 @@ export default function Apple() {
|
||||
Fixes an issue where the order of upstream / system DNS resolvers was
|
||||
not respected.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="10824">
|
||||
Adds support for <code>hideResourceList</code> managed configuration
|
||||
key to hide the Resource List in the macOS and iOS apps.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.5.9" date={new Date("2025-10-20")}>
|
||||
<ChangeItem pull="10603">
|
||||
|
||||
Reference in New Issue
Block a user