A Mac app for working with Mongo databases.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

QueryViewController.swift 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. //
  2. // QueryViewController.swift
  3. // MongoView
  4. //
  5. // Created by Shadowfacts on 1/9/20.
  6. // Copyright © 2020 Shadowfacts. All rights reserved.
  7. //
  8. import Cocoa
  9. import MongoSwift
  10. class QueryViewController: NSViewController {
  11. @IBOutlet weak var verticalSplitView: NSSplitView!
  12. @IBOutlet var filterTextView: JavaScriptEditorView!
  13. @IBOutlet weak var outlineView: NSOutlineView!
  14. @IBOutlet weak var documentCountLabel: NSTextField!
  15. let mongoController: MongoController
  16. let collection: DatabaseCollection
  17. var defaultFilter: String {
  18. "{}"
  19. }
  20. var hasFilterChanged: Bool {
  21. return filterTextView.string != defaultFilter
  22. }
  23. var mostRecentQuery: String? = nil
  24. var rootNodes: [Node] = []
  25. init(mongoController: MongoController, collection: DatabaseCollection) {
  26. self.mongoController = mongoController
  27. self.collection = collection
  28. super.init(nibName: "QueryViewController", bundle: .main)
  29. }
  30. required init?(coder: NSCoder) {
  31. fatalError("init(coder:) has not been implemented")
  32. }
  33. override func viewDidLoad() {
  34. super.viewDidLoad()
  35. refresh()
  36. verticalSplitView.delegate = self
  37. verticalSplitView.setHoldingPriority(.defaultHigh, forSubviewAt: 0)
  38. filterTextView.isAutomaticQuoteSubstitutionEnabled = false
  39. filterTextView.string = defaultFilter
  40. outlineView.dataSource = self
  41. outlineView.delegate = self
  42. outlineView.target = self
  43. outlineView.doubleAction = #selector(outlineCellDoubleClicked)
  44. }
  45. override func viewWillAppear() {
  46. super.viewWillAppear()
  47. verticalSplitView.setPosition(80, ofDividerAt: 0)
  48. }
  49. override func viewDidAppear() {
  50. super.viewDidAppear()
  51. view.window!.makeFirstResponder(outlineView)
  52. }
  53. func refresh(reload: Bool = true) {
  54. let filterText = filterTextView.string.trimmingCharacters(in: .whitespacesAndNewlines)
  55. let filter: Document
  56. if !filterText.isEmpty,
  57. let normalized = ExtendedJSON.normalize(filterText),
  58. let doc = try? Document(fromJSON: normalized) {
  59. filter = doc
  60. } else {
  61. filter = [:]
  62. }
  63. let documents = try! mongoController.collection(collection).find(filter).all()
  64. rootNodes = documents.map { Node(document: $0) }
  65. title = "\(self.collection.database).\(self.collection.name)"
  66. documentCountLabel.stringValue = "\(documents.count) document\(documents.count == 1 ? "" : "s")"
  67. if reload {
  68. outlineView.reloadData()
  69. }
  70. }
  71. func deleteRootNode(_ node: Node) {
  72. guard case let .document(doc) = node.value else { return }
  73. let alert = NSAlert()
  74. alert.alertStyle = .warning
  75. alert.messageText = "Confirm deletion"
  76. alert.informativeText = "Are you sure you want to delete the document"
  77. let id: ObjectId?
  78. if case let .objectId(docId) = doc["_id"] {
  79. id = docId
  80. alert.informativeText += " with id \(docId)"
  81. } else {
  82. id = nil
  83. }
  84. alert.addButton(withTitle: "Delete")
  85. alert.addButton(withTitle: "Cancel")
  86. alert.beginSheetModal(for: view.window!) { (response) in
  87. guard response == .alertFirstButtonReturn else { return }
  88. self.mongoController.collection(self.collection).deleteOne(doc).whenComplete { (result) in
  89. DispatchQueue.main.async {
  90. switch result {
  91. case let .success(result):
  92. guard let result = result, result.deletedCount == 1 else {
  93. let alert = NSAlert()
  94. alert.alertStyle = .critical
  95. alert.messageText = "Error deleting document"
  96. if let id = id {
  97. alert.informativeText = "The document with id \(id) could not be deleted."
  98. } else {
  99. alert.informativeText = "The document could not be deleted."
  100. }
  101. alert.beginSheetModal(for: self.view.window!, completionHandler: nil)
  102. return
  103. }
  104. self.refresh()
  105. case let .failure(error):
  106. let alert = NSAlert(error: error)
  107. alert.beginSheetModal(for: self.view.window!, completionHandler: nil)
  108. }
  109. }
  110. }
  111. }
  112. }
  113. @objc func outlineCellDoubleClicked() {
  114. if let item = outlineView.item(atRow: outlineView.clickedRow) {
  115. if outlineView.isItemExpanded(item) {
  116. outlineView.collapseItem(item)
  117. } else {
  118. outlineView.expandItem(item)
  119. }
  120. }
  121. }
  122. @IBAction func deleteNode(_ sender: Any) {
  123. guard let node = outlineView.item(atRow: outlineView.clickedRow) as? Node else {
  124. return
  125. }
  126. if node.parent == nil {
  127. deleteRootNode(node)
  128. }
  129. }
  130. }
  131. extension QueryViewController: NSMenuItemValidation {
  132. func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
  133. if menuItem.action == #selector(deleteNode(_:)) {
  134. if outlineView.clickedRow != -1, let node = outlineView.item(atRow: outlineView.clickedRow) as? Node, node.parent == nil {
  135. return true
  136. } else {
  137. return false
  138. }
  139. }
  140. return true
  141. }
  142. }
  143. extension QueryViewController: NSSplitViewDelegate {
  144. func splitView(_ splitView: NSSplitView, constrainSplitPosition proposedPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat {
  145. return max(80, min(splitView.bounds.height / 2, proposedPosition))
  146. }
  147. }
  148. extension QueryViewController: NSOutlineViewDataSource {
  149. func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
  150. if item == nil {
  151. return rootNodes.count
  152. } else if let node = item as? Node {
  153. return node.numberOfChildren
  154. } else {
  155. return 0
  156. }
  157. }
  158. func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
  159. if let node = item as? Node {
  160. return node.hasChildren
  161. } else {
  162. return false
  163. }
  164. }
  165. func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
  166. if item == nil {
  167. return rootNodes[index]
  168. } else if let node = item as? Node {
  169. return node.children[index]
  170. } else {
  171. fatalError("unreachable")
  172. }
  173. }
  174. }
  175. extension QueryViewController: NSOutlineViewDelegate {
  176. func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
  177. guard let tableColumn = tableColumn,
  178. let node = item as? Node else {
  179. fatalError()
  180. }
  181. if tableColumn.identifier == .fieldNameColumn {
  182. let cell = outlineView.makeView(withIdentifier: .fieldNameCell, owner: nil) as! NSTableCellView
  183. cell.textField!.stringValue = node.keyString
  184. cell.textField!.isEditable = false
  185. return cell
  186. } else if tableColumn.identifier == .fieldValueColumn {
  187. let cell = outlineView.makeView(withIdentifier: .fieldValueCell, owner: nil) as! NSTableCellView
  188. cell.textField!.stringValue = node.valueString
  189. cell.textField!.isEditable = false
  190. return cell
  191. } else if tableColumn.identifier == .valueTypeColumn {
  192. let cell = outlineView.makeView(withIdentifier: .valueTypeCell, owner: nil) as! NSTableCellView
  193. cell.textField!.stringValue = node.typeString
  194. cell.textField!.isEditable = false
  195. return cell
  196. } else {
  197. return nil
  198. }
  199. }
  200. }
  201. extension NSUserInterfaceItemIdentifier {
  202. static let fieldNameColumn = NSUserInterfaceItemIdentifier(rawValue: "FieldNameCol")
  203. static let fieldValueColumn = NSUserInterfaceItemIdentifier(rawValue: "FieldValueCol")
  204. static let fieldNameCell = NSUserInterfaceItemIdentifier(rawValue: "FieldNameCell")
  205. static let fieldValueCell = NSUserInterfaceItemIdentifier(rawValue: "FieldValueCell")
  206. static let valueTypeColumn = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCol")
  207. static let valueTypeCell = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCell")
  208. }