2019-01-02 19:46:27 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2023-02-14 15:10:32 +00:00
|
|
|
// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved.
|
2019-01-02 19:46:27 +00:00
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
|
|
|
|
class TunnelDetailTableViewController: NSViewController {
|
|
|
|
|
|
|
|
private enum TableViewModelRow {
|
|
|
|
case interfaceFieldRow(TunnelViewModel.InterfaceField)
|
|
|
|
case peerFieldRow(peer: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField)
|
2019-01-08 22:44:08 +00:00
|
|
|
case onDemandRow
|
2019-03-08 09:54:56 +00:00
|
|
|
case onDemandSSIDRow
|
2019-01-02 19:46:27 +00:00
|
|
|
case spacerRow
|
|
|
|
|
|
|
|
func localizedSectionKeyString() -> String {
|
|
|
|
switch self {
|
|
|
|
case .interfaceFieldRow: return tr("tunnelSectionTitleInterface")
|
|
|
|
case .peerFieldRow: return tr("tunnelSectionTitlePeer")
|
2019-03-08 09:54:56 +00:00
|
|
|
case .onDemandRow: return tr("macFieldOnDemand")
|
|
|
|
case .onDemandSSIDRow: return ""
|
2019-01-02 19:46:27 +00:00
|
|
|
case .spacerRow: return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func isTitleRow() -> Bool {
|
|
|
|
switch self {
|
|
|
|
case .interfaceFieldRow(let field): return field == .name
|
|
|
|
case .peerFieldRow(_, let field): return field == .publicKey
|
2019-01-08 22:44:08 +00:00
|
|
|
case .onDemandRow: return true
|
2019-03-08 09:54:56 +00:00
|
|
|
case .onDemandSSIDRow: return false
|
2019-01-02 19:46:27 +00:00
|
|
|
case .spacerRow: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-04 12:55:32 +00:00
|
|
|
static let interfaceFields: [TunnelViewModel.InterfaceField] = [
|
2019-03-16 10:55:17 +00:00
|
|
|
.name, .status, .publicKey, .addresses,
|
2019-03-17 11:08:07 +00:00
|
|
|
.listenPort, .mtu, .dns, .toggleStatus
|
2019-01-02 19:46:27 +00:00
|
|
|
]
|
|
|
|
|
2019-02-04 12:55:32 +00:00
|
|
|
static let peerFields: [TunnelViewModel.PeerField] = [
|
2019-01-02 19:46:27 +00:00
|
|
|
.publicKey, .preSharedKey, .endpoint,
|
2019-01-23 23:33:22 +00:00
|
|
|
.allowedIPs, .persistentKeepAlive,
|
|
|
|
.rxBytes, .txBytes, .lastHandshakeTime
|
2019-01-02 19:46:27 +00:00
|
|
|
]
|
|
|
|
|
2019-03-08 09:54:56 +00:00
|
|
|
static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
|
|
|
|
.onDemand, .ssid
|
|
|
|
]
|
|
|
|
|
2019-01-02 19:46:27 +00:00
|
|
|
let tableView: NSTableView = {
|
|
|
|
let tableView = NSTableView()
|
|
|
|
tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelDetail")))
|
|
|
|
tableView.headerView = nil
|
|
|
|
tableView.rowSizeStyle = .medium
|
|
|
|
tableView.backgroundColor = .clear
|
|
|
|
tableView.selectionHighlightStyle = .none
|
|
|
|
return tableView
|
|
|
|
}()
|
|
|
|
|
2019-01-03 13:40:31 +00:00
|
|
|
let editButton: NSButton = {
|
|
|
|
let button = NSButton()
|
2020-04-11 11:26:54 +00:00
|
|
|
button.title = tr("macButtonEdit")
|
2019-01-03 13:40:31 +00:00
|
|
|
button.setButtonType(.momentaryPushIn)
|
|
|
|
button.bezelStyle = .rounded
|
2019-03-17 14:49:25 +00:00
|
|
|
button.toolTip = tr("macToolTipEditTunnel")
|
2019-01-03 13:40:31 +00:00
|
|
|
return button
|
|
|
|
}()
|
|
|
|
|
2019-01-03 14:04:28 +00:00
|
|
|
let box: NSBox = {
|
|
|
|
let box = NSBox()
|
|
|
|
box.titlePosition = .noTitle
|
|
|
|
box.fillColor = .unemphasizedSelectedContentBackgroundColor
|
|
|
|
return box
|
|
|
|
}()
|
|
|
|
|
2019-01-02 19:46:27 +00:00
|
|
|
let tunnelsManager: TunnelsManager
|
|
|
|
let tunnel: TunnelContainer
|
2019-02-04 10:57:11 +00:00
|
|
|
|
2019-01-02 19:46:27 +00:00
|
|
|
var tunnelViewModel: TunnelViewModel {
|
2019-02-04 12:55:32 +00:00
|
|
|
didSet {
|
|
|
|
updateTableViewModelRowsBySection()
|
|
|
|
updateTableViewModelRows()
|
|
|
|
}
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
2019-03-08 09:54:56 +00:00
|
|
|
|
|
|
|
var onDemandViewModel: ActivateOnDemandViewModel
|
|
|
|
|
2019-02-04 12:55:32 +00:00
|
|
|
private var tableViewModelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
|
2019-01-02 19:46:27 +00:00
|
|
|
private var tableViewModelRows = [TableViewModelRow]()
|
2019-02-04 10:57:11 +00:00
|
|
|
|
2019-01-03 13:40:31 +00:00
|
|
|
private var statusObservationToken: AnyObject?
|
2019-01-22 14:00:06 +00:00
|
|
|
private var tunnelEditVC: TunnelEditViewController?
|
2019-01-31 09:13:37 +00:00
|
|
|
private var reloadRuntimeConfigurationTimer: Timer?
|
2019-01-02 19:46:27 +00:00
|
|
|
|
|
|
|
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
|
|
|
|
self.tunnelsManager = tunnelsManager
|
|
|
|
self.tunnel = tunnel
|
|
|
|
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
2019-03-11 12:39:48 +00:00
|
|
|
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
|
2019-01-02 19:46:27 +00:00
|
|
|
super.init(nibName: nil, bundle: nil)
|
2019-02-04 12:55:32 +00:00
|
|
|
updateTableViewModelRowsBySection()
|
2019-01-02 19:46:27 +00:00
|
|
|
updateTableViewModelRows()
|
2019-01-03 13:40:31 +00:00
|
|
|
statusObservationToken = tunnel.observe(\TunnelContainer.status) { [weak self] _, _ in
|
2019-05-27 09:11:56 +00:00
|
|
|
guard let self = self else { return }
|
|
|
|
if tunnel.status == .active {
|
|
|
|
self.startUpdatingRuntimeConfiguration()
|
|
|
|
} else if tunnel.status == .inactive {
|
|
|
|
self.reloadRuntimeConfiguration()
|
|
|
|
self.stopUpdatingRuntimeConfiguration()
|
|
|
|
}
|
2019-01-03 13:40:31 +00:00
|
|
|
}
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
override func loadView() {
|
|
|
|
tableView.dataSource = self
|
|
|
|
tableView.delegate = self
|
|
|
|
|
2019-01-03 13:40:31 +00:00
|
|
|
editButton.target = self
|
2019-01-16 19:11:32 +00:00
|
|
|
editButton.action = #selector(handleEditTunnelAction)
|
2019-01-03 13:40:31 +00:00
|
|
|
|
2019-01-02 19:46:27 +00:00
|
|
|
let clipView = NSClipView()
|
|
|
|
clipView.documentView = tableView
|
|
|
|
|
|
|
|
let scrollView = NSScrollView()
|
|
|
|
scrollView.contentView = clipView // Set contentView before setting drawsBackground
|
|
|
|
scrollView.drawsBackground = false
|
|
|
|
scrollView.hasVerticalScroller = true
|
|
|
|
scrollView.autohidesScrollers = true
|
|
|
|
|
2019-01-03 13:40:31 +00:00
|
|
|
let containerView = NSView()
|
|
|
|
let bottomControlsContainer = NSLayoutGuide()
|
|
|
|
containerView.addLayoutGuide(bottomControlsContainer)
|
2019-01-03 14:04:28 +00:00
|
|
|
containerView.addSubview(box)
|
2019-01-03 13:40:31 +00:00
|
|
|
containerView.addSubview(scrollView)
|
|
|
|
containerView.addSubview(editButton)
|
2019-01-03 14:04:28 +00:00
|
|
|
box.translatesAutoresizingMaskIntoConstraints = false
|
2019-01-03 13:40:31 +00:00
|
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
editButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
|
|
|
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
|
|
|
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
|
|
|
containerView.leadingAnchor.constraint(equalTo: bottomControlsContainer.leadingAnchor),
|
|
|
|
containerView.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
|
2019-01-03 14:04:50 +00:00
|
|
|
bottomControlsContainer.heightAnchor.constraint(equalToConstant: 32),
|
2019-01-03 13:40:31 +00:00
|
|
|
scrollView.bottomAnchor.constraint(equalTo: bottomControlsContainer.topAnchor),
|
|
|
|
bottomControlsContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
|
|
|
editButton.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
|
2019-03-16 20:56:56 +00:00
|
|
|
bottomControlsContainer.bottomAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 0)
|
2019-01-03 13:40:31 +00:00
|
|
|
])
|
|
|
|
|
2019-01-03 14:04:28 +00:00
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
scrollView.topAnchor.constraint(equalTo: box.topAnchor),
|
|
|
|
scrollView.bottomAnchor.constraint(equalTo: box.bottomAnchor),
|
|
|
|
scrollView.leadingAnchor.constraint(equalTo: box.leadingAnchor),
|
|
|
|
scrollView.trailingAnchor.constraint(equalTo: box.trailingAnchor)
|
|
|
|
])
|
|
|
|
|
2019-02-15 10:14:06 +00:00
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 320),
|
|
|
|
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
|
|
|
|
])
|
|
|
|
|
2019-01-03 13:40:31 +00:00
|
|
|
view = containerView
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
|
|
|
|
2019-02-04 10:57:11 +00:00
|
|
|
func updateTableViewModelRowsBySection() {
|
|
|
|
var modelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
|
|
|
|
|
|
|
|
var interfaceSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
|
2019-02-04 12:55:32 +00:00
|
|
|
for field in TunnelDetailTableViewController.interfaceFields {
|
2019-03-17 11:08:07 +00:00
|
|
|
let isStatus = field == .status || field == .toggleStatus
|
2019-03-16 10:55:17 +00:00
|
|
|
let isEmpty = tunnelViewModel.interfaceData[field].isEmpty
|
|
|
|
interfaceSection.append((isVisible: isStatus || !isEmpty, modelRow: .interfaceFieldRow(field)))
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
2019-02-04 10:57:11 +00:00
|
|
|
interfaceSection.append((isVisible: true, modelRow: .spacerRow))
|
|
|
|
modelRowsBySection.append(interfaceSection)
|
|
|
|
|
2019-01-02 19:46:27 +00:00
|
|
|
for peerData in tunnelViewModel.peersData {
|
2019-02-04 10:57:11 +00:00
|
|
|
var peerSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
|
2019-02-04 12:55:32 +00:00
|
|
|
for field in TunnelDetailTableViewController.peerFields {
|
2019-02-04 10:57:11 +00:00
|
|
|
peerSection.append((isVisible: !peerData[field].isEmpty, modelRow: .peerFieldRow(peer: peerData, field: field)))
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
2019-02-04 10:57:11 +00:00
|
|
|
peerSection.append((isVisible: true, modelRow: .spacerRow))
|
|
|
|
modelRowsBySection.append(peerSection)
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
2019-02-04 10:57:11 +00:00
|
|
|
|
|
|
|
var onDemandSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
|
|
|
|
onDemandSection.append((isVisible: true, modelRow: .onDemandRow))
|
2019-03-08 09:54:56 +00:00
|
|
|
if onDemandViewModel.isWiFiInterfaceEnabled {
|
|
|
|
onDemandSection.append((isVisible: true, modelRow: .onDemandSSIDRow))
|
|
|
|
}
|
2019-02-04 10:57:11 +00:00
|
|
|
modelRowsBySection.append(onDemandSection)
|
|
|
|
|
|
|
|
tableViewModelRowsBySection = modelRowsBySection
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateTableViewModelRows() {
|
|
|
|
tableViewModelRows = tableViewModelRowsBySection.flatMap { $0.filter { $0.isVisible }.map { $0.modelRow } }
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
2019-01-03 13:40:31 +00:00
|
|
|
|
2019-01-16 19:11:32 +00:00
|
|
|
@objc func handleEditTunnelAction() {
|
2019-02-06 02:23:51 +00:00
|
|
|
PrivateDataConfirmation.confirmAccess(to: tr("macViewPrivateData")) { [weak self] in
|
|
|
|
guard let self = self else { return }
|
|
|
|
let tunnelEditVC = TunnelEditViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
|
|
|
|
tunnelEditVC.delegate = self
|
|
|
|
self.presentAsSheet(tunnelEditVC)
|
|
|
|
self.tunnelEditVC = tunnelEditVC
|
|
|
|
}
|
2019-01-03 13:40:31 +00:00
|
|
|
}
|
|
|
|
|
2019-01-16 20:14:40 +00:00
|
|
|
@objc func handleToggleActiveStatusAction() {
|
2021-09-22 00:40:49 +00:00
|
|
|
if tunnel.hasOnDemandRules {
|
|
|
|
let turnOn = !tunnel.isActivateOnDemandEnabled
|
|
|
|
tunnelsManager.setOnDemandEnabled(turnOn, on: tunnel) { error in
|
2021-07-30 05:34:38 +00:00
|
|
|
if error == nil && !turnOn {
|
|
|
|
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if tunnel.status == .inactive {
|
|
|
|
tunnelsManager.startActivation(of: tunnel)
|
|
|
|
} else if tunnel.status == .active {
|
|
|
|
tunnelsManager.startDeactivation(of: tunnel)
|
|
|
|
}
|
2019-01-16 20:14:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-26 14:34:02 +00:00
|
|
|
override func viewWillAppear() {
|
|
|
|
if tunnel.status == .active {
|
|
|
|
startUpdatingRuntimeConfiguration()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-22 14:00:06 +00:00
|
|
|
override func viewWillDisappear() {
|
|
|
|
super.viewWillDisappear()
|
|
|
|
if let tunnelEditVC = tunnelEditVC {
|
|
|
|
dismiss(tunnelEditVC)
|
|
|
|
}
|
2019-01-31 09:13:37 +00:00
|
|
|
stopUpdatingRuntimeConfiguration()
|
2019-01-22 14:00:06 +00:00
|
|
|
}
|
2019-01-23 23:33:22 +00:00
|
|
|
|
2019-02-04 12:55:32 +00:00
|
|
|
func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
|
|
|
|
// Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
|
2019-02-09 21:37:30 +00:00
|
|
|
|
|
|
|
let tableView = self.tableView
|
|
|
|
|
|
|
|
func handleSectionFieldsModified<T>(fields: [T], modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
|
|
|
var modifiedRowIndices = IndexSet()
|
|
|
|
for (index, field) in fields.enumerated() {
|
|
|
|
guard let change = changes[field] else { continue }
|
2020-12-09 13:35:21 +00:00
|
|
|
if case .modified = change {
|
2019-02-09 21:37:30 +00:00
|
|
|
let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
|
|
|
|
modifiedRowIndices.insert(rowOffset + row)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !modifiedRowIndices.isEmpty {
|
|
|
|
tableView.reloadData(forRowIndexes: modifiedRowIndices, columnIndexes: IndexSet(integer: 0))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleSectionFieldsAddedOrRemoved<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
2019-02-04 12:55:32 +00:00
|
|
|
for (index, field) in fields.enumerated() {
|
|
|
|
guard let change = changes[field] else { continue }
|
|
|
|
let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
|
|
|
|
switch change {
|
|
|
|
case .added:
|
|
|
|
tableView.insertRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
|
|
|
|
modelRowsInSection[index].isVisible = true
|
|
|
|
case .removed:
|
|
|
|
tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
|
|
|
|
modelRowsInSection[index].isVisible = false
|
|
|
|
case .modified:
|
2019-02-09 21:37:30 +00:00
|
|
|
break
|
2019-02-04 12:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-09 21:37:30 +00:00
|
|
|
let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
|
|
|
|
|
|
|
|
if !changes.interfaceChanges.isEmpty {
|
|
|
|
handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields,
|
|
|
|
modelRowsInSection: self.tableViewModelRowsBySection[0],
|
|
|
|
rowOffset: 0, changes: changes.interfaceChanges)
|
|
|
|
}
|
|
|
|
for (peerIndex, peerChanges) in changes.peerChanges {
|
|
|
|
let sectionIndex = 1 + peerIndex
|
|
|
|
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
|
|
|
|
handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields,
|
|
|
|
modelRowsInSection: self.tableViewModelRowsBySection[sectionIndex],
|
|
|
|
rowOffset: rowOffset, changes: peerChanges)
|
|
|
|
}
|
|
|
|
|
|
|
|
let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
|
|
|
|
let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
|
|
|
|
|
|
|
|
if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !changes.peersRemovedIndices.isEmpty || !changes.peersInsertedIndices.isEmpty {
|
|
|
|
tableView.beginUpdates()
|
|
|
|
if isAnyInterfaceFieldAddedOrRemoved {
|
|
|
|
handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields,
|
|
|
|
modelRowsInSection: &self.tableViewModelRowsBySection[0],
|
|
|
|
rowOffset: 0, changes: changes.interfaceChanges)
|
|
|
|
}
|
|
|
|
if isAnyPeerFieldAddedOrRemoved {
|
|
|
|
for (peerIndex, peerChanges) in changes.peerChanges {
|
|
|
|
let sectionIndex = 1 + peerIndex
|
|
|
|
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
|
|
|
|
handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex], rowOffset: rowOffset, changes: peerChanges)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !changes.peersRemovedIndices.isEmpty {
|
|
|
|
for peerIndex in changes.peersRemovedIndices {
|
2019-02-04 12:55:32 +00:00
|
|
|
let sectionIndex = 1 + peerIndex
|
|
|
|
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
|
|
|
|
let count = self.tableViewModelRowsBySection[sectionIndex].filter { $0.isVisible }.count
|
|
|
|
self.tableView.removeRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
|
|
|
|
self.tableViewModelRowsBySection.remove(at: sectionIndex)
|
|
|
|
}
|
2019-02-09 21:37:30 +00:00
|
|
|
}
|
|
|
|
if !changes.peersInsertedIndices.isEmpty {
|
|
|
|
for peerIndex in changes.peersInsertedIndices {
|
2019-02-04 12:55:32 +00:00
|
|
|
let peerData = self.tunnelViewModel.peersData[peerIndex]
|
|
|
|
let sectionIndex = 1 + peerIndex
|
|
|
|
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
|
|
|
|
var modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)] = TunnelDetailTableViewController.peerFields.map {
|
|
|
|
(isVisible: !peerData[$0].isEmpty, modelRow: .peerFieldRow(peer: peerData, field: $0))
|
|
|
|
}
|
|
|
|
modelRowsInSection.append((isVisible: true, modelRow: .spacerRow))
|
|
|
|
let count = modelRowsInSection.filter { $0.isVisible }.count
|
|
|
|
self.tableView.insertRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
|
|
|
|
self.tableViewModelRowsBySection.insert(modelRowsInSection, at: sectionIndex)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
updateTableViewModelRows()
|
2019-02-09 21:37:30 +00:00
|
|
|
tableView.endUpdates()
|
2019-02-04 12:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-23 23:33:22 +00:00
|
|
|
private func reloadRuntimeConfiguration() {
|
2019-02-04 12:55:32 +00:00
|
|
|
tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
|
|
|
|
guard let tunnelConfiguration = tunnelConfiguration else { return }
|
|
|
|
self?.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
|
|
|
|
}
|
2019-01-23 23:33:22 +00:00
|
|
|
}
|
2019-01-31 09:13:37 +00:00
|
|
|
|
|
|
|
func startUpdatingRuntimeConfiguration() {
|
|
|
|
reloadRuntimeConfiguration()
|
|
|
|
reloadRuntimeConfigurationTimer?.invalidate()
|
|
|
|
let reloadTimer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
|
|
|
|
self?.reloadRuntimeConfiguration()
|
|
|
|
}
|
|
|
|
reloadRuntimeConfigurationTimer = reloadTimer
|
2019-01-31 11:34:34 +00:00
|
|
|
RunLoop.main.add(reloadTimer, forMode: .common)
|
2019-01-31 09:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func stopUpdatingRuntimeConfiguration() {
|
|
|
|
reloadRuntimeConfiguration()
|
|
|
|
reloadRuntimeConfigurationTimer?.invalidate()
|
|
|
|
reloadRuntimeConfigurationTimer = nil
|
|
|
|
}
|
|
|
|
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
extension TunnelDetailTableViewController: NSTableViewDataSource {
|
|
|
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
|
|
return tableViewModelRows.count
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension TunnelDetailTableViewController: NSTableViewDelegate {
|
|
|
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
|
|
|
let modelRow = tableViewModelRows[row]
|
|
|
|
switch modelRow {
|
|
|
|
case .interfaceFieldRow(let field):
|
2019-03-16 10:55:17 +00:00
|
|
|
if field == .status {
|
|
|
|
return statusCell()
|
2019-03-17 11:08:07 +00:00
|
|
|
} else if field == .toggleStatus {
|
|
|
|
return toggleStatusCell()
|
2019-03-16 10:55:17 +00:00
|
|
|
} else {
|
|
|
|
let cell: KeyValueRow = tableView.dequeueReusableCell()
|
|
|
|
let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
|
|
|
|
cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
|
|
|
|
cell.value = tunnelViewModel.interfaceData[field]
|
|
|
|
cell.isKeyInBold = modelRow.isTitleRow()
|
|
|
|
return cell
|
|
|
|
}
|
2019-01-02 19:46:27 +00:00
|
|
|
case .peerFieldRow(let peerData, let field):
|
2019-01-07 07:34:50 +00:00
|
|
|
let cell: KeyValueRow = tableView.dequeueReusableCell()
|
2019-01-02 19:46:27 +00:00
|
|
|
let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
|
2019-01-06 13:21:06 +00:00
|
|
|
cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
|
2019-01-31 12:47:46 +00:00
|
|
|
if field == .persistentKeepAlive {
|
|
|
|
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
|
2019-02-12 13:55:42 +00:00
|
|
|
} else if field == .preSharedKey {
|
|
|
|
cell.value = tr("tunnelPeerPresharedKeyEnabled")
|
2019-01-31 12:47:46 +00:00
|
|
|
} else {
|
|
|
|
cell.value = peerData[field]
|
|
|
|
}
|
2019-01-02 19:46:27 +00:00
|
|
|
cell.isKeyInBold = modelRow.isTitleRow()
|
|
|
|
return cell
|
|
|
|
case .spacerRow:
|
|
|
|
return NSView()
|
2019-01-08 22:44:08 +00:00
|
|
|
case .onDemandRow:
|
|
|
|
let cell: KeyValueRow = tableView.dequeueReusableCell()
|
2019-03-08 09:54:56 +00:00
|
|
|
cell.key = modelRow.localizedSectionKeyString()
|
|
|
|
cell.value = onDemandViewModel.localizedInterfaceDescription
|
2019-01-08 22:44:08 +00:00
|
|
|
cell.isKeyInBold = true
|
|
|
|
return cell
|
2019-03-08 09:54:56 +00:00
|
|
|
case .onDemandSSIDRow:
|
|
|
|
let cell: KeyValueRow = tableView.dequeueReusableCell()
|
|
|
|
cell.key = tr("macFieldOnDemandSSIDs")
|
2019-03-09 10:35:22 +00:00
|
|
|
let value: String
|
|
|
|
if onDemandViewModel.ssidOption == .anySSID {
|
|
|
|
value = onDemandViewModel.ssidOption.localizedUIString
|
|
|
|
} else {
|
|
|
|
value = tr(format: "tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)",
|
|
|
|
onDemandViewModel.ssidOption.localizedUIString,
|
|
|
|
onDemandViewModel.selectedSSIDs.joined(separator: ", "))
|
|
|
|
}
|
|
|
|
cell.value = value
|
2019-03-08 09:54:56 +00:00
|
|
|
cell.isKeyInBold = false
|
|
|
|
return cell
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-16 10:55:17 +00:00
|
|
|
|
|
|
|
func statusCell() -> NSView {
|
|
|
|
let cell: KeyValueImageRow = tableView.dequeueReusableCell()
|
|
|
|
cell.key = tr(format: "macFieldKey (%@)", tr("tunnelInterfaceStatus"))
|
2021-07-29 10:27:04 +00:00
|
|
|
cell.value = TunnelDetailTableViewController.localizedStatusDescription(for: tunnel)
|
|
|
|
cell.valueImage = TunnelDetailTableViewController.image(for: tunnel)
|
|
|
|
let changeHandler: (TunnelContainer, Any) -> Void = { [weak cell] tunnel, _ in
|
2019-03-16 10:55:17 +00:00
|
|
|
guard let cell = cell else { return }
|
2021-07-29 10:27:04 +00:00
|
|
|
cell.value = TunnelDetailTableViewController.localizedStatusDescription(for: tunnel)
|
|
|
|
cell.valueImage = TunnelDetailTableViewController.image(for: tunnel)
|
2019-03-16 10:55:17 +00:00
|
|
|
}
|
2021-07-29 10:27:04 +00:00
|
|
|
cell.statusObservationToken = tunnel.observe(\.status, changeHandler: changeHandler)
|
|
|
|
cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled, changeHandler: changeHandler)
|
|
|
|
cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules, changeHandler: changeHandler)
|
2019-03-16 10:55:17 +00:00
|
|
|
return cell
|
|
|
|
}
|
|
|
|
|
2019-03-17 11:08:07 +00:00
|
|
|
func toggleStatusCell() -> NSView {
|
|
|
|
let cell: ButtonRow = tableView.dequeueReusableCell()
|
2021-07-30 05:34:38 +00:00
|
|
|
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(for: tunnel)
|
|
|
|
cell.isButtonEnabled = (tunnel.hasOnDemandRules || tunnel.status == .active || tunnel.status == .inactive)
|
2019-03-17 14:49:25 +00:00
|
|
|
cell.buttonToolTip = tr("macToolTipToggleStatus")
|
2019-03-17 11:08:07 +00:00
|
|
|
cell.onButtonClicked = { [weak self] in
|
|
|
|
self?.handleToggleActiveStatusAction()
|
|
|
|
}
|
2021-07-30 05:34:38 +00:00
|
|
|
let changeHandler: (TunnelContainer, Any) -> Void = { [weak cell] tunnel, _ in
|
2019-03-17 11:08:07 +00:00
|
|
|
guard let cell = cell else { return }
|
2021-07-30 05:34:38 +00:00
|
|
|
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(for: tunnel)
|
|
|
|
cell.isButtonEnabled = (tunnel.hasOnDemandRules || tunnel.status == .active || tunnel.status == .inactive)
|
2019-03-17 11:08:07 +00:00
|
|
|
}
|
2021-07-30 05:34:38 +00:00
|
|
|
cell.statusObservationToken = tunnel.observe(\.status, changeHandler: changeHandler)
|
|
|
|
cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled, changeHandler: changeHandler)
|
|
|
|
cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules, changeHandler: changeHandler)
|
2019-03-17 11:08:07 +00:00
|
|
|
return cell
|
|
|
|
}
|
|
|
|
|
2021-07-29 10:27:04 +00:00
|
|
|
private static func localizedStatusDescription(for tunnel: TunnelContainer) -> String {
|
|
|
|
let status = tunnel.status
|
|
|
|
let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
|
|
|
|
|
|
|
|
var text: String
|
2019-03-16 10:55:17 +00:00
|
|
|
switch status {
|
|
|
|
case .inactive:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusInactive")
|
2019-03-16 10:55:17 +00:00
|
|
|
case .activating:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusActivating")
|
2019-03-16 10:55:17 +00:00
|
|
|
case .active:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusActive")
|
2019-03-16 10:55:17 +00:00
|
|
|
case .deactivating:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusDeactivating")
|
2019-03-16 10:55:17 +00:00
|
|
|
case .reasserting:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusReasserting")
|
2019-03-16 10:55:17 +00:00
|
|
|
case .restarting:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusRestarting")
|
2019-03-16 10:55:17 +00:00
|
|
|
case .waiting:
|
2021-07-29 10:27:04 +00:00
|
|
|
text = tr("tunnelStatusWaiting")
|
|
|
|
}
|
|
|
|
|
|
|
|
if tunnel.hasOnDemandRules {
|
|
|
|
text += isOnDemandEngaged ?
|
|
|
|
tr("tunnelStatusAddendumOnDemandEnabled") : tr("tunnelStatusAddendumOnDemandDisabled")
|
2019-03-16 10:55:17 +00:00
|
|
|
}
|
2021-07-29 10:27:04 +00:00
|
|
|
|
|
|
|
return text
|
2019-03-16 10:55:17 +00:00
|
|
|
}
|
|
|
|
|
2021-07-29 10:27:04 +00:00
|
|
|
private static func image(for tunnel: TunnelContainer?) -> NSImage? {
|
|
|
|
guard let tunnel = tunnel else { return nil }
|
|
|
|
switch tunnel.status {
|
2019-03-16 10:55:17 +00:00
|
|
|
case .active, .restarting, .reasserting:
|
|
|
|
return NSImage(named: NSImage.statusAvailableName)
|
|
|
|
case .activating, .waiting, .deactivating:
|
|
|
|
return NSImage(named: NSImage.statusPartiallyAvailableName)
|
|
|
|
case .inactive:
|
2021-07-29 10:27:04 +00:00
|
|
|
if tunnel.isActivateOnDemandEnabled {
|
|
|
|
return NSImage(named: NSImage.Name.statusOnDemandEnabled)
|
|
|
|
} else {
|
|
|
|
return NSImage(named: NSImage.statusNoneName)
|
|
|
|
}
|
2019-03-16 10:55:17 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-17 11:08:07 +00:00
|
|
|
|
2021-07-30 05:34:38 +00:00
|
|
|
private static func localizedToggleStatusActionText(for tunnel: TunnelContainer) -> String {
|
|
|
|
if tunnel.hasOnDemandRules {
|
|
|
|
let turnOn = !tunnel.isActivateOnDemandEnabled
|
|
|
|
if turnOn {
|
|
|
|
return tr("macToggleStatusButtonEnableOnDemand")
|
|
|
|
} else {
|
|
|
|
if tunnel.status == .active {
|
|
|
|
return tr("macToggleStatusButtonDisableOnDemandDeactivate")
|
|
|
|
} else {
|
|
|
|
return tr("macToggleStatusButtonDisableOnDemand")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch tunnel.status {
|
|
|
|
case .waiting:
|
|
|
|
return tr("macToggleStatusButtonWaiting")
|
|
|
|
case .inactive:
|
|
|
|
return tr("macToggleStatusButtonActivate")
|
|
|
|
case .activating:
|
|
|
|
return tr("macToggleStatusButtonActivating")
|
|
|
|
case .active:
|
|
|
|
return tr("macToggleStatusButtonDeactivate")
|
|
|
|
case .deactivating:
|
|
|
|
return tr("macToggleStatusButtonDeactivating")
|
|
|
|
case .reasserting:
|
|
|
|
return tr("macToggleStatusButtonReasserting")
|
|
|
|
case .restarting:
|
|
|
|
return tr("macToggleStatusButtonRestarting")
|
|
|
|
}
|
2019-03-17 11:08:07 +00:00
|
|
|
}
|
|
|
|
}
|
2019-01-02 19:46:27 +00:00
|
|
|
}
|
2019-01-08 19:22:11 +00:00
|
|
|
|
|
|
|
extension TunnelDetailTableViewController: TunnelEditViewControllerDelegate {
|
|
|
|
func tunnelSaved(tunnel: TunnelContainer) {
|
|
|
|
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
|
2019-03-11 12:39:48 +00:00
|
|
|
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
|
2019-02-05 10:45:43 +00:00
|
|
|
updateTableViewModelRowsBySection()
|
|
|
|
updateTableViewModelRows()
|
2019-01-08 19:22:11 +00:00
|
|
|
tableView.reloadData()
|
2019-01-23 13:56:35 +00:00
|
|
|
self.tunnelEditVC = nil
|
2019-01-08 19:22:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func tunnelEditingCancelled() {
|
2019-01-22 14:00:06 +00:00
|
|
|
self.tunnelEditVC = nil
|
2019-01-08 19:22:11 +00:00
|
|
|
}
|
|
|
|
}
|