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.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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 queryTextView: JavaScriptEditorView!
  13. @IBOutlet weak var outlineView: NSOutlineView!
  14. @IBOutlet weak var documentCountLabel: NSTextField!
  15. let mongoController: MongoController
  16. let collection: DatabaseCollection
  17. var defaultQuery: String {
  18. "db.getCollection('\(collection.name)').find({}).toArray()"
  19. }
  20. var hasQueryChanged: Bool {
  21. return queryTextView.string != defaultQuery
  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. queryTextView.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
  39. queryTextView.isAutomaticQuoteSubstitutionEnabled = false
  40. queryTextView.string = defaultQuery
  41. outlineView.dataSource = self
  42. outlineView.delegate = self
  43. outlineView.target = self
  44. outlineView.doubleAction = #selector(outlineCellDoubleClicked)
  45. }
  46. override func viewWillAppear() {
  47. super.viewWillAppear()
  48. verticalSplitView.setPosition(80, ofDividerAt: 0)
  49. }
  50. override func viewDidAppear() {
  51. super.viewDidAppear()
  52. view.window!.makeFirstResponder(outlineView)
  53. }
  54. func refresh(reload: Bool = true) {
  55. if let query = mostRecentQuery {
  56. let connStr = "\(mongoController.connectionString)/\(collection.database)"
  57. rootNodes = MongoEvaluator.eval(command: query, connectingTo: connStr).map {
  58. Node(value: $0)
  59. }
  60. title = query
  61. documentCountLabel.stringValue = "\(rootNodes.count) result\(rootNodes.count == 1 ? "" : "s")"
  62. } else {
  63. let documents = try! mongoController.collection(collection).find().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. }
  68. if reload {
  69. outlineView.reloadData()
  70. }
  71. }
  72. func runQuery() {
  73. mostRecentQuery = queryTextView.string
  74. refresh()
  75. }
  76. func deleteRootNode(_ node: Node) {
  77. guard case let .document(doc) = node.value else { return }
  78. let alert = NSAlert()
  79. alert.alertStyle = .warning
  80. alert.messageText = "Confirm deletion"
  81. alert.informativeText = "Are you sure you want to delete the document"
  82. let id: ObjectId?
  83. if case let .objectId(docId) = doc["_id"] {
  84. id = docId
  85. alert.informativeText += " with id \(docId)"
  86. } else {
  87. id = nil
  88. }
  89. alert.addButton(withTitle: "Delete")
  90. alert.addButton(withTitle: "Cancel")
  91. alert.beginSheetModal(for: view.window!) { (response) in
  92. guard response == .alertFirstButtonReturn else { return }
  93. self.mongoController.collection(self.collection).deleteOne(doc).whenComplete { (result) in
  94. DispatchQueue.main.async {
  95. switch result {
  96. case let .success(result):
  97. guard let result = result, result.deletedCount == 1 else {
  98. let alert = NSAlert()
  99. alert.alertStyle = .critical
  100. alert.messageText = "Error deleting document"
  101. if let id = id {
  102. alert.informativeText = "The document with id \(id) could not be deleted."
  103. } else {
  104. alert.informativeText = "The document could not be deleted."
  105. }
  106. alert.beginSheetModal(for: self.view.window!, completionHandler: nil)
  107. return
  108. }
  109. self.refresh()
  110. case let .failure(error):
  111. let alert = NSAlert(error: error)
  112. alert.beginSheetModal(for: self.view.window!, completionHandler: nil)
  113. }
  114. }
  115. }
  116. }
  117. }
  118. @objc func outlineCellDoubleClicked() {
  119. if let item = outlineView.item(atRow: outlineView.clickedRow) {
  120. if outlineView.isItemExpanded(item) {
  121. outlineView.collapseItem(item)
  122. } else {
  123. outlineView.expandItem(item)
  124. }
  125. }
  126. }
  127. @IBAction func deleteNode(_ sender: Any) {
  128. guard let node = outlineView.item(atRow: outlineView.clickedRow) as? Node else {
  129. return
  130. }
  131. if node.parent == nil {
  132. deleteRootNode(node)
  133. }
  134. }
  135. }
  136. extension QueryViewController: NSMenuItemValidation {
  137. func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
  138. if menuItem.action == #selector(deleteNode(_:)) {
  139. if outlineView.clickedRow != -1, let node = outlineView.item(atRow: outlineView.clickedRow) as? Node, node.parent == nil {
  140. return true
  141. } else {
  142. return false
  143. }
  144. }
  145. return true
  146. }
  147. }
  148. extension QueryViewController: NSSplitViewDelegate {
  149. func splitView(_ splitView: NSSplitView, constrainSplitPosition proposedPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat {
  150. return max(80, min(splitView.bounds.height / 2, proposedPosition))
  151. }
  152. }
  153. extension QueryViewController: NSOutlineViewDataSource {
  154. func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
  155. if item == nil {
  156. return rootNodes.count
  157. } else if let node = item as? Node {
  158. return node.numberOfChildren
  159. } else {
  160. return 0
  161. }
  162. }
  163. func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
  164. if let node = item as? Node {
  165. return node.hasChildren
  166. } else {
  167. return false
  168. }
  169. }
  170. func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
  171. if item == nil {
  172. return rootNodes[index]
  173. } else if let node = item as? Node {
  174. return node.children[index]
  175. } else {
  176. fatalError("unreachable")
  177. }
  178. }
  179. }
  180. extension QueryViewController: NSOutlineViewDelegate {
  181. func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
  182. guard let tableColumn = tableColumn,
  183. let node = item as? Node else {
  184. fatalError()
  185. }
  186. if tableColumn.identifier == .fieldNameColumn {
  187. let cell = outlineView.makeView(withIdentifier: .fieldNameCell, owner: nil) as! NSTableCellView
  188. cell.textField!.stringValue = node.keyString
  189. cell.textField!.isEditable = false
  190. return cell
  191. } else if tableColumn.identifier == .fieldValueColumn {
  192. let cell = outlineView.makeView(withIdentifier: .fieldValueCell, owner: nil) as! NSTableCellView
  193. cell.textField!.stringValue = node.valueString
  194. cell.textField!.isEditable = false
  195. return cell
  196. } else if tableColumn.identifier == .valueTypeColumn {
  197. let cell = outlineView.makeView(withIdentifier: .valueTypeCell, owner: nil) as! NSTableCellView
  198. cell.textField!.stringValue = node.typeString
  199. cell.textField!.isEditable = false
  200. return cell
  201. } else {
  202. return nil
  203. }
  204. }
  205. }
  206. extension NSUserInterfaceItemIdentifier {
  207. static let fieldNameColumn = NSUserInterfaceItemIdentifier(rawValue: "FieldNameCol")
  208. static let fieldValueColumn = NSUserInterfaceItemIdentifier(rawValue: "FieldValueCol")
  209. static let fieldNameCell = NSUserInterfaceItemIdentifier(rawValue: "FieldNameCell")
  210. static let fieldValueCell = NSUserInterfaceItemIdentifier(rawValue: "FieldValueCell")
  211. static let valueTypeColumn = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCol")
  212. static let valueTypeCell = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCell")
  213. }