mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
Implement new sharing
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user