Implement new sharing

This commit is contained in:
Alex Grebenyuk
2023-04-16 07:54:29 -04:00
parent c3a22da5b3
commit 4d64af066c
10 changed files with 99 additions and 81 deletions
+1 -4
View File
@@ -817,9 +817,6 @@ extension LoggerStore {
}
}
#warning("add cancellation support")
#warning("check if priority for Task is OK")
/// Creates a copy of the current store at the given URL. The created copy
/// has `.pulse` extension.
///
@@ -833,7 +830,7 @@ extension LoggerStore {
/// - returns: The information about the created store.
@discardableResult
public func export(to targetURL: URL, as documentType: DocumentType = .archive, options: ExportOptions = .init()) async throws -> Info {
try await Task.detached {
try await Task.detached(priority: .userInitiated) {
try self._export(to: targetURL, as: documentType, options: options)
}.value
}
@@ -109,7 +109,7 @@ extension ConsoleRouterView {
private var destinationShareStore: some View {
NavigationView {
ShareStoreView(onDismiss: { router.isShowingShareStore = false })
}.backport.presentationDetents([.medium])
}.backport.presentationDetents([.medium, .large])
}
}
@@ -6,16 +6,38 @@ import SwiftUI
import Pulse
struct ConsoleSearchLogLevelsCell: View {
@ObservedObject var viewModel: ConsoleSearchCriteriaViewModel
@Binding var selection: Set<LoggerStore.Level>
var isAllSelected: Bool {
selection.count == LoggerStore.Level.allCases.count
}
func toggleSelectAll() {
if isAllSelected {
selection = []
} else {
selection = Set(LoggerStore.Level.allCases)
}
}
func binding(forLevel level: LoggerStore.Level) -> Binding<Bool> {
Binding(get: {
self.selection.contains(level)
}, set: { isOn in
if isOn {
self.selection.insert(level)
} else {
self.selection.remove(level)
}
})
}
#if os(macOS)
var body: some View {
VStack(alignment: .leading, spacing: -16) {
HStack {
Spacer()
Button(viewModel.isAllLogLevelsEnabled ? "Deselect All" : "Select All") {
viewModel.isAllLogLevelsEnabled.toggle()
}
Button(isAllSelected ? "Deselect All" : "Select All", action: toggleSelectAll)
}
HStack(spacing: 24) {
makeLevelsSection(levels: [.trace, .debug, .info])
@@ -30,17 +52,27 @@ struct ConsoleSearchLogLevelsCell: View {
VStack(alignment: .leading) {
Spacer()
ForEach(levels, id: \.self) { level in
Toggle(level.name.capitalized, isOn: viewModel.binding(forLevel: level))
Toggle(level.name.capitalized, isOn: binding(forLevel: level))
}
}
}
#else
var body: some View {
ForEach(LoggerStore.Level.allCases, id: \.self) { level in
Checkbox(level.name.capitalized, isOn: viewModel.binding(forLevel: level))
Section {
ForEach(LoggerStore.Level.allCases, id: \.self) { level in
HStack {
Checkbox(level.name.capitalized, isOn: binding(forLevel: level))
Circle()
.frame(width: 8, height: 8)
.foregroundColor(Color.textColor(for: level))
}
}
}
Button(viewModel.isAllLogLevelsEnabled ? "Deselect All" : "Select All") {
viewModel.isAllLogLevelsEnabled.toggle()
Section {
Button("Select Only Errors") {
selection = [.error, .critical]
}
Button(isAllSelected ? "Deselect All" : "Select All", action: toggleSelectAll)
}
}
#endif
@@ -115,7 +115,7 @@ extension ConsoleSearchCriteriaView {
ConsoleSection(header: {
ConsoleSectionHeader(icon: "flag", title: "Levels", filter: $viewModel.criteria.messages.logLevels)
}, content: {
ConsoleSearchLogLevelsCell(viewModel: viewModel)
ConsoleSearchLogLevelsCell(selection: $viewModel.criteria.messages.logLevels.levels)
})
}
@@ -98,33 +98,6 @@ final class ConsoleSearchCriteriaViewModel: ObservableObject {
}
}
// MARK: Binding (LogLevels)
func binding(forLevel level: LoggerStore.Level) -> Binding<Bool> {
Binding(get: {
self.criteria.messages.logLevels.levels.contains(level)
}, set: { isOn in
if isOn {
self.criteria.messages.logLevels.levels.insert(level)
} else {
self.criteria.messages.logLevels.levels.remove(level)
}
})
}
var isAllLogLevelsEnabled: Bool {
get {
criteria.messages.logLevels.levels.count == LoggerStore.Level.allCases.count
}
set {
if newValue {
criteria.messages.logLevels.levels = Set(LoggerStore.Level.allCases)
} else {
criteria.messages.logLevels.levels = []
}
}
}
// MARK: Binding (Labels)
var selectedLabels: Set<String> {
@@ -103,8 +103,11 @@ struct SessionListView: View {
}
}
#warning("fix this not working and not displaying data correctly")
private func getFilteredSessions() -> [LoggerSessionEntity] {
sessions.filter { $0.formattedDate(isCompact: false).localizedCaseInsensitiveContains(filterTerm) }
sessions.filter {
$0.formattedDate.firstRange(of: filterTerm, options: [.caseInsensitive]) != nil
}
}
}
@@ -17,9 +17,6 @@ final class ConsoleSettings: PersistentSettings {
@UserDefault("link-detection")
var isLinkDetectionEnabled = false
@UserDefaultRaw("sharing-level")
var sharingLevel: LoggerStore.Level = .trace
@UserDefaultRaw("sharing-output")
var sharingOutput: ShareStoreOutput = .store
+1
View File
@@ -16,6 +16,7 @@ struct Checkbox<Label: View>: View {
.font(.title3)
.foregroundColor(isOn ? .blue : .separator)
label()
.frame(maxWidth: .infinity, alignment: .leading)
}
.contentShape(Rectangle())
}.buttonStyle(.plain)
+20 -19
View File
@@ -9,7 +9,6 @@ import CoreData
import Pulse
import Combine
#warning("handle preselected sessions")
struct ShareStoreView: View {
/// Preselected sessions.
var sessions: Set<UUID> = []
@@ -19,7 +18,6 @@ struct ShareStoreView: View {
@Environment(\.store) private var store: LoggerStore
#warning("test this on macos")
#if os(macOS)
let onShare: (ShareItems) -> Void
#endif
@@ -30,7 +28,9 @@ struct ShareStoreView: View {
sectionShare
}
.onAppear {
if viewModel.sessions.isEmpty {
if !sessions.isEmpty {
viewModel.sessions = sessions
} else if viewModel.sessions.isEmpty {
viewModel.sessions = [store.session.id]
}
viewModel.store = store
@@ -38,43 +38,44 @@ struct ShareStoreView: View {
.navigationTitle("Share Logs")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarItems)
.navigationBarItems(leading: Button("Cancel", action: onDismiss))
#endif
.sheet(item: $viewModel.shareItems) {
ShareView($0).onCompletion(onDismiss)
}
#if os(macOS)
.onChange(of: viewModel.shareItems) {
onShare($0)
}
.padding()
#endif
}
private var leadingBarItems: some View {
Button("Cancel", action: onDismiss)
}
@ViewBuilder
private var sectionSharingOptions: some View {
Section {
NavigationLink(destination: SessionPickerView(selection: $viewModel.sessions)) {
HStack {
Text("Sessions")
Spacer()
Text(viewModel.selectedSessionTitle)
.foregroundColor(.secondary)
}
InfoRow(title: "Sessions", details: viewModel.selectedSessionTitle)
}
Picker("Minimum Log Level", selection: $viewModel.level) {
Text("Trace").tag(LoggerStore.Level.trace)
Text("Debug").tag(LoggerStore.Level.debug)
Text("Error").tag(LoggerStore.Level.error)
NavigationLink(destination: destinationLogLevels) {
InfoRow(title: "Log Levels", details: viewModel.selectedLevelsTitle)
}
}
Section {
Picker("Output Format", selection: $viewModel.output) {
Text("Pulse File").tag(ShareStoreOutput.store)
Text("Pulse").tag(ShareStoreOutput.store)
Text("Plain Text").tag(ShareStoreOutput.text)
Text("HTML").tag(ShareStoreOutput.html)
}
}
}
private var destinationLogLevels: some View {
Form {
ConsoleSearchLogLevelsCell(selection: $viewModel.logLevels)
}.inlineNavigationTitle("Log Levels")
}
private var sectionShare: some View {
Section {
Button(action: { viewModel.buttonSharedTapped() }) {
+30 -16
View File
@@ -12,7 +12,7 @@ import Combine
@MainActor final class ShareStoreViewModel: ObservableObject {
// Sharing options
@Published var sessions: Set<UUID> = []
@Published var level: LoggerStore.Level
@Published var logLevels = Set(LoggerStore.Level.allCases)
@Published var output: ShareStoreOutput
@Published private(set) var isPreparingForSharing = false
@@ -22,7 +22,6 @@ import Combine
var store: LoggerStore?
init() {
level = ConsoleSettings.shared.sharingLevel
output = ConsoleSettings.shared.sharingOutput
}
@@ -34,7 +33,6 @@ import Combine
}
private func saveSharingOptions() {
ConsoleSettings.shared.sharingLevel = level
ConsoleSettings.shared.sharingOutput = output
}
@@ -47,7 +45,8 @@ import Combine
Task {
do {
self.shareItems = try await prepareForSharing(store: store, predicate: predicate, output: output)
let options = LoggerStore.ExportOptions(predicate: predicate, sessions: sessions)
self.shareItems = try await prepareForSharing(store: store, options: options)
} catch {
guard !(error is CancellationError) else { return }
self.errorMessage = error.localizedDescription
@@ -78,41 +77,56 @@ import Combine
return try? store?.viewContext.fetch(request).first
}
#warning("add sessions predicate")
var selectedLevelsTitle: String {
if logLevels.count == 1 {
return logLevels.first!.name.capitalized
} else if logLevels.count == 0 {
return "–"
} else if logLevels == [.error, .critical] {
return "Errors"
} else if logLevels == [.warning, .error, .critical] {
return "Warnings & Errors"
} else if logLevels.count == LoggerStore.Level.allCases.count {
return "All"
} else {
return "\(logLevels.count)"
}
}
private var predicate: NSPredicate? {
var predicates: [NSPredicate] = []
if level == .trace {
return nil
if logLevels != Set(LoggerStore.Level.allCases) {
predicates.append(.init(format: "level IN %@", logLevels.map(\.rawValue)))
}
if level != .trace {
predicates.append(.init(format: "level >= %i", level.rawValue))
if !sessions.isEmpty {
predicates.append(.init(format: "session IN %@", sessions))
}
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
}
private func prepareForSharing(store: LoggerStore, predicate: NSPredicate?, output: ShareStoreOutput) async throws -> ShareItems {
private func prepareForSharing(store: LoggerStore, options: LoggerStore.ExportOptions) async throws -> ShareItems {
switch output {
case .store:
return try await prepareStoreForSharing(store: store, predicate: predicate)
return try await prepareStoreForSharing(store: store, options: options)
case .text, .html:
let output: ShareOutput = output == .text ? .plainText : .html
return try await prepareForSharing(store: store, output: output, predicate: predicate)
return try await prepareForSharing(store: store, output: output, options: options)
}
}
private func prepareStoreForSharing(store: LoggerStore, predicate: NSPredicate?) async throws -> ShareItems {
private func prepareStoreForSharing(store: LoggerStore, options: LoggerStore.ExportOptions) async throws -> ShareItems {
let directory = TemporaryDirectory()
let logsURL = directory.url.appendingPathComponent("logs-\(makeCurrentDate()).\(output.fileExtension)")
try await store.export(to: logsURL, options: .init(predicate: predicate))
try await store.export(to: logsURL, options: options)
return ShareItems([logsURL], cleanup: directory.remove)
}
private func prepareForSharing(store: LoggerStore, output: ShareOutput, predicate: NSPredicate?) async throws -> ShareItems {
private func prepareForSharing(store: LoggerStore, output: ShareOutput, options: LoggerStore.ExportOptions) async throws -> ShareItems {
let entities = try await withUnsafeThrowingContinuation { continuation in
store.backgroundContext.perform {
let request = NSFetchRequest<LoggerMessageEntity>(entityName: "\(LoggerMessageEntity.self)")
request.predicate = predicate
request.predicate = options.predicate // improtant: contains sessions
let result = Result(catching: { try store.backgroundContext.fetch(request) })
continuation.resume(with: result)
}